diff --git a/docs/docs/cli/template.md b/docs/docs/cli/template.md index 45bcc61..70167e8 100644 --- a/docs/docs/cli/template.md +++ b/docs/docs/cli/template.md @@ -55,6 +55,7 @@ Templates are organized into categories: - `devops` - DevOps tools and automation - `mobile` - Mobile app projects - `tools` - CLI tools and utilities +- `seo` - SEO and Answer Engine Optimization toolkits ## Examples diff --git a/src/cli.ts b/src/cli.ts index f410560..5e239e9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,5 @@ #!/usr/bin/env node -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; import chalk from 'chalk'; import { Command } from 'commander'; import { authCommand } from './commands/auth.js'; @@ -19,13 +16,10 @@ import { sourceCommand } from './commands/source.js'; import { templateCommand } from './commands/template.js'; import { startMcpServer } from './mcp/server.js'; import { formatPresetsHelp, getPresetNames } from './presets/index.js'; +import { getPackageVersion } from './utils/version.js'; import { runIdeaMode, runWizard } from './wizard/index.js'; -// Read version from package.json dynamically -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')); -const VERSION = packageJson.version; +const VERSION = getPackageVersion(); const program = new Command(); @@ -264,7 +258,10 @@ program program .command('template [action] [args...]') .description('Browse and use project templates from ralph-templates') - .option('--category ', 'Filter by category (web-dev, blockchain, devops, mobile, tools)') + .option( + '--category ', + 'Filter by category (web-dev, blockchain, devops, mobile, tools, seo)' + ) .option('--refresh', 'Force refresh the cache') .option('--auto', 'Skip confirmation prompts') .option('--output-dir ', 'Directory to create the project in') diff --git a/src/commands/template.ts b/src/commands/template.ts index a3e7c88..37be65c 100644 --- a/src/commands/template.ts +++ b/src/commands/template.ts @@ -374,7 +374,7 @@ ${chalk.bold('Commands:')} browse Interactive template browser ${chalk.bold('Options:')} - --category Filter by category (web-dev, blockchain, devops, mobile, tools) + --category Filter by category (web-dev, blockchain, devops, mobile, tools, seo) --refresh Force refresh the cache --auto Skip confirmation prompts --output-dir Directory to create the project in diff --git a/src/mcp/prompts.ts b/src/mcp/prompts.ts index a01c84e..277da6e 100644 --- a/src/mcp/prompts.ts +++ b/src/mcp/prompts.ts @@ -45,16 +45,17 @@ export function getPrompts(): Prompt[] { }, { name: 'fetch_and_build', - description: 'Fetch a spec from a source and start building', + description: + 'Fetch a spec from an external source (GitHub, Linear, Notion, Figma) and start building', arguments: [ { name: 'source', - description: 'Source to fetch from (url, github, todoist, linear, notion)', + description: 'Source to fetch from (url, github, linear, notion, figma)', required: true, }, { name: 'identifier', - description: 'Source identifier (URL, project name, etc.)', + description: 'Source identifier (URL, project name, issue number, Figma file URL, etc.)', required: true, }, { @@ -64,6 +65,62 @@ export function getPrompts(): Prompt[] { }, ], }, + { + name: 'figma_to_code', + description: + 'Extract a Figma design and build it as code. Supports design specs, tokens, components, and content extraction.', + arguments: [ + { + name: 'figma_url', + description: 'Figma file or frame URL', + required: true, + }, + { + name: 'framework', + description: + 'Target framework: react, vue, svelte, astro, nextjs, nuxt, html (default: react)', + required: false, + }, + { + name: 'mode', + description: + 'Extraction mode: spec (full design spec), tokens (design tokens as CSS/Tailwind), components (component code), content (text/IA extraction)', + required: false, + }, + { + name: 'path', + description: 'Project directory to build in', + required: false, + }, + ], + }, + { + name: 'batch_issues', + description: + 'Process multiple GitHub or Linear issues automatically in sequence. Each issue becomes a task with its own branch, commits, and PR.', + arguments: [ + { + name: 'source', + description: 'Issue source: github or linear', + required: true, + }, + { + name: 'project', + description: 'GitHub repo (owner/repo) or Linear project name', + required: true, + }, + { + name: 'label', + description: 'Filter issues by label (e.g., "good first issue", "bug", "enhancement")', + required: false, + }, + { + name: 'path', + description: 'Project directory path', + required: false, + }, + ], + }, ]; } @@ -150,13 +207,14 @@ Show me where we are!`, type: 'text', text: `Please fetch a spec and build a project from it. -Source: ${args?.source || '(specify source)'} +Source: ${args?.source || '(specify source: github, linear, notion, or figma)'} Identifier: ${args?.identifier || '(specify identifier)'} Path: ${cwd} -1. First, initialize Ralph Playbook at the path if needed -2. Use ralph_run with the --from option to fetch the spec and start building -3. Monitor progress and help resolve any issues +1. First, use ralph_fetch_spec to preview the spec content +2. Initialize Ralph Playbook at the path if needed +3. Use ralph_run with the --from option to fetch the spec and start building +4. Monitor progress and help resolve any issues Let's build it!`, }, @@ -164,6 +222,67 @@ Let's build it!`, ], }; + case 'figma_to_code': { + const framework = args?.framework || 'react'; + const displayFramework = framework.charAt(0).toUpperCase() + framework.slice(1); + return { + description: 'Convert Figma design to code', + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please extract a Figma design and build it as code. + +Figma URL: ${args?.figma_url || '(specify Figma file URL)'} +Framework: ${framework} +Mode: ${args?.mode || 'spec'} +Path: ${cwd} + +1. First, use ralph_fetch_spec with source "figma" to extract the design: + - Use mode "${args?.mode || 'spec'}" to get ${args?.mode === 'tokens' ? 'design tokens' : args?.mode === 'components' ? 'component structure' : args?.mode === 'content' ? 'text content and IA' : 'the full design specification'} +2. Review the extracted spec — check colors, typography, spacing, and component structure +3. Initialize Ralph Playbook if needed, then use ralph_run to build the ${displayFramework} implementation +4. The agent will iterate until the UI matches the design spec, running validation between iterations + +Let's bring this design to life!`, + }, + }, + ], + }; + } + + case 'batch_issues': + return { + description: 'Process multiple issues automatically', + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please process multiple issues from ${args?.source || '(github or linear)'} automatically. + +Source: ${args?.source || '(specify: github or linear)'} +Project: ${args?.project || '(specify repo or project name)'} +${args?.label ? `Label filter: ${args.label}` : 'Label: (all issues)'} +Path: ${cwd} + +1. Use ralph_run with auto mode to batch-process issues from ${args?.source || 'the source'}: + - Set from="${args?.source || '(github or linear)'}" and project="${args?.project || '(specify)'}" + ${args?.label ? `- Filter by label: "${args.label}"` : ''} + - Enable auto=true, commit=true, and validate=true + - Each issue gets its own branch + - Code changes are validated and auto-committed + - A PR is created for each completed issue +2. Monitor progress across all issues +3. If an individual issue fails, note the failure and continue to the next one + +Let's batch process these issues!`, + }, + }, + ], + }; + default: throw new Error(`Unknown prompt: ${name}`); } diff --git a/src/mcp/resources.ts b/src/mcp/resources.ts index 25fa021..dfbd521 100644 --- a/src/mcp/resources.ts +++ b/src/mcp/resources.ts @@ -67,6 +67,18 @@ export async function getResources(): Promise { } } + // Activity log + const activityPath = join(cwd, '.ralph', 'activity.md'); + if (existsSync(activityPath)) { + resources.push({ + uri: 'ralph://project/activity', + name: 'Activity Log', + description: + 'Loop execution history with timing, cost data, and task outcomes (.ralph/activity.md)', + mimeType: 'text/markdown', + }); + } + return resources; } @@ -105,6 +117,10 @@ export async function handleResourceRead(uri: string): Promise<{ filePath = join(cwd, 'PROMPT_plan.md'); break; + case 'activity': + filePath = join(cwd, '.ralph', 'activity.md'); + break; + default: // Handle specs if (resourcePath.startsWith('specs/')) { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 2ac073b..ac04994 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -8,6 +8,7 @@ import { ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; +import { getPackageVersion } from '../utils/version.js'; import { getPrompts, handleGetPrompt } from './prompts.js'; import { getResources, handleResourceRead } from './resources.js'; import { getTools, handleToolCall } from './tools.js'; @@ -19,7 +20,7 @@ export function createMcpServer(): Server { const server = new Server( { name: 'ralph-starter', - version: '0.1.0', + version: getPackageVersion(), }, { capabilities: { diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 133ab94..049a2cc 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -41,6 +41,31 @@ const toolSchemas = { ralph_validate: z.object({ path: z.string().describe('Project path'), }), + + ralph_list_presets: z.object({ + category: z + .string() + .optional() + .describe('Filter by category (development, debugging, review, documentation, specialized)'), + }), + + ralph_fetch_spec: z.object({ + path: z.string().min(1).describe('Project directory path'), + source: z + .enum(['github', 'linear', 'notion', 'figma']) + .describe('Integration source to fetch from'), + identifier: z + .string() + .describe( + 'Source identifier: GitHub repo/issue URL, Linear project name, Notion page URL, or Figma file URL' + ), + mode: z + .string() + .optional() + .describe('Figma-specific mode: spec, tokens, components, content, assets'), + project: z.string().optional().describe('Project or team filter (for Linear/GitHub)'), + label: z.string().optional().describe('Label filter (for GitHub/Linear issues)'), + }), }; /** @@ -51,17 +76,17 @@ export function getTools(): Tool[] { { name: 'ralph_init', description: - 'Initialize Ralph Playbook in a project. Creates AGENTS.md, PROMPT_plan.md, PROMPT_build.md, specs/, and IMPLEMENTATION_PLAN.md.', + 'Initialize Ralph Playbook in a project directory. Creates the scaffolding files needed for autonomous coding: AGENTS.md (agent config), PROMPT_plan.md and PROMPT_build.md (workflow prompts), specs/ directory, and IMPLEMENTATION_PLAN.md. Auto-detects project type (Node.js, Python, Rust, Go) and configures validation commands.', inputSchema: { type: 'object', properties: { path: { type: 'string', - description: 'Project path to initialize', + description: 'Absolute path to the project directory to initialize', }, name: { type: 'string', - description: 'Project name', + description: 'Project name (defaults to directory name)', }, }, required: ['path'], @@ -70,17 +95,17 @@ export function getTools(): Tool[] { { name: 'ralph_plan', description: - 'Create an implementation plan from specs. Analyzes specs/ directory and generates IMPLEMENTATION_PLAN.md.', + 'Create an implementation plan from specification files. Analyzes the specs/ directory using an AI coding agent and generates a structured IMPLEMENTATION_PLAN.md with checkboxed tasks. The plan breaks down the spec into actionable development tasks.', inputSchema: { type: 'object', properties: { path: { type: 'string', - description: 'Project path', + description: 'Project directory path containing specs/ folder', }, auto: { type: 'boolean', - description: 'Run in automated mode (skip permissions)', + description: 'Run in automated mode without interactive prompts', }, }, required: ['path'], @@ -89,41 +114,43 @@ export function getTools(): Tool[] { { name: 'ralph_run', description: - 'Execute an autonomous coding loop. Uses the implementation plan and agents to build the project.', + 'Execute an autonomous AI coding loop that iterates until task completion. The agent reads specs, writes code, runs validation (tests/lint/build), and auto-commits. Supports Claude Code, Cursor, Codex, OpenCode, Copilot, Gemini CLI, Amp, and Openclaw agents. Can fetch tasks from GitHub issues, Linear tickets, Notion pages, or Figma designs. Use workflow presets (see ralph_list_presets) to configure behavior.', inputSchema: { type: 'object', properties: { path: { type: 'string', - description: 'Project path', + description: 'Project directory path', }, task: { type: 'string', - description: 'Task to execute (optional if using Ralph Playbook)', + description: + 'Task description to execute. Optional if using Ralph Playbook (reads from IMPLEMENTATION_PLAN.md)', }, auto: { type: 'boolean', - description: 'Run in automated mode (skip permissions)', + description: 'Run in automated mode — processes all tasks without interactive prompts', }, commit: { type: 'boolean', - description: 'Auto-commit changes after each task', + description: 'Auto-commit changes after each completed task', }, validate: { type: 'boolean', - description: 'Run tests/lint/build validation', + description: 'Run validation commands (tests, lint, build) after each iteration', }, from: { type: 'string', - description: 'Source to fetch spec from (file, url, github, todoist, linear, notion)', + description: + 'Source integration to fetch spec from: file, url, github, linear, notion, figma', }, project: { type: 'string', - description: 'Project/repo name for source integrations', + description: 'Project/repo name filter for GitHub or Linear integrations', }, label: { type: 'string', - description: 'Label filter for source integrations', + description: 'Label filter to select specific issues from GitHub or Linear', }, }, required: ['path'], @@ -132,13 +159,13 @@ export function getTools(): Tool[] { { name: 'ralph_status', description: - 'Check Ralph Playbook status. Shows available files, implementation progress, and agent availability.', + 'Check Ralph Playbook status for a project. Returns available playbook files, implementation plan progress (completed/total tasks), and spec files. Useful for understanding where a project stands before continuing work.', inputSchema: { type: 'object', properties: { path: { type: 'string', - description: 'Project path', + description: 'Project directory path to check', }, }, required: ['path'], @@ -147,18 +174,71 @@ export function getTools(): Tool[] { { name: 'ralph_validate', description: - 'Run validation commands (tests, lint, build). Checks project health and reports issues.', + 'Run all detected validation commands (tests, linting, build) for a project. Auto-detects validation commands from package.json scripts, Makefile targets, and common patterns. Returns pass/fail status with output for each command.', inputSchema: { type: 'object', properties: { path: { type: 'string', - description: 'Project path', + description: 'Project directory path to validate', }, }, required: ['path'], }, }, + { + name: 'ralph_list_presets', + description: + 'List all available workflow presets for ralph-starter. Presets configure the coding loop behavior: iteration limits, validation, auto-commit, and specialized prompts. Categories include Development (feature, TDD, refactor), Debugging (debug, incident-response), Review (code review, PR review, adversarial), Documentation, and Specialized (API design, migration, performance).', + inputSchema: { + type: 'object', + properties: { + category: { + type: 'string', + description: + 'Filter by category: development, debugging, review, documentation, specialized. Returns all if omitted.', + }, + }, + }, + }, + { + name: 'ralph_fetch_spec', + description: + 'Fetch a specification from an external integration without running the coding loop. Returns the raw spec content as markdown. Supports GitHub (issues, PRs), Linear (tickets by project/team), Notion (pages, databases), and Figma (design specs, tokens, components, content, assets). Use this to preview what will be built before committing to a full run.', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Project directory path (used as working directory)', + }, + source: { + type: 'string', + description: 'Integration source: github, linear, notion, or figma', + enum: ['github', 'linear', 'notion', 'figma'], + }, + identifier: { + type: 'string', + description: + 'Source identifier — GitHub: repo URL or "owner/repo#123", Linear: project name, Notion: page URL, Figma: file URL', + }, + mode: { + type: 'string', + description: + 'Figma-specific extraction mode: spec (design specs), tokens (design tokens), components (component code), content (text extraction), assets (icons/images)', + }, + project: { + type: 'string', + description: 'Project or team name filter (for Linear and GitHub)', + }, + label: { + type: 'string', + description: 'Label filter for issue selection (for GitHub and Linear)', + }, + }, + required: ['path', 'source', 'identifier'], + }, + }, ]; } @@ -186,6 +266,12 @@ export async function handleToolCall( case 'ralph_validate': return await handleValidate(args); + case 'ralph_list_presets': + return await handleListPresets(args); + + case 'ralph_fetch_spec': + return await handleFetchSpec(args); + default: return { content: [ @@ -344,6 +430,74 @@ async function handleValidate( }; } +async function handleListPresets( + args: Record | undefined +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + const parsed = toolSchemas.ralph_list_presets.parse(args ?? {}); + + const { getPresetsByCategory } = await import('../presets/index.js'); + + const allCategories = getPresetsByCategory(); + const filterCategory = parsed.category?.toLowerCase(); + + const result: Record< + string, + Array<{ + name: string; + description: string; + maxIterations: number; + validate: boolean; + commit: boolean; + }> + > = {}; + + for (const [category, presets] of Object.entries(allCategories)) { + if (filterCategory && category.toLowerCase() !== filterCategory) { + continue; + } + result[category] = presets.map((p) => ({ + name: p.name, + description: p.description, + maxIterations: p.maxIterations, + validate: p.validate, + commit: p.commit, + })); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} + +async function handleFetchSpec( + args: Record | undefined +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + const parsed = toolSchemas.ralph_fetch_spec.parse(args); + + const { fetchFromIntegration } = await import('../integrations/index.js'); + + const options: Record = { path: parsed.path }; + if (parsed.mode) options.mode = parsed.mode; + if (parsed.project) options.project = parsed.project; + if (parsed.label) options.label = parsed.label; + + const result = await fetchFromIntegration(parsed.source, parsed.identifier, options); + + return { + content: [ + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), + }, + ], + }; +} + function formatInitResult(result: InitCoreResult): string { if (result.success) { return `Successfully initialized Ralph Playbook at ${result.path}\n\nFiles created:\n${result.filesCreated.map((f) => `- ${f}`).join('\n')}`; diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 0000000..d00cd43 --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const DEFAULT_VERSION = '0.1.0'; + +let cachedVersion: string | null = null; + +/** + * Resolves the package version by walking up parent directories + * from the compiled dist/ location to find package.json. + */ +export function getPackageVersion(): string { + if (cachedVersion) return cachedVersion; + + const __dirname = dirname(fileURLToPath(import.meta.url)); + + // Walk up from current file to find package.json + // Handles both src/ (dev) and dist/ (built) depths + const candidates = [ + join(__dirname, '..', 'package.json'), // src/utils/ → package.json + join(__dirname, '..', '..', 'package.json'), // dist/utils/ → package.json + join(__dirname, '..', '..', '..', 'package.json'), // dist/src/utils/ → package.json + ]; + + for (const candidate of candidates) { + try { + const pkg = JSON.parse(readFileSync(candidate, 'utf-8')); + if (pkg.version) { + cachedVersion = pkg.version as string; + return cachedVersion; + } + } catch { + // Try next candidate + } + } + + cachedVersion = DEFAULT_VERSION; + return cachedVersion; +}