feat: whyrating - initial project from turbostarter boilerplate

This commit is contained in:
Alejandro Gutiérrez
2026-02-04 01:54:52 +01:00
commit 5cdc07cd39
1618 changed files with 338230 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
/**
* Claude Code Adapter
*
* Standalone interface for Claude Code-specific operations.
* Generates CLAUDE.md and syncs commands to .claude/commands/.
*/
import { writeFile, mkdir, rm, readdir } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { stringify as stringifyYaml } from 'yaml';
import { loadKnowledge, type KnowledgeData, type SyncOptions } from '../sync.js';
import type { SyncResult } from '../types.js';
// ============================================
// Types
// ============================================
/** Options for generating CLAUDE.md content */
export interface ClaudeMdOptions {
includeCapabilities?: boolean;
includeRules?: boolean;
includeWisdom?: boolean;
headerContent?: string;
footerContent?: string;
}
/** Structure of generated CLAUDE.md */
export interface ClaudeMdStructure {
projectName: string;
mainContent: string;
capabilities?: string;
rules?: string;
wisdomRefs?: Array<{ name: string; heading: string }>;
}
/** Options for syncing commands */
export interface SyncCommandsOptions extends SyncOptions {
deleteStale?: boolean;
}
// ============================================
// Utilities
// ============================================
function extractFirstHeading(content: string): string {
const match = content.match(/^#\s+(.+)$/m);
return match ? match[1].trim() : 'Untitled';
}
async function ensureDir(dirPath: string): Promise<void> {
await mkdir(dirPath, { recursive: true });
}
// ============================================
// CLAUDE.md Generation
// ============================================
/** Generate CLAUDE.md content from knowledge data */
export function generateClaudeMd(
knowledge: KnowledgeData,
options: ClaudeMdOptions = {}
): string {
const {
includeCapabilities = true,
includeRules = true,
includeWisdom = true,
headerContent,
footerContent,
} = options;
const sections: string[] = [];
if (headerContent) sections.push(headerContent, '');
sections.push(`# ${knowledge.projectName}`, '');
if (knowledge.orientation || knowledge.summary) {
sections.push(knowledge.orientation || knowledge.summary, '');
}
if (includeCapabilities && Object.keys(knowledge.capabilities).length > 0) {
sections.push('## Capabilities', '', '```yaml', stringifyYaml(knowledge.capabilities).trim(), '```', '');
}
if (includeRules && Object.keys(knowledge.rules).length > 0) {
sections.push('## Rules', '', '```yaml', stringifyYaml(knowledge.rules).trim(), '```', '');
}
if (includeWisdom && knowledge.wisdom.size > 0) {
sections.push('## Wisdom', '', 'Cached patterns and answers:', '');
for (const [name, content] of knowledge.wisdom) {
sections.push(`- **${name}**: ${extractFirstHeading(content)}`);
}
sections.push('');
}
if (footerContent) sections.push(footerContent, '');
return sections.join('\n');
}
// ============================================
// Commands Sync
// ============================================
/** Sync commands to .claude/commands/ directory */
export async function syncCommands(
projectRoot: string,
commands: Map<string, string>,
options: SyncCommandsOptions = {}
): Promise<SyncResult> {
const commandsDir = join(projectRoot, '.claude', 'commands');
const filesWritten: string[] = [];
const filesDeleted: string[] = [];
try {
if (!options.dryRun) await ensureDir(commandsDir);
const currentCommands = new Set<string>();
for (const [name, content] of commands) {
const fileName = `${name}.md`;
currentCommands.add(fileName);
const commandPath = join(commandsDir, fileName);
if (!options.dryRun) {
await ensureDir(dirname(commandPath));
await writeFile(commandPath, content, 'utf-8');
}
filesWritten.push(commandPath);
}
if (options.deleteStale !== false) {
try {
const entries = await readdir(commandsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith('.md') && !currentCommands.has(entry.name)) {
const filePath = join(commandsDir, entry.name);
if (!options.dryRun) await rm(filePath);
filesDeleted.push(filePath);
}
}
} catch { /* Directory doesn't exist */ }
}
return { tool: 'claude', filesWritten, filesDeleted, success: true };
} catch (error) {
return { tool: 'claude', filesWritten, filesDeleted, success: false, error: error instanceof Error ? error.message : String(error) };
}
}
// ============================================
// Full Sync
// ============================================
/** Full sync to Claude Code format (CLAUDE.md + .claude/commands/) */
export async function syncToClaudeCode(
projectRoot: string,
options: SyncOptions & ClaudeMdOptions = {}
): Promise<SyncResult> {
const filesWritten: string[] = [];
const filesDeleted: string[] = [];
try {
const knowledge = await loadKnowledge(projectRoot);
if (knowledge.summary || knowledge.orientation) {
const claudeMdPath = join(projectRoot, 'CLAUDE.md');
if (!options.dryRun) await writeFile(claudeMdPath, generateClaudeMd(knowledge, options), 'utf-8');
filesWritten.push(claudeMdPath);
}
if (knowledge.commands.size > 0) {
const result = await syncCommands(projectRoot, knowledge.commands, options);
filesWritten.push(...result.filesWritten);
filesDeleted.push(...result.filesDeleted);
if (!result.success) return { tool: 'claude', filesWritten, filesDeleted, success: false, error: result.error };
}
return { tool: 'claude', filesWritten, filesDeleted, success: true };
} catch (error) {
return { tool: 'claude', filesWritten, filesDeleted, success: false, error: error instanceof Error ? error.message : String(error) };
}
}
// ============================================
// Re-exports
// ============================================
export { loadKnowledge, type KnowledgeData, type SyncOptions } from '../sync.js';
export type { SyncResult } from '../types.js';

View File

@@ -0,0 +1,299 @@
/**
* Cursor Adapter Module
*
* Provides a standalone interface for Cursor-specific operations.
* Generates and manages .mdc files in .cursor/rules/.
*/
import { readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import {
loadKnowledge,
syncToTool,
type SyncOptions,
type KnowledgeData,
} from '../sync.js';
import type { SyncResult, ToolConfig } from '../types.js';
// ============================================
// Types
// ============================================
/**
* Options for generating an MDC file
*/
export interface MdcFileOptions {
/** Description for the frontmatter */
description: string;
/** If true, rule always applies to all files */
alwaysApply?: boolean;
/** Glob patterns for files this rule applies to (optional) */
globs?: string[];
}
/**
* Structure of a generated MDC file
*/
export interface MdcFile {
/** File name (without path) */
name: string;
/** Full MDC content including frontmatter */
content: string;
/** Parsed frontmatter */
frontmatter: MdcFrontmatter;
}
/**
* MDC frontmatter structure
*/
export interface MdcFrontmatter {
description: string;
alwaysApply?: boolean;
globs?: string[];
}
/**
* Validation result for rules directory
*/
export interface RulesValidation {
/** Whether the rules directory exists */
exists: boolean;
/** List of .mdc files found */
files: string[];
/** List of issues found */
issues: ValidationIssue[];
/** Overall validity */
valid: boolean;
}
/**
* Individual validation issue
*/
export interface ValidationIssue {
file: string;
type: 'missing-frontmatter' | 'invalid-frontmatter' | 'empty-content';
message: string;
}
// ============================================
// Constants
// ============================================
const DEFAULT_RULES_PATH = '.cursor/rules';
// ============================================
// MDC File Generation
// ============================================
/**
* Generate MDC frontmatter string
*/
function formatMdcFrontmatter(options: MdcFileOptions): string {
const lines = ['---', `description: ${options.description}`];
if (options.alwaysApply) {
lines.push('alwaysApply: true');
}
if (options.globs && options.globs.length > 0) {
lines.push('globs:');
for (const glob of options.globs) {
lines.push(` - ${glob}`);
}
}
lines.push('---', '');
return lines.join('\n');
}
/**
* Generate a single .mdc file content
*
* @param name - File name (without .mdc extension)
* @param content - Markdown content (without frontmatter)
* @param options - MDC file options
* @returns Complete MDC file structure
*/
export function generateMdcFile(
name: string,
content: string,
options: MdcFileOptions
): MdcFile {
const frontmatter = formatMdcFrontmatter(options);
const fullContent = frontmatter + content;
return {
name: name.endsWith('.mdc') ? name : `${name}.mdc`,
content: fullContent,
frontmatter: {
description: options.description,
alwaysApply: options.alwaysApply,
globs: options.globs,
},
};
}
// ============================================
// Sync Operations
// ============================================
/**
* Sync cognitive knowledge to .cursor/rules
*
* @param projectRoot - Project root directory
* @param options - Sync options
* @returns Sync result
*/
export async function syncToRules(
projectRoot: string,
options: SyncOptions = {}
): Promise<SyncResult> {
const knowledge = await loadKnowledge(projectRoot);
const toolConfig: ToolConfig = {
name: 'cursor',
enabled: true,
outputPath: DEFAULT_RULES_PATH,
};
return syncToTool('cursor', projectRoot, toolConfig, knowledge, options);
}
/**
* Sync with pre-loaded knowledge data
*
* @param projectRoot - Project root directory
* @param knowledge - Pre-loaded knowledge data
* @param options - Sync options
* @returns Sync result
*/
export async function syncToRulesWithKnowledge(
projectRoot: string,
knowledge: KnowledgeData,
options: SyncOptions = {}
): Promise<SyncResult> {
const toolConfig: ToolConfig = {
name: 'cursor',
enabled: true,
outputPath: DEFAULT_RULES_PATH,
};
return syncToTool('cursor', projectRoot, toolConfig, knowledge, options);
}
// ============================================
// Validation
// ============================================
/**
* Validate the .cursor/rules directory
*
* Checks for:
* - Directory existence
* - Valid .mdc files with proper frontmatter
* - Non-empty content
*
* @param projectRoot - Project root directory
* @returns Validation result
*/
export async function validateRules(
projectRoot: string
): Promise<RulesValidation> {
const rulesDir = join(projectRoot, DEFAULT_RULES_PATH);
const issues: ValidationIssue[] = [];
const files: string[] = [];
// Check if directory exists
try {
const stats = await stat(rulesDir);
if (!stats.isDirectory()) {
return {
exists: false,
files: [],
issues: [
{
file: rulesDir,
type: 'missing-frontmatter',
message: 'Rules path exists but is not a directory',
},
],
valid: false,
};
}
} catch {
return {
exists: false,
files: [],
issues: [],
valid: true, // Not existing is valid (just means no rules yet)
};
}
// List .mdc files
try {
const entries = await readdir(rulesDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith('.mdc')) {
files.push(entry.name);
// Read and validate file
const filePath = join(rulesDir, entry.name);
const { readFile } = await import('node:fs/promises');
const content = await readFile(filePath, 'utf-8');
// Check for frontmatter
if (!content.startsWith('---')) {
issues.push({
file: entry.name,
type: 'missing-frontmatter',
message: 'File does not start with YAML frontmatter (---)',
});
continue;
}
// Check for closing frontmatter
const secondDash = content.indexOf('---', 3);
if (secondDash === -1) {
issues.push({
file: entry.name,
type: 'invalid-frontmatter',
message: 'Frontmatter is not properly closed',
});
continue;
}
// Check for content after frontmatter
const bodyContent = content.slice(secondDash + 3).trim();
if (!bodyContent) {
issues.push({
file: entry.name,
type: 'empty-content',
message: 'File has no content after frontmatter',
});
}
}
}
} catch (error) {
issues.push({
file: rulesDir,
type: 'invalid-frontmatter',
message: `Failed to read rules directory: ${error instanceof Error ? error.message : String(error)}`,
});
}
return {
exists: true,
files,
issues,
valid: issues.length === 0,
};
}
// ============================================
// Re-exports
// ============================================
// Re-export sync types for convenience
export type { SyncOptions, KnowledgeData } from '../sync.js';
export type { SyncResult, ToolConfig } from '../types.js';

View File

@@ -0,0 +1,8 @@
/**
* Tool-specific adapters for Cognitive Context
*
* @packageDocumentation
*/
export * from './cursor.js';
export * from './claude.js';