diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index f3129a835..484f6856e 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -20,7 +20,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - **Automatic hooks**: file save, git commit, dependency change, and context file change handlers - **Follow-up suggestions**: context-aware buttons after each command -- **`/diag` command**: diagnose extension issues with step-by-step timing + +### Changed + +- Slash-command surface reconciled to exactly the 45 commands the + extension dispatches. Singular commands renamed to plurals: + `/change`→`/changes`, `/dep`→`/deps`, `/task`→`/tasks`, + `/permission`→`/permissions`. Dedicated `/decisions` and `/learnings` + added alongside `/add`. + +### Removed + +- `/loop` and `/diag` slash commands. `/site` is now reached through + `/journal site`, and `/doctor` runs inline rather than as a top-level + slash command. ### Configuration diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 7c3065327..0ea64916e 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -92,13 +92,17 @@ "description": "Reconcile context with codebase" }, { - "name": "task", - "description": "Task operations: complete, archive, snapshot" + "name": "complete", + "description": "Mark a task complete by number or description" }, { "name": "remind", "description": "Manage session-scoped reminders" }, + { + "name": "tasks", + "description": "Task operations: complete, archive, snapshot" + }, { "name": "pad", "description": "Encrypted scratchpad for sensitive notes" @@ -112,52 +116,72 @@ "description": "System diagnostics and bootstrap" }, { - "name": "memory", - "description": "Memory bridge operations (sync, status, diff, import, publish)" + "name": "wrapup", + "description": "End-of-session wrap-up: capture learnings, decisions, and tasks" }, { - "name": "journal", - "description": "Journal management (site, obsidian)" + "name": "remember", + "description": "Recall project context and present a structured readback" }, { - "name": "doctor", - "description": "Context health diagnostics" + "name": "next", + "description": "Suggest what to work on next" }, { - "name": "config", - "description": "Runtime configuration (switch, status, schema)" + "name": "brainstorm", + "description": "Design before implementation: turn a vague idea into a validated design" + }, + { + "name": "reflect", + "description": "Reflect on session progress at a natural breakpoint" + }, + { + "name": "spec", + "description": "Scaffold a feature spec from the project template" + }, + { + "name": "implement", + "description": "Execute a plan step-by-step with verification" + }, + { + "name": "verify", + "description": "Verify a change does what it should by exercising it end-to-end" + }, + { + "name": "map", + "description": "Build and maintain architecture maps (ARCHITECTURE.md)" }, { "name": "prompt", "description": "Prompt templates (list, add, show, rm)" }, { - "name": "why", - "description": "Show rationale for context design decisions" + "name": "blog", + "description": "Generate a blog post draft from project progress" }, { - "name": "change", - "description": "Show recent codebase changes" + "name": "changelog", + "description": "Generate a themed blog post from commits between releases" }, { - "name": "dep", - "description": "Show project dependencies" + "name": "check-links", + "description": "Audit docs for dead links" }, { - "name": "guide", - "description": "Quick start guide" + "name": "journal", + "description": "Journal management (site, obsidian)" }, { - "name": "permission", - "description": "Permission snapshot and restore" + "name": "consolidate", + "description": "Consolidate redundant entries in LEARNINGS.md or DECISIONS.md" }, { - "name": "site", - "description": "Documentation site (feed)" + "name": "audit", + "description": "Run a code-level convention audit across the codebase" }, { - "name": "loop", - "description": "Generate autonomous iteration scripts" + "name": "worktree", + "description": "Manage git worktrees for parallel agent development" }, { "name": "pause", @@ -167,13 +191,45 @@ "name": "resume", "description": "Resume context hooks" }, + { + "name": "memory", + "description": "Memory bridge operations (sync, status, diff, import, publish)" + }, + { + "name": "decisions", + "description": "Manage DECISIONS.md architectural decisions" + }, + { + "name": "learnings", + "description": "Manage LEARNINGS.md gotchas and lessons" + }, + { + "name": "config", + "description": "Runtime configuration (switch, status, schema)" + }, + { + "name": "permissions", + "description": "Permission snapshot and restore" + }, + { + "name": "changes", + "description": "Show recent codebase changes" + }, + { + "name": "deps", + "description": "Show project dependencies" + }, + { + "name": "guide", + "description": "Quick start guide" + }, { "name": "reindex", "description": "Rebuild context file indices" }, { - "name": "diag", - "description": "Diagnose extension issues — times each step to find hangs" + "name": "why", + "description": "Show rationale for context design decisions" } ], "disambiguation": [ diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 0b618bb04..197578452 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -20,6 +20,73 @@ let resolvedCtxPath: string | undefined; // Extension context — set during activation let extensionCtx: vscode.ExtensionContext | undefined; +// Status bar item for context reminders +let reminderStatusBar: vscode.StatusBarItem | undefined; + +// --- Detection ring: deny patterns for governance --- +const DENY_COMMAND_PATTERNS: RegExp[] = [ + /\bsudo\s/, + /\brm\s+-rf\s+[/~]/, + /\bgit\s+push\b/, + /\bgit\s+reset\s+--hard\b/, + /\bcurl\s/, + /\bwget\s/, + /\bchmod\s+777\b/, +]; +const DENY_COMMAND_SCRIPT_PATTERNS: RegExp[] = [ + /hack\/[^/]+\.sh\b/, + /hack\\[^\\]+\.sh\b/, +]; +const SENSITIVE_FILE_PATTERNS: RegExp[] = [ + /\.env$/, + /\.env\./, + /\.pem$/, + /\.key$/, + /credentials/i, + /secret/i, +]; + +/** + * Record a governance violation to .context/state/violations.json. + * The MCP governance engine reads this file and escalates to CRITICAL + * warnings in tool responses. + */ +function recordViolation( + cwd: string, + kind: string, + detail: string +): void { + try { + const stateDir = path.join(cwd, ".context", "state"); + if (!fs.existsSync(stateDir)) { + fs.mkdirSync(stateDir, { recursive: true }); + } + const violationsPath = path.join(stateDir, "violations.json"); + let violations: { entries: Array<{ kind: string; detail: string; timestamp: string }> } = { + entries: [], + }; + if (fs.existsSync(violationsPath)) { + try { + violations = JSON.parse(fs.readFileSync(violationsPath, "utf-8")); + } catch { + // corrupt file — reset + } + } + violations.entries.push({ + kind, + detail, + timestamp: new Date().toISOString(), + }); + // Keep only the last 50 violations + if (violations.entries.length > 50) { + violations.entries = violations.entries.slice(-50); + } + fs.writeFileSync(violationsPath, JSON.stringify(violations, null, 2), "utf-8"); + } catch { + // Non-fatal — governance is advisory + } +} + function getCtxPath(): string { if (resolvedCtxPath) { return resolvedCtxPath; @@ -263,6 +330,21 @@ async function bootstrap(): Promise { return bootstrapPromise; } +/** + * Merge stdout and stderr without duplicating lines that appear in both. + * Cobra prints errors to both streams — naive concatenation doubles them. + */ +function mergeOutput(stdout: string, stderr: string): string { + const out = stdout.trim(); + const err = stderr.trim(); + if (!out) return err; + if (!err) return out; + // If stderr content already appears in stdout, skip it + if (out.includes(err)) return out; + if (err.includes(out)) return err; + return out + "\n" + err; +} + function runCtx( args: string[], cwd?: string, @@ -307,6 +389,13 @@ function runCtx( }); } +/** + * Check if .context/ directory exists in the workspace root. + */ +function hasContextDir(cwd: string): boolean { + return fs.existsSync(path.join(cwd, ".context")); +} + async function handleInit( stream: vscode.ChatResponseStream, cwd: string, @@ -314,8 +403,8 @@ async function handleInit( ): Promise { stream.progress("Initializing .context/ directory..."); try { - const { stdout, stderr } = await runCtx(["init", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["init", "--caller", "vscode"], cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } @@ -324,15 +413,15 @@ async function handleInit( // project context automatically. stream.progress("Generating Copilot instructions..."); try { - const setupResult = await runCtx( - ["setup", "copilot", "--write", "--no-color"], + const hookResult = await runCtx( + ["hook", "copilot", "--write"], cwd, token ); - const setupOutput = (setupResult.stdout + setupResult.stderr).trim(); - if (setupOutput) { + const hookOutput = mergeOutput(hookResult.stdout, hookResult.stderr); + if (hookOutput) { stream.markdown( - "\n**Copilot integration:**\n```\n" + setupOutput + "\n```" + "\n**Copilot integration:**\n```\n" + hookOutput + "\n```" ); } else { stream.markdown( @@ -340,10 +429,10 @@ async function handleInit( ); } } catch { - // Non-fatal — init succeeded, setup is a bonus + // Non-fatal — init succeeded, hook is a bonus stream.markdown( "\n> **Note:** Could not generate `.github/copilot-instructions.md`. " + - "Run `@ctx /setup copilot` manually." + "Run `@ctx /hook copilot` manually." ); } @@ -352,6 +441,9 @@ async function handleInit( "`.context/` directory initialized. Run `@ctx /status` to see your project context." ); } + + // Fire session-start since activate() missed it (no .context/ at activation time) + runCtx(["system", "session-event", "--type", "start", "--caller", "vscode"], cwd).catch(() => {}); } catch (err: unknown) { stream.markdown( `**Error:** Failed to initialize context.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` @@ -367,8 +459,8 @@ async function handleStatus( ): Promise { stream.progress("Checking context status..."); try { - const { stdout, stderr } = await runCtx(["status", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["status"], cwd, token); + const output = mergeOutput(stdout, stderr); stream.markdown("```\n" + output + "\n```"); } catch (err: unknown) { stream.markdown( @@ -380,13 +472,19 @@ async function handleStatus( async function handleAgent( stream: vscode.ChatResponseStream, + prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { stream.progress("Generating AI-ready context packet..."); try { - const { stdout, stderr } = await runCtx(["agent", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const args = ["agent"]; + const budgetMatch = prompt.match(/(?:--budget\s+|budget\s+)(\d+)/); + if (budgetMatch) { + args.splice(1, 0, "--budget", budgetMatch[1]); + } + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); stream.markdown(output); } catch (err: unknown) { stream.markdown( @@ -403,8 +501,8 @@ async function handleDrift( ): Promise { stream.progress("Detecting context drift..."); try { - const { stdout, stderr } = await runCtx(["drift", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["drift"], cwd, token); + const output = mergeOutput(stdout, stderr); stream.markdown("```\n" + output + "\n```"); } catch (err: unknown) { stream.markdown( @@ -420,18 +518,74 @@ async function handleRecall( cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Searching session history..."); - try { - const args = ["recall", "list", "--no-color"]; - if (prompt.trim()) { - args.push("--query", prompt.trim()); + const parts = prompt.trim().split(/\s+/); + const subcmd = parts[0]?.toLowerCase(); + const rest = parts.slice(1).join(" "); + + let args: string[]; + let progressMsg: string; + + switch (subcmd) { + case "show": + if (!rest) { + stream.markdown("**Usage:** `@ctx /recall show `"); + return { metadata: { command: "recall" } }; + } + args = ["recall", "show", rest]; + progressMsg = "Loading session details..."; + break; + case "export": + if (!rest) { + stream.markdown("**Usage:** `@ctx /recall export `"); + return { metadata: { command: "recall" } }; + } + args = ["recall", "export", rest]; + progressMsg = "Exporting session..."; + break; + case "lock": + if (!rest) { + stream.markdown("**Usage:** `@ctx /recall lock `"); + return { metadata: { command: "recall" } }; + } + args = ["recall", "lock", rest]; + progressMsg = "Locking session..."; + break; + case "unlock": + if (!rest) { + stream.markdown("**Usage:** `@ctx /recall unlock `"); + return { metadata: { command: "recall" } }; + } + args = ["recall", "unlock", rest]; + progressMsg = "Unlocking session..."; + break; + case "sync": + args = ["recall", "sync"]; + progressMsg = "Syncing recall database..."; + break; + case "list": + default: { + args = ["recall", "list"]; + progressMsg = "Searching session history..."; + const limitMatch = prompt.match(/(?:--limit\s+|limit\s+)(\d+)/); + if (limitMatch) { + args.push("--limit", limitMatch[1]); + } + const query = (subcmd === "list" ? rest : prompt.trim()).replace(/--limit\s+\d+/, "").replace(/limit\s+\d+/, "").trim(); + if (query) { + args.push("--query", query); + } + break; } + } + + stream.progress(progressMsg); + try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { - stream.markdown("No session history found."); + stream.markdown(subcmd === "list" || !subcmd ? "No session history found." : "No output."); } } catch (err: unknown) { stream.markdown( @@ -441,7 +595,7 @@ async function handleRecall( return { metadata: { command: "recall" } }; } -async function handleSetup( +async function handleHook( stream: vscode.ChatResponseStream, prompt: string, cwd: string, @@ -451,11 +605,10 @@ async function handleSetup( const tool = parts[0] || "copilot"; const preview = parts.includes("preview") || parts.includes("--preview"); - const args = ["setup", tool]; + const args = ["hook", tool]; if (!preview) { args.push("--write"); } - args.push("--no-color"); stream.progress( preview @@ -464,7 +617,7 @@ async function handleSetup( ); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -479,7 +632,7 @@ async function handleSetup( `**Error:** Failed to generate hook.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "setup" } }; + return { metadata: { command: "hook" } }; } async function handleAdd( @@ -495,7 +648,7 @@ async function handleAdd( if (!type) { stream.markdown( "**Usage:** `@ctx /add `\n\n" + - "Types: `task`, `decision`, `learning`\n\n" + + "Types: `task`, `decision`, `learning`, `convention`\n\n" + "Example: `@ctx /add task Implement user authentication`" ); return { metadata: { command: "add" } }; @@ -508,7 +661,7 @@ async function handleAdd( args.push(content); } const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -529,8 +682,8 @@ async function handleLoad( ): Promise { stream.progress("Loading assembled context..."); try { - const { stdout, stderr } = await runCtx(["load", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["load"], cwd, token); + const output = mergeOutput(stdout, stderr); stream.markdown(output); } catch (err: unknown) { stream.markdown( @@ -547,8 +700,8 @@ async function handleCompact( ): Promise { stream.progress("Compacting context..."); try { - const { stdout, stderr } = await runCtx(["compact", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["compact"], cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -569,8 +722,8 @@ async function handleSync( ): Promise { stream.progress("Syncing context with codebase..."); try { - const { stdout, stderr } = await runCtx(["sync", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["sync"], cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -584,82 +737,40 @@ async function handleSync( return { metadata: { command: "sync" } }; } -async function handleTask( +async function handleComplete( stream: vscode.ChatResponseStream, prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - const parts = prompt.trim().split(/\s+/); - const subcmd = parts[0]?.toLowerCase(); - const rest = parts.slice(1).join(" "); - - let args: string[]; - let progressMsg: string; - - switch (subcmd) { - case "complete": { - const taskRef = rest.trim(); - if (!taskRef) { - stream.markdown( - "**Usage:** `@ctx /task complete `\n\n" + - "Example: `@ctx /task complete 3` or " + - "`@ctx /task complete Fix login bug`" - ); - return { metadata: { command: "task" } }; - } - args = ["task", "complete", taskRef]; - progressMsg = "Marking task as completed..."; - break; - } - case "archive": - args = ["task", "archive"]; - progressMsg = "Archiving completed tasks..."; - break; - case "snapshot": - args = rest ? ["task", "snapshot", rest] : ["task", "snapshot"]; - progressMsg = "Creating task snapshot..."; - break; - default: - stream.markdown( - "**Usage:** `@ctx /task `\n\n" + - "| Subcommand | Description |\n" + - "|------------|-------------|\n" + - "| `complete ` | Mark a task as completed |\n" + - "| `archive` | Move completed tasks to archive |\n" + - "| `snapshot [name]` | Create point-in-time snapshot |\n\n" + - "Example: `@ctx /task complete 3` or " + - "`@ctx /task archive`" - ); - return { metadata: { command: "task" } }; + const taskRef = prompt.trim(); + if (!taskRef) { + stream.markdown( + "**Usage:** `@ctx /complete `\n\n" + + "Example: `@ctx /complete 3` or `@ctx /complete Fix login bug`" + ); + return { metadata: { command: "complete" } }; } - args.push("--no-color"); - stream.progress(progressMsg); + stream.progress("Marking task as completed..."); try { - const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx( + ["complete", taskRef], + cwd, + token + ); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { - switch (subcmd) { - case "complete": - stream.markdown(`Task **${rest.trim()}** marked as completed.`); - break; - case "archive": - stream.markdown("Completed tasks archived."); - break; - default: - stream.markdown("Task snapshot created."); - break; - } + stream.markdown(`Task **${taskRef}** marked as completed.`); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to ${subcmd} task.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Failed to complete task.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "task" } }; + return { metadata: { command: "complete" } }; } async function handleRemind( @@ -701,12 +812,11 @@ async function handleRemind( } break; } - args.push("--no-color"); stream.progress(progressMsg); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -720,6 +830,61 @@ async function handleRemind( return { metadata: { command: "remind" } }; } +async function handleTasks( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + token: vscode.CancellationToken +): Promise { + const parts = prompt.trim().split(/\s+/); + const subcmd = parts[0]?.toLowerCase(); + const rest = parts.slice(1).join(" "); + + let args: string[]; + let progressMsg: string; + + switch (subcmd) { + case "archive": + args = ["tasks", "archive"]; + progressMsg = "Archiving completed tasks..."; + break; + case "snapshot": + args = rest ? ["tasks", "snapshot", rest] : ["tasks", "snapshot"]; + progressMsg = "Creating task snapshot..."; + break; + default: + stream.markdown( + "**Usage:** `@ctx /tasks `\n\n" + + "| Subcommand | Description |\n" + + "|------------|-------------|\n" + + "| `archive` | Move completed tasks to archive |\n" + + "| `snapshot [name]` | Create point-in-time snapshot |\n\n" + + "Example: `@ctx /tasks archive` or `@ctx /tasks snapshot pre-refactor`" + ); + return { metadata: { command: "tasks" } }; + } + + stream.progress(progressMsg); + try { + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); + if (output) { + stream.markdown("```\n" + output + "\n```"); + } else { + stream.markdown( + subcmd === "archive" + ? "Completed tasks archived." + : "Task snapshot created." + ); + } + } catch (err: unknown) { + stream.markdown( + `**Error:** Failed to ${subcmd} tasks.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + ); + } + return { metadata: { command: "tasks" } }; +} + async function handlePad( stream: vscode.ChatResponseStream, prompt: string, @@ -766,18 +931,37 @@ async function handlePad( args = ["pad", "mv", ...parts.slice(1)]; progressMsg = "Moving scratchpad entry..."; break; + case "resolve": + args = ["pad", "resolve"]; + progressMsg = "Resolving scratchpad conflicts..."; + break; + case "import": + if (!rest) { + stream.markdown("**Usage:** `@ctx /pad import `"); + return { metadata: { command: "pad" } }; + } + args = ["pad", "import", rest]; + progressMsg = "Importing scratchpad archive..."; + break; + case "export": + args = rest ? ["pad", "export", rest] : ["pad", "export"]; + progressMsg = "Exporting scratchpad..."; + break; + case "merge": + args = rest ? ["pad", "merge", rest] : ["pad", "merge"]; + progressMsg = "Merging scratchpads..."; + break; default: // No subcommand or unknown — list all entries args = ["pad"]; progressMsg = "Listing scratchpad..."; break; } - args.push("--no-color"); stream.progress(progressMsg); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -799,6 +983,7 @@ async function handleNotify( ): Promise { const parts = prompt.trim().split(/\s+/); const subcmd = parts[0]?.toLowerCase(); + const rest = parts.slice(1).join(" "); let args: string[]; let progressMsg: string; @@ -831,12 +1016,11 @@ async function handleNotify( break; } } - args.push("--no-color"); stream.progress(progressMsg); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -877,28 +1061,48 @@ async function handleSystem( args = ["system", "bootstrap"]; progressMsg = "Running bootstrap..."; break; + case "doctor": + args = ["doctor"]; + progressMsg = "Running diagnostics..."; + break; case "message": args = ["system", "message", ...parts.slice(1)]; + if (parts.length < 2 || !["list", "show", "edit", "reset"].includes(parts[1]?.toLowerCase())) { + args = ["system", "message", "list"]; + } progressMsg = "Managing hook messages..."; break; + case "stats": + args = ["system", "stats"]; + progressMsg = "Loading system stats..."; + break; + case "backup": + args = ["system", "backup"]; + progressMsg = "Running backup..."; + break; default: stream.markdown( "**Usage:** `@ctx /system `\n\n" + "| Subcommand | Description |\n" + "|------------|-------------|\n" + "| `resources` | Show system resource usage |\n" + + "| `doctor` | Diagnose context health |\n" + "| `bootstrap` | Print context location for AI agents |\n" + - "| `message list|show|edit|reset` | Manage hook messages |\n\n" + - "Example: `@ctx /system resources` or `@ctx /system bootstrap`" + "| `stats` | Show session and context stats |\n" + + "| `backup` | Backup context data |\n" + + "| `message list` | List hook message templates |\n" + + "| `message show ` | Show a hook message |\n" + + "| `message edit ` | Edit a hook message |\n" + + "| `message reset ` | Reset a hook message |\n\n" + + "Example: `@ctx /system resources` or `@ctx /system message list`" ); return { metadata: { command: "system" } }; } - args.push("--no-color"); stream.progress(progressMsg); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { @@ -912,364 +1116,921 @@ async function handleSystem( return { metadata: { command: "system" } }; } -async function handleMemory( +async function handleWrapup( stream: vscode.ChatResponseStream, - prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - const parts = prompt.trim().split(/\s+/); - const subcmd = parts[0]?.toLowerCase(); + stream.progress("Generating session wrap-up..."); + try { + // Gather status + drift in parallel for a comprehensive wrap-up + const [statusResult, driftResult] = await Promise.all([ + runCtx(["status"], cwd, token), + runCtx(["drift"], cwd, token), + ]); + const statusOutput = mergeOutput(statusResult.stdout, statusResult.stderr); + const driftOutput = mergeOutput(driftResult.stdout, driftResult.stderr); + + stream.markdown("## Session Wrap-up\n\n"); + stream.markdown("### Context Status\n```\n" + statusOutput + "\n```\n\n"); + stream.markdown("### Drift Check\n```\n" + driftOutput + "\n```\n\n"); + stream.markdown( + "**Before closing:** Review any open tasks in `.context/TASKS.md`. " + + "Record decisions or learnings with `@ctx /add decision ...` or `@ctx /add learning ...`.\n" + ); - let args: string[]; - let progressMsg: string; + // 2.15: Journal audit + try { + const stateDir = path.join(cwd, ".context", "state"); + if (fs.existsSync(stateDir)) { + const journalFiles = fs.readdirSync(stateDir).filter((f) => f.includes("journal") || f.includes("event")); + if (journalFiles.length > 0) { + const latest = journalFiles.sort().slice(-1)[0]; + const content = fs.readFileSync(path.join(stateDir, latest), "utf-8"); + const lines = content.split("\n").filter((l) => l.trim()); + stream.markdown(`\n### Journal\n${lines.length} entries in \`${latest}\`. `); + const today = new Date().toISOString().split("T")[0]; + if (!content.includes(today)) { + stream.markdown("**No entries today.** Consider logging your work.\n"); + } else { + stream.markdown("Today's entries present.\n"); + } + } + } + } catch { /* non-fatal */ } - switch (subcmd) { - case "sync": - args = ["memory", "sync"]; - progressMsg = "Syncing memory bridge..."; - break; - case "status": - args = ["memory", "status"]; - progressMsg = "Checking memory status..."; - break; - case "diff": - args = ["memory", "diff"]; - progressMsg = "Diffing memory state..."; - break; - case "import": - args = ["memory", "import"]; - progressMsg = "Importing memory..."; - break; - case "publish": - args = ["memory", "publish"]; - progressMsg = "Publishing memory..."; - break; - case "unpublish": - args = ["memory", "unpublish"]; - progressMsg = "Unpublishing memory..."; - break; - default: - stream.markdown( - "**Usage:** `@ctx /memory `\n\n" + - "| Subcommand | Description |\n" + - "|------------|-------------|\n" + - "| `sync` | Synchronize memory bridge |\n" + - "| `status` | Show memory bridge status |\n" + - "| `diff` | Show memory diff |\n" + - "| `import` | Import external memory |\n" + - "| `publish` | Publish curated context |\n" + - "| `unpublish` | Remove published context |\n\n" + - "Example: `@ctx /memory status` or `@ctx /memory sync`" - ); - return { metadata: { command: "memory" } }; - } - args.push("--no-color"); + // 2.18: Memory drift + try { + const memDir = path.join(cwd, ".context", "memory"); + if (fs.existsSync(memDir)) { + const memFiles = fs.readdirSync(memDir).filter((f) => f.endsWith(".md")); + if (memFiles.length > 0) { + const contextFiles = ["DECISIONS.md", "LEARNINGS.md", "CONVENTIONS.md", "TASKS.md"]; + const drifts: string[] = []; + for (const memFile of memFiles) { + const memStat = fs.statSync(path.join(memDir, memFile)); + for (const ctxFile of contextFiles) { + const ctxPath = path.join(cwd, ".context", ctxFile); + if (fs.existsSync(ctxPath)) { + const ctxStat = fs.statSync(ctxPath); + if (memStat.mtimeMs < ctxStat.mtimeMs - 86400000) { + drifts.push(`\`memory/${memFile}\` older than \`${ctxFile}\``); + } + } + } + } + if (drifts.length > 0) { + stream.markdown("\n### Memory Drift\n" + drifts.map((d) => `- ${d}`).join("\n") + "\n"); + } + } + } + } catch { /* non-fatal */ } - stream.progress(progressMsg); - try { - const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); - } else { - stream.markdown(`Memory ${subcmd} completed.`); - } + // Record session end + runCtx(["system", "session-event", "--type", "end", "--caller", "vscode"], cwd).catch(() => {}); } catch (err: unknown) { stream.markdown( - `**Error:** Failed to run memory ${subcmd}.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Wrap-up failed.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "memory" } }; + return { metadata: { command: "wrapup" } }; } -async function handleJournal( +async function handleRemember( stream: vscode.ChatResponseStream, prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - const parts = prompt.trim().split(/\s+/); - const subcmd = parts[0]?.toLowerCase(); - - let args: string[]; - let progressMsg: string; - - switch (subcmd) { - case "site": - args = ["journal", "site"]; - progressMsg = "Generating journal site..."; - break; - case "obsidian": - args = ["journal", "obsidian"]; - progressMsg = "Exporting journal to Obsidian..."; - break; - default: - stream.markdown( - "**Usage:** `@ctx /journal `\n\n" + - "| Subcommand | Description |\n" + - "|------------|-------------|\n" + - "| `site` | Generate journal site |\n" + - "| `obsidian` | Export journal to Obsidian |\n\n" + - "Example: `@ctx /journal site`" - ); - return { metadata: { command: "journal" } }; - } - args.push("--no-color"); - - stream.progress(progressMsg); + stream.progress("Loading recent sessions..."); try { + const args = ["recall", "list"]; + const limitMatch = prompt.match(/(?:--limit\s+|limit\s+)(\d+)/); + args.push("--limit", limitMatch ? limitMatch[1] : "3"); const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { - stream.markdown("```\n" + output + "\n```"); + stream.markdown("## Recent Sessions\n\n```\n" + output + "\n```"); } else { - stream.markdown(`Journal ${subcmd} completed.`); + stream.markdown("No recent sessions found. Start working and sessions will be recorded."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to run journal ${subcmd}.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Failed to load sessions.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "journal" } }; + return { metadata: { command: "remember" } }; } -async function handleDoctor( +async function handleNext( stream: vscode.ChatResponseStream, cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Running context health diagnostics..."); + stream.progress("Finding next task..."); try { - const { stdout, stderr } = await runCtx(["doctor", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); + const tasksPath = path.join(cwd, ".context", "TASKS.md"); + if (!fs.existsSync(tasksPath)) { + stream.markdown("No `.context/TASKS.md` found. Add tasks with `@ctx /add task ...`."); + return { metadata: { command: "next" } }; + } + const content = fs.readFileSync(tasksPath, "utf-8"); + const lines = content.split("\n"); + const openTasks = lines.filter((l) => /^\s*-\s*\[ \]/.test(l)); + if (openTasks.length === 0) { + stream.markdown("All tasks completed! Add new tasks with `@ctx /add task ...`."); } else { - stream.markdown("Context health check passed."); + stream.markdown("## Next Task\n\n" + openTasks[0].trim() + "\n"); + if (openTasks.length > 1) { + stream.markdown( + `\n*${openTasks.length - 1} more open task(s) remaining.*` + ); + } } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to run doctor.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Failed to read tasks.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "doctor" } }; + return { metadata: { command: "next" } }; } -async function handleConfig( +async function handleBrainstorm( stream: vscode.ChatResponseStream, prompt: string, cwd: string, - token: vscode.CancellationToken + _token: vscode.CancellationToken ): Promise { - const parts = prompt.trim().split(/\s+/); - const subcmd = parts[0]?.toLowerCase(); - const rest = parts.slice(1).join(" "); - - let args: string[]; - let progressMsg: string; - - switch (subcmd) { - case "switch": - args = rest ? ["config", "switch", rest] : ["config", "switch"]; - progressMsg = "Switching configuration..."; - break; - case "status": - args = ["config", "status"]; - progressMsg = "Checking configuration status..."; - break; - case "schema": - args = ["config", "schema"]; - progressMsg = "Showing configuration schema..."; - break; - default: + stream.progress("Loading ideas..."); + try { + const ideasDir = path.join(cwd, "ideas"); + if (!fs.existsSync(ideasDir)) { stream.markdown( - "**Usage:** `@ctx /config `\n\n" + - "| Subcommand | Description |\n" + - "|------------|-------------|\n" + - "| `switch` | Switch active configuration |\n" + - "| `status` | Show current configuration |\n" + - "| `schema` | Show configuration schema |\n\n" + - "Example: `@ctx /config status` or `@ctx /config switch minimal`" + "No `ideas/` directory found. Run `@ctx /init` first, then add ideas to `ideas/README.md`." ); - return { metadata: { command: "config" } }; + return { metadata: { command: "brainstorm" } }; + } + const readmePath = path.join(ideasDir, "README.md"); + if (fs.existsSync(readmePath)) { + const content = fs.readFileSync(readmePath, "utf-8").trim(); + stream.markdown("## Current Ideas\n\n" + content + "\n"); + } else { + stream.markdown("Ideas directory exists but `ideas/README.md` is empty.\n"); + } + // List any other files in ideas/ + const files = fs.readdirSync(ideasDir).filter((f) => f !== "README.md" && f.endsWith(".md")); + if (files.length > 0) { + stream.markdown( + "\n### Idea Files\n" + files.map((f) => `- \`ideas/${f}\``).join("\n") + "\n" + ); + } + if (prompt.trim()) { + stream.markdown( + "\n---\n\nTo develop **" + prompt.trim() + "** into a spec, create `specs/" + + prompt.trim().toLowerCase().replace(/\s+/g, "-") + ".md` with your design." + ); + } else { + stream.markdown( + "\n---\nTo develop an idea into a spec, run `@ctx /brainstorm `." + ); + } + } catch (err: unknown) { + stream.markdown( + `**Error:** Failed to load ideas.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + ); } - args.push("--no-color"); + return { metadata: { command: "brainstorm" } }; +} - stream.progress(progressMsg); +async function handleReflect( + stream: vscode.ChatResponseStream, + cwd: string, + token: vscode.CancellationToken +): Promise { + stream.progress("Reflecting on session..."); try { - const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); - } else { - stream.markdown(`Config ${subcmd} completed.`); + const [statusResult, driftResult] = await Promise.all([ + runCtx(["status"], cwd, token), + runCtx(["drift"], cwd, token), + ]); + const statusOutput = mergeOutput(statusResult.stdout, statusResult.stderr); + const driftOutput = mergeOutput(driftResult.stdout, driftResult.stderr); + + stream.markdown("## Session Reflection\n\n"); + stream.markdown("### Current State\n```\n" + statusOutput + "\n```\n\n"); + if (driftOutput) { + stream.markdown("### Drift Detected\n```\n" + driftOutput + "\n```\n\n"); } + stream.markdown( + "### Worth Persisting?\n\n" + + "Consider what happened this session:\n" + + "- **Decision?** Did you make a design choice? → `@ctx /add decision ...`\n" + + "- **Learning?** Hit a gotcha or discovered something? → `@ctx /add learning ...`\n" + + "- **Convention?** Established a pattern? → `@ctx /add convention ...`\n" + + "- **Task?** Identified work for later? → `@ctx /add task ...`\n" + ); + + // 2.15: Journal audit — check journal completeness + try { + const stateDir = path.join(cwd, ".context", "state"); + if (fs.existsSync(stateDir)) { + const journalFiles = fs.readdirSync(stateDir).filter((f) => f.includes("journal") || f.includes("event")); + if (journalFiles.length > 0) { + const latest = journalFiles.sort().slice(-1)[0]; + const content = fs.readFileSync(path.join(stateDir, latest), "utf-8"); + const lines = content.split("\n").filter((l) => l.trim()); + stream.markdown(`\n### Journal\n${lines.length} entries in \`${latest}\`. `); + const today = new Date().toISOString().split("T")[0]; + if (!content.includes(today)) { + stream.markdown("**No entries today.** Consider logging your work.\n"); + } else { + stream.markdown("Today's entries present.\n"); + } + } + } + } catch { /* non-fatal */ } + + // 2.18: Memory drift — compare memory with context files + try { + const memDir = path.join(cwd, ".context", "memory"); + if (fs.existsSync(memDir)) { + const memFiles = fs.readdirSync(memDir).filter((f) => f.endsWith(".md")); + if (memFiles.length > 0) { + const contextFiles = ["DECISIONS.md", "LEARNINGS.md", "CONVENTIONS.md", "TASKS.md"]; + const drifts: string[] = []; + for (const memFile of memFiles) { + const memStat = fs.statSync(path.join(memDir, memFile)); + for (const ctxFile of contextFiles) { + const ctxPath = path.join(cwd, ".context", ctxFile); + if (fs.existsSync(ctxPath)) { + const ctxStat = fs.statSync(ctxPath); + // Memory older than context by 24+ hours = potentially stale + if (memStat.mtimeMs < ctxStat.mtimeMs - 86400000) { + drifts.push(`\`memory/${memFile}\` older than \`${ctxFile}\``); + } + } + } + } + if (drifts.length > 0) { + stream.markdown("\n### Memory Drift\n" + drifts.map((d) => `- ${d}`).join("\n") + "\n"); + } + } + } + } catch { /* non-fatal */ } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to run config ${subcmd}.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Reflection failed.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "config" } }; + return { metadata: { command: "reflect" } }; } -async function handlePrompt( +async function handleSpec( stream: vscode.ChatResponseStream, prompt: string, cwd: string, - token: vscode.CancellationToken + _token: vscode.CancellationToken ): Promise { - const parts = prompt.trim().split(/\s+/); - const subcmd = parts[0]?.toLowerCase(); - const rest = parts.slice(1).join(" "); + stream.progress("Loading specs..."); + try { + const specsDir = path.join(cwd, "specs"); + const tplDir = path.join(specsDir, "tpl"); + if (!prompt.trim()) { + const specs = fs.existsSync(specsDir) ? fs.readdirSync(specsDir).filter((f) => f.endsWith(".md")) : []; + const templates = fs.existsSync(tplDir) ? fs.readdirSync(tplDir).filter((f) => f.endsWith(".md")) : []; + stream.markdown("## Specs\n\n"); + if (specs.length) { + stream.markdown("### Existing\n" + specs.map((f) => `- \`specs/${f}\``).join("\n") + "\n\n"); + } + if (templates.length) { + stream.markdown("### Templates\n" + templates.map((f) => `- \`specs/tpl/${f}\``).join("\n") + "\n\nScaffold: `@ctx /spec `\n"); + } else { + stream.markdown("No templates in `specs/tpl/`. Create one to enable scaffolding.\n"); + } + } else { + const name = prompt.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9\-]/g, ""); + const target = path.join(specsDir, `${name}.md`); + if (fs.existsSync(target)) { + const content = fs.readFileSync(target, "utf-8"); + stream.markdown(`\`specs/${name}.md\` exists:\n\n\`\`\`markdown\n${content}\n\`\`\``); + } else { + const templates = fs.existsSync(tplDir) ? fs.readdirSync(tplDir).filter((f) => f.endsWith(".md")) : []; + let content: string; + if (templates.length > 0) { + content = fs.readFileSync(path.join(tplDir, templates[0]), "utf-8") + .replace(/\{\{name\}\}/gi, name) + .replace(/\{\{title\}\}/gi, prompt.trim()); + } else { + content = `# ${prompt.trim()}\n\n## Problem\n\n## Proposal\n\n## Implementation\n\n## Verification\n`; + } + if (!fs.existsSync(specsDir)) { fs.mkdirSync(specsDir, { recursive: true }); } + fs.writeFileSync(target, content, "utf-8"); + stream.markdown(`Created \`specs/${name}.md\`.\n\n\`\`\`markdown\n${content}\n\`\`\``); + } + } + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "spec" } }; +} - let args: string[]; - let progressMsg: string; +async function handleImplement( + stream: vscode.ChatResponseStream, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Loading implementation plan..."); + try { + const planPath = path.join(cwd, "IMPLEMENTATION_PLAN.md"); + if (!fs.existsSync(planPath)) { + stream.markdown("No `IMPLEMENTATION_PLAN.md` found in project root."); + return { metadata: { command: "implement" } }; + } + const content = fs.readFileSync(planPath, "utf-8"); + const lines = content.split("\n"); + const done = lines.filter((l) => /^\s*-\s*\[x\]/i.test(l)).length; + const open = lines.filter((l) => /^\s*-\s*\[ \]/.test(l)).length; + const total = done + open; + if (total > 0) { + stream.markdown(`## Implementation Plan (${done}/${total} steps done)\n\n`); + const nextStep = lines.find((l) => /^\s*-\s*\[ \]/.test(l)); + if (nextStep) { + stream.markdown("**Next step:** " + nextStep.replace(/^\s*-\s*\[ \]\s*/, "").trim() + "\n\n---\n\n"); + } + } else { + stream.markdown("## Implementation Plan\n\n"); + } + stream.markdown(content); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "implement" } }; +} - switch (subcmd) { - case "list": - case "ls": - args = ["prompt", "list"]; - progressMsg = "Listing prompt templates..."; - break; - case "add": - args = rest ? ["prompt", "add", rest] : ["prompt", "add"]; - progressMsg = "Adding prompt template..."; - break; - case "show": - args = rest ? ["prompt", "show", rest] : ["prompt", "show"]; - progressMsg = "Showing prompt template..."; - break; - case "rm": - args = rest ? ["prompt", "rm", rest] : ["prompt", "rm"]; - progressMsg = "Removing prompt template..."; - break; - default: - stream.markdown( - "**Usage:** `@ctx /prompt `\n\n" + - "| Subcommand | Description |\n" + - "|------------|-------------|\n" + - "| `list` | List prompt templates |\n" + - "| `add ` | Add a prompt template |\n" + - "| `show ` | Show a prompt template |\n" + - "| `rm ` | Remove a prompt template |\n\n" + - "Example: `@ctx /prompt list` or `@ctx /prompt show review`" - ); - return { metadata: { command: "prompt" } }; +async function handleVerify( + stream: vscode.ChatResponseStream, + cwd: string, + token: vscode.CancellationToken +): Promise { + stream.progress("Running verification checks..."); + try { + const results: string[] = []; + try { + const { stdout, stderr } = await runCtx(["doctor"], cwd, token); + results.push("### Context Health\n```\n" + mergeOutput(stdout, stderr) + "\n```"); + } catch (err: unknown) { + results.push("### Context Health\n```\nFailed: " + (err instanceof Error ? err.message : String(err)) + "\n```"); + } + try { + const { stdout, stderr } = await runCtx(["drift"], cwd, token); + const output = mergeOutput(stdout, stderr); + if (output) { results.push("### Drift\n```\n" + output + "\n```"); } + } catch { /* non-fatal */ } + stream.markdown("## Verification Report\n\n" + results.join("\n\n")); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); } - args.push("--no-color"); + return { metadata: { command: "verify" } }; +} - stream.progress(progressMsg); +async function handleMap( + stream: vscode.ChatResponseStream, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Mapping dependencies..."); try { - const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); + const results: string[] = []; + const gomodPath = path.join(cwd, "go.mod"); + if (fs.existsSync(gomodPath)) { + const content = fs.readFileSync(gomodPath, "utf-8"); + const moduleLine = content.match(/^module\s+(.+)$/m); + const requires = content.match(/require\s*\(([\s\S]*?)\)/); + results.push("### Go Module: " + (moduleLine ? moduleLine[1] : "unknown")); + if (requires) { + const deps = requires[1].trim().split("\n").filter((l) => l.trim() && !l.trim().startsWith("//")); + results.push(`${deps.length} dependencies:\n\`\`\`\n${deps.map((d) => d.trim()).join("\n")}\n\`\`\``); + } + } + const pkgPath = path.join(cwd, "package.json"); + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + const deps = Object.keys(pkg.dependencies || {}); + const devDeps = Object.keys(pkg.devDependencies || {}); + results.push("### Node Package: " + (pkg.name || "unknown")); + if (deps.length) { results.push(`Dependencies: ${deps.join(", ")}`); } + if (devDeps.length) { results.push(`Dev: ${devDeps.join(", ")}`); } + } + if (results.length === 0) { + stream.markdown("No `go.mod` or `package.json` found."); } else { - stream.markdown(`Prompt ${subcmd} completed.`); + stream.markdown("## Dependency Map\n\n" + results.join("\n\n")); } } catch (err: unknown) { - stream.markdown( - `**Error:** Failed to run prompt ${subcmd}.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` - ); + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); } - return { metadata: { command: "prompt" } }; + return { metadata: { command: "map" } }; } -async function handleWhy( +async function handlePromptTpl( stream: vscode.ChatResponseStream, prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - const filename = prompt.trim(); - const args = filename ? ["why", filename] : ["why"]; - args.push("--no-color"); - - stream.progress("Looking up design rationale..."); + stream.progress("Loading prompt templates..."); try { - const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); + const promptsDir = path.join(cwd, ".context", "prompts"); + const parts = prompt.trim().split(/\s+/); + const subcmd = parts[0]?.toLowerCase(); + const rest = parts.slice(1).join(" "); + + if (subcmd === "add") { + if (!rest) { + stream.markdown("**Usage:** `@ctx /prompt add `\n\nAdds a file as a prompt template."); + return { metadata: { command: "prompt" } }; + } + const args = ["prompt", "add", rest]; + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); + stream.markdown(output ? "```\n" + output + "\n```" : `Template **${rest}** added.`); + return { metadata: { command: "prompt" } }; + } + + if (subcmd === "rm" || subcmd === "remove" || subcmd === "delete") { + if (!rest) { + stream.markdown("**Usage:** `@ctx /prompt rm `"); + return { metadata: { command: "prompt" } }; + } + const args = ["prompt", "rm", rest]; + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); + stream.markdown(output ? "```\n" + output + "\n```" : `Template **${rest}** removed.`); + return { metadata: { command: "prompt" } }; + } + + if (!fs.existsSync(promptsDir)) { + stream.markdown("No `.context/prompts/` directory found."); + return { metadata: { command: "prompt" } }; + } + const files = fs.readdirSync(promptsDir).filter((f) => f.endsWith(".md")); + if (!prompt.trim()) { + if (files.length === 0) { + stream.markdown("`.context/prompts/` is empty. Add prompt templates with `@ctx /prompt add `."); + } else { + stream.markdown("## Prompt Templates\n\n" + files.map((f) => `- \`${f}\``).join("\n") + + "\n\nView: `@ctx /prompt `\nAdd: `@ctx /prompt add `\nRemove: `@ctx /prompt rm `"); + } } else { - stream.markdown("No rationale found."); + const name = prompt.trim(); + const match = files.find((f) => f.toLowerCase().includes(name.toLowerCase())); + if (match) { + const content = fs.readFileSync(path.join(promptsDir, match), "utf-8"); + stream.markdown(`## ${match}\n\n${content}`); + } else { + stream.markdown(`No template matching "${name}". Available: ${files.join(", ")}`); + } } } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "prompt" } }; +} + +async function handleBlog( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Drafting blog post..."); + try { + const sections: string[] = []; + const decisionsPath = path.join(cwd, ".context", "DECISIONS.md"); + const learningsPath = path.join(cwd, ".context", "LEARNINGS.md"); + if (fs.existsSync(decisionsPath)) { + const entries = fs.readFileSync(decisionsPath, "utf-8").split("\n").filter((l) => l.startsWith("- ")); + if (entries.length) { sections.push("## Key Decisions\n\n" + entries.slice(-5).join("\n")); } + } + if (fs.existsSync(learningsPath)) { + const entries = fs.readFileSync(learningsPath, "utf-8").split("\n").filter((l) => l.startsWith("- ")); + if (entries.length) { sections.push("## Lessons Learned\n\n" + entries.slice(-5).join("\n")); } + } + const title = prompt.trim() || "Untitled Post"; + const date = new Date().toISOString().split("T")[0]; stream.markdown( - `**Error:** Failed to look up rationale.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `# Blog Draft: ${title}\n\n*Date: ${date}*\n\n` + + (sections.length ? sections.join("\n\n") : "No decisions or learnings to draw from.") + + "\n\n---\n*Edit and refine this draft, then save to `docs/blog/`.*" ); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); } - return { metadata: { command: "why" } }; + return { metadata: { command: "blog" } }; +} + +async function handleChangelog( + stream: vscode.ChatResponseStream, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Generating changelog..."); + try { + const result = await new Promise((resolve, reject) => { + execFile("git", ["log", "--oneline", "--no-decorate", "-20"], { cwd }, (err, stdout) => { + if (err) { reject(err); } else { resolve(stdout); } + }); + }); + if (result.trim()) { + stream.markdown("## Recent Commits\n\n```\n" + result.trim() + "\n```\n\n" + + "Use these to draft release notes or a changelog blog post."); + } else { + stream.markdown("No commits found."); + } + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "changelog" } }; +} + +async function handleCheckLinks( + stream: vscode.ChatResponseStream, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Checking links in context files..."); + try { + const contextDir = path.join(cwd, ".context"); + if (!fs.existsSync(contextDir)) { + stream.markdown("No `.context/` directory found."); + return { metadata: { command: "check-links" } }; + } + const files = fs.readdirSync(contextDir).filter((f) => f.endsWith(".md")); + const broken: string[] = []; + let total = 0; + for (const file of files) { + const content = fs.readFileSync(path.join(contextDir, file), "utf-8"); + const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g; + let m; + while ((m = linkRegex.exec(content)) !== null) { + const target = m[2]; + if (target.startsWith("http://") || target.startsWith("https://")) { continue; } + total++; + const resolved = path.resolve(contextDir, target); + if (!fs.existsSync(resolved)) { + broken.push(`- \`${file}\`: [${m[1]}](${target}) \u2192 not found`); + } + } + } + stream.markdown(`## Link Check\n\nChecked ${total} local links in ${files.length} context files.\n\n`); + if (broken.length) { + stream.markdown("### Broken Links\n" + broken.join("\n") + "\n"); + } else { + stream.markdown("All local links are valid.\n"); + } + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "check-links" } }; } -async function handleChange( +async function handleJournal( stream: vscode.ChatResponseStream, + prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Checking recent codebase changes..."); + stream.progress("Checking journal..."); try { - const { stdout, stderr } = await runCtx(["change", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); + const parts = prompt.trim().split(/\s+/); + const subcmd = parts[0]?.toLowerCase(); + const args = ["journal"]; + let progressOverride: string | undefined; + if (subcmd === "site") { + args.push("site", ...parts.slice(1)); + progressOverride = "Exporting journal to static site..."; + } else if (subcmd === "obsidian") { + args.push("obsidian", ...parts.slice(1)); + progressOverride = "Exporting journal to Obsidian..."; + } else if (prompt.trim()) { + args.push(...parts); + } + if (progressOverride) { stream.progress(progressOverride); } + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); + if (output) { stream.markdown("```\n" + output + "\n```"); } + else { stream.markdown("No journal output."); } + } catch { + try { + const stateDir = path.join(cwd, ".context", "state"); + if (fs.existsSync(stateDir)) { + const files = fs.readdirSync(stateDir).filter((f) => f.includes("journal") || f.includes("event")); + if (files.length) { + stream.markdown("## Journal Entries\n\n"); + for (const f of files.slice(-3)) { + try { + const content = fs.readFileSync(path.join(stateDir, f), "utf-8").trim(); + const preview = content.split("\n").slice(0, 10).join("\n"); + stream.markdown(`### ${f}\n\`\`\`\n${preview}\n\`\`\`\n\n`); + } catch { /* skip unreadable */ } + } + } else { + stream.markdown("No journal or event log files found in `.context/state/`."); + } + } else { + stream.markdown("No `.context/state/` directory found."); + } + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + } + return { metadata: { command: "journal" } }; +} + +async function handleConsolidate( + stream: vscode.ChatResponseStream, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Scanning for overlapping entries..."); + try { + const contextDir = path.join(cwd, ".context"); + const targetFiles = ["DECISIONS.md", "LEARNINGS.md", "CONVENTIONS.md", "TASKS.md"]; + const findings: string[] = []; + for (const file of targetFiles) { + const filePath = path.join(contextDir, file); + if (!fs.existsSync(filePath)) { continue; } + const entries = fs.readFileSync(filePath, "utf-8").split("\n") + .filter((l) => /^\s*-\s/.test(l)) + .map((l) => l.replace(/^\s*-\s*(\[.\]\s*)?/, "").trim().toLowerCase()); + const seen = new Map(); + for (const entry of entries) { + const key = entry.replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " "); + seen.set(key, (seen.get(key) || 0) + 1); + } + const dupes = [...seen.entries()].filter(([, count]) => count > 1); + if (dupes.length) { + findings.push(`### ${file}\n` + dupes.map(([text, count]) => `- "${text}" (\u00d7${count})`).join("\n")); + } + } + stream.markdown("## Consolidation Report\n\n"); + if (findings.length) { + stream.markdown(findings.join("\n\n") + "\n\nReview and merge manually."); } else { - stream.markdown("No recent changes found."); + stream.markdown("No duplicate entries found across context files."); } } catch (err: unknown) { - stream.markdown( - `**Error:** Failed to check changes.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` - ); + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); } - return { metadata: { command: "change" } }; + return { metadata: { command: "consolidate" } }; } -async function handleDep( +async function handleAudit( stream: vscode.ChatResponseStream, cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Checking project dependencies..."); + stream.progress("Running alignment audit..."); try { - const { stdout, stderr } = await runCtx(["dep", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); - if (output) { - stream.markdown("```\n" + output + "\n```"); + const { stdout: driftOut, stderr: driftErr } = await runCtx(["drift"], cwd, token); + stream.markdown("## Alignment Audit\n\n### Drift\n```\n" + mergeOutput(driftOut, driftErr) + "\n```\n\n"); + const convPath = path.join(cwd, ".context", "CONVENTIONS.md"); + if (fs.existsSync(convPath)) { + const entries = fs.readFileSync(convPath, "utf-8").split("\n").filter((l) => /^\s*-\s/.test(l)); + stream.markdown(`### Conventions: ${entries.length} documented\n\n`); + if (entries.length === 0) { + stream.markdown("**Warning:** No conventions documented. Run `@ctx /add convention ...`\n"); + } + } + const archPath = path.join(cwd, ".context", "ARCHITECTURE.md"); + if (fs.existsSync(archPath)) { + const lines = fs.readFileSync(archPath, "utf-8").split("\n").filter((l) => l.trim() && !l.startsWith("#")); + if (lines.length < 3) { + stream.markdown("**Warning:** `ARCHITECTURE.md` appears sparse. Consider documenting system structure.\n"); + } + } + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "audit" } }; +} + +async function handleWorktree( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + _token: vscode.CancellationToken +): Promise { + stream.progress("Managing worktrees..."); + try { + const subcmd = prompt.trim().split(/\s+/)[0]?.toLowerCase() || "list"; + if (subcmd === "list" || !prompt.trim()) { + const result = await new Promise((resolve, reject) => { + execFile("git", ["worktree", "list"], { cwd }, (err, stdout) => { + if (err) { reject(err); } else { resolve(stdout); } + }); + }); + stream.markdown("## Git Worktrees\n\n```\n" + result.trim() + "\n```\n\nCreate: `@ctx /worktree add `"); + } else if (subcmd === "add") { + const branch = prompt.trim().split(/\s+/).slice(1).join("-").replace(/[^a-zA-Z0-9_\-\/]/g, ""); + if (!branch) { + stream.markdown("Usage: `@ctx /worktree add `"); + } else { + const worktreePath = path.join(path.dirname(cwd), path.basename(cwd) + "-" + branch); + const result = await new Promise((resolve, reject) => { + execFile("git", ["worktree", "add", worktreePath, "-b", branch], { cwd }, (err, stdout, stderr) => { + if (err) { reject(err); } else { resolve(stdout + stderr); } + }); + }); + stream.markdown(`Worktree created at \`${worktreePath}\`.\n\n\`\`\`\n${result.trim()}\n\`\`\``); + } } else { - stream.markdown("No dependencies found."); + stream.markdown("**Usage:** `@ctx /worktree [list|add ]`"); } } catch (err: unknown) { - stream.markdown( - `**Error:** Failed to check dependencies.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` - ); + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); } - return { metadata: { command: "dep" } }; + return { metadata: { command: "worktree" } }; } -async function handleGuide( +async function handlePause( + stream: vscode.ChatResponseStream, + cwd: string, + token: vscode.CancellationToken +): Promise { + stream.progress("Pausing session..."); + try { + const stateDir = path.join(cwd, ".context", "state"); + if (!fs.existsSync(stateDir)) { fs.mkdirSync(stateDir, { recursive: true }); } + const { stdout } = await runCtx(["status"], cwd, token); + const state = { paused_at: new Date().toISOString(), status: stdout.trim(), cwd }; + fs.writeFileSync(path.join(stateDir, "paused-session.json"), JSON.stringify(state, null, 2), "utf-8"); + stream.markdown("Session paused. State saved to `.context/state/paused-session.json`.\n\nResume with `@ctx /resume`."); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "pause" } }; +} + +async function handleResume( stream: vscode.ChatResponseStream, cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Loading quick start guide..."); + stream.progress("Resuming session..."); try { - const { stdout, stderr } = await runCtx(["guide", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const statePath = path.join(cwd, ".context", "state", "paused-session.json"); + if (!fs.existsSync(statePath)) { + stream.markdown("No paused session found. Start fresh with `@ctx /status`."); + return { metadata: { command: "resume" } }; + } + const state = JSON.parse(fs.readFileSync(statePath, "utf-8")); + stream.markdown("## Resuming Session\n\n" + `Paused at: ${state.paused_at}\n\n` + + "### Status at pause\n```\n" + state.status + "\n```\n\n"); + try { + const { stdout } = await runCtx(["status"], cwd, token); + stream.markdown("### Current Status\n```\n" + stdout.trim() + "\n```\n"); + } catch { /* non-fatal */ } + fs.unlinkSync(statePath); + stream.markdown("\nSession resumed. Pause file removed."); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + return { metadata: { command: "resume" } }; +} + +async function handleMemory( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + token: vscode.CancellationToken +): Promise { + const parts = prompt.trim().split(/\s+/); + const subcmd = parts[0]?.toLowerCase(); + const rest = parts.slice(1).join(" "); + + let args: string[]; + let progressMsg: string; + + switch (subcmd) { + case "sync": + args = ["memory", "sync"]; + progressMsg = "Syncing memory bridge..."; + break; + case "status": + args = ["memory", "status"]; + progressMsg = "Checking memory bridge status..."; + break; + case "diff": + args = ["memory", "diff"]; + progressMsg = "Comparing memory with context..."; + break; + case "import": + args = rest ? ["memory", "import", rest] : ["memory", "import"]; + progressMsg = "Importing memory..."; + break; + case "publish": + args = rest ? ["memory", "publish", rest] : ["memory", "publish"]; + progressMsg = "Publishing memory..."; + break; + case "unpublish": + args = rest ? ["memory", "unpublish", rest] : ["memory", "unpublish"]; + progressMsg = "Unpublishing memory..."; + break; + default: + stream.markdown( + "**Usage:** `@ctx /memory `\n\n" + + "| Subcommand | Description |\n" + + "|------------|-------------|\n" + + "| `sync` | Sync Claude Code memory bridge |\n" + + "| `status` | Show memory bridge status |\n" + + "| `diff` | Compare memory with context files |\n" + + "| `import` | Import from Claude Code memory |\n" + + "| `publish` | Publish context to Claude Code memory |\n" + + "| `unpublish` | Remove published memory |\n\n" + + "Example: `@ctx /memory sync` or `@ctx /memory diff`" + ); + return { metadata: { command: "memory" } }; + } + + stream.progress(progressMsg); + try { + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { - stream.markdown(output); + stream.markdown("```\n" + output + "\n```"); } else { - stream.markdown("No guide content available."); + stream.markdown("No output."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to load guide.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Memory command failed.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "guide" } }; + return { metadata: { command: "memory" } }; } -async function handlePermission( +async function handleDecisions( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + token: vscode.CancellationToken +): Promise { + const subcmd = prompt.trim().split(/\s+/)[0]?.toLowerCase(); + + if (subcmd === "reindex") { + stream.progress("Reindexing decisions..."); + try { + const { stdout, stderr } = await runCtx(["decisions", "reindex"], cwd, token); + const output = mergeOutput(stdout, stderr); + stream.markdown(output ? "```\n" + output + "\n```" : "Decision index rebuilt."); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + } else { + stream.progress("Loading decisions..."); + try { + const { stdout, stderr } = await runCtx(["decisions"], cwd, token); + const output = mergeOutput(stdout, stderr); + stream.markdown(output ? "```\n" + output + "\n```" : "No decisions found."); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + } + return { metadata: { command: "decisions" } }; +} + +async function handleLearnings( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + token: vscode.CancellationToken +): Promise { + const subcmd = prompt.trim().split(/\s+/)[0]?.toLowerCase(); + + if (subcmd === "reindex") { + stream.progress("Reindexing learnings..."); + try { + const { stdout, stderr } = await runCtx(["learnings", "reindex"], cwd, token); + const output = mergeOutput(stdout, stderr); + stream.markdown(output ? "```\n" + output + "\n```" : "Learning index rebuilt."); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + } else { + stream.progress("Loading learnings..."); + try { + const { stdout, stderr } = await runCtx(["learnings"], cwd, token); + const output = mergeOutput(stdout, stderr); + stream.markdown(output ? "```\n" + output + "\n```" : "No learnings found."); + } catch (err: unknown) { + stream.markdown(`**Error:** ${err instanceof Error ? err.message : String(err)}`); + } + } + return { metadata: { command: "learnings" } }; +} + +async function handleConfig( stream: vscode.ChatResponseStream, prompt: string, cwd: string, @@ -1277,164 +2038,193 @@ async function handlePermission( ): Promise { const parts = prompt.trim().split(/\s+/); const subcmd = parts[0]?.toLowerCase(); + const rest = parts.slice(1).join(" "); let args: string[]; let progressMsg: string; switch (subcmd) { - case "snapshot": - args = ["permission", "snapshot"]; - progressMsg = "Taking permission snapshot..."; + case "switch": + if (!rest) { + stream.markdown("**Usage:** `@ctx /config switch `\n\nExample: `@ctx /config switch dev`"); + return { metadata: { command: "config" } }; + } + args = ["config", "switch", rest]; + progressMsg = `Switching to profile "${rest}"...`; break; - case "restore": - args = ["permission", "restore"]; - progressMsg = "Restoring permissions..."; + case "status": + args = ["config", "status"]; + progressMsg = "Checking config status..."; + break; + case "schema": + args = ["config", "schema"]; + progressMsg = "Loading config schema..."; break; default: stream.markdown( - "**Usage:** `@ctx /permission `\n\n" + + "**Usage:** `@ctx /config `\n\n" + "| Subcommand | Description |\n" + "|------------|-------------|\n" + - "| `snapshot` | Capture current file permissions |\n" + - "| `restore` | Restore saved permissions |\n\n" + - "Example: `@ctx /permission snapshot`" + "| `switch ` | Switch to a config profile |\n" + + "| `status` | Show current config profile |\n" + + "| `schema` | Show config schema |\n\n" + + "Example: `@ctx /config switch dev`" ); - return { metadata: { command: "permission" } }; + return { metadata: { command: "config" } }; } - args.push("--no-color"); stream.progress(progressMsg); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { - stream.markdown(`Permission ${subcmd} completed.`); + stream.markdown("No output."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to ${subcmd} permissions.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Config command failed.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "permission" } }; + return { metadata: { command: "config" } }; } -async function handleSite( +async function handlePermissions( stream: vscode.ChatResponseStream, prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - const parts = prompt.trim().split(/\s+/); - const subcmd = parts[0]?.toLowerCase(); + const subcmd = prompt.trim().split(/\s+/)[0]?.toLowerCase(); let args: string[]; let progressMsg: string; switch (subcmd) { - case "feed": - args = ["site", "feed"]; - progressMsg = "Generating site feed..."; + case "snapshot": + args = ["permissions", "snapshot"]; + progressMsg = "Saving permissions snapshot..."; + break; + case "restore": + args = ["permissions", "restore"]; + progressMsg = "Restoring permissions..."; break; default: stream.markdown( - "**Usage:** `@ctx /site `\n\n" + + "**Usage:** `@ctx /permissions `\n\n" + "| Subcommand | Description |\n" + "|------------|-------------|\n" + - "| `feed` | Generate documentation site feed |\n\n" + - "Example: `@ctx /site feed`" + "| `snapshot` | Backup current Claude settings |\n" + + "| `restore` | Restore settings from backup |\n\n" + + "Example: `@ctx /permissions snapshot`" ); - return { metadata: { command: "site" } }; + return { metadata: { command: "permissions" } }; } - args.push("--no-color"); stream.progress(progressMsg); try { const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { - stream.markdown(`Site ${subcmd} completed.`); + stream.markdown(subcmd === "snapshot" ? "Permissions snapshot saved." : "Permissions restored."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to run site ${subcmd}.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Permissions command failed.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "site" } }; + return { metadata: { command: "permissions" } }; } -async function handleLoop( +async function handleChanges( stream: vscode.ChatResponseStream, prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - const toolName = prompt.trim(); - const args = toolName ? ["loop", toolName] : ["loop"]; - args.push("--no-color"); - - stream.progress("Generating iteration script..."); + stream.progress("Checking what changed..."); try { + const args = ["changes"]; + const sinceMatch = prompt.match(/--since\s+(\S+)/); + if (sinceMatch) { + args.push("--since", sinceMatch[1]); + } const { stdout, stderr } = await runCtx(args, cwd, token); - const output = (stdout + stderr).trim(); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { - stream.markdown("No loop script generated."); + stream.markdown("No changes detected since last session."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to generate loop script.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Failed to check changes.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "loop" } }; + return { metadata: { command: "changes" } }; } -async function handlePause( +async function handleDeps( stream: vscode.ChatResponseStream, + prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Pausing context hooks..."); + stream.progress("Analyzing dependencies..."); try { - const { stdout, stderr } = await runCtx(["pause", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const args = ["deps"]; + const formatMatch = prompt.match(/--format\s+(\S+)/); + if (formatMatch) { + args.push("--format", formatMatch[1]); + } + if (prompt.includes("--external")) { + args.push("--external"); + } + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { - stream.markdown("```\n" + output + "\n```"); + stream.markdown(output); } else { - stream.markdown("Context hooks paused for this session."); + stream.markdown("No dependency information available."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to pause hooks.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Failed to analyze dependencies.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "pause" } }; + return { metadata: { command: "deps" } }; } -async function handleResume( +async function handleGuide( stream: vscode.ChatResponseStream, + prompt: string, cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Resuming context hooks..."); + stream.progress("Loading guide..."); try { - const { stdout, stderr } = await runCtx(["resume", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const args = ["guide"]; + if (prompt.includes("--skills")) { + args.push("--skills"); + } else if (prompt.includes("--commands")) { + args.push("--commands"); + } + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { - stream.markdown("```\n" + output + "\n```"); + stream.markdown(output); } else { - stream.markdown("Context hooks resumed."); + stream.markdown("No guide output."); } } catch (err: unknown) { stream.markdown( - `**Error:** Failed to resume hooks.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + `**Error:** Failed to load guide.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` ); } - return { metadata: { command: "resume" } }; + return { metadata: { command: "guide" } }; } async function handleReindex( @@ -1442,14 +2232,14 @@ async function handleReindex( cwd: string, token: vscode.CancellationToken ): Promise { - stream.progress("Rebuilding context file indices..."); + stream.progress("Regenerating indices..."); try { - const { stdout, stderr } = await runCtx(["reindex", "--no-color"], cwd, token); - const output = (stdout + stderr).trim(); + const { stdout, stderr } = await runCtx(["reindex"], cwd, token); + const output = mergeOutput(stdout, stderr); if (output) { stream.markdown("```\n" + output + "\n```"); } else { - stream.markdown("Context indices rebuilt."); + stream.markdown("Indices regenerated for DECISIONS.md and LEARNINGS.md."); } } catch (err: unknown) { stream.markdown( @@ -1459,6 +2249,33 @@ async function handleReindex( return { metadata: { command: "reindex" } }; } +async function handleWhy( + stream: vscode.ChatResponseStream, + prompt: string, + cwd: string, + token: vscode.CancellationToken +): Promise { + stream.progress("Loading philosophy..."); + try { + const args = ["why"]; + if (prompt.trim()) { + args.push(prompt.trim()); + } + const { stdout, stderr } = await runCtx(args, cwd, token); + const output = mergeOutput(stdout, stderr); + if (output) { + stream.markdown(output); + } else { + stream.markdown("No philosophy content available."); + } + } catch (err: unknown) { + stream.markdown( + `**Error:** Failed to load philosophy.\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\`` + ); + } + return { metadata: { command: "why" } }; +} + async function handleFreeform( request: vscode.ChatRequest, stream: vscode.ChatResponseStream, @@ -1481,13 +2298,13 @@ async function handleFreeform( return handleRecall(stream, request.prompt, cwd, token); } if (prompt.includes("complete") || prompt.includes("done") || prompt.includes("finish")) { - return handleTask(stream, "complete " + request.prompt, cwd, token); + return handleComplete(stream, request.prompt, cwd, token); } if (prompt.includes("remind")) { return handleRemind(stream, request.prompt, cwd, token); } if (prompt.includes("task")) { - return handleTask(stream, request.prompt, cwd, token); + return handleTasks(stream, request.prompt, cwd, token); } if (prompt.includes("pad") || prompt.includes("scratchpad") || prompt.includes("scratch")) { return handlePad(stream, request.prompt, cwd, token); @@ -1498,88 +2315,159 @@ async function handleFreeform( if (prompt.includes("system") || prompt.includes("resource") || prompt.includes("bootstrap")) { return handleSystem(stream, request.prompt, cwd, token); } - if (prompt.includes("memory")) { - return handleMemory(stream, request.prompt, cwd, token); + if (prompt.includes("wrap") || prompt.includes("end session") || prompt.includes("closing")) { + return handleWrapup(stream, cwd, token); + } + if (prompt.includes("remember") || prompt.includes("last session") || prompt.includes("what were we")) { + return handleRemember(stream, request.prompt, cwd, token); + } + if (prompt.includes("next") || prompt.includes("what should") || prompt.includes("pick task")) { + return handleNext(stream, cwd, token); + } + if (prompt.includes("brainstorm") || prompt.includes("idea")) { + return handleBrainstorm(stream, request.prompt, cwd, token); + } + if (prompt.includes("reflect") || prompt.includes("persist") || prompt.includes("worth saving")) { + return handleReflect(stream, cwd, token); + } + if (prompt.includes("spec") || prompt.includes("scaffold")) { + return handleSpec(stream, request.prompt, cwd, token); + } + if (prompt.includes("implement") || prompt.includes("execution plan")) { + return handleImplement(stream, cwd, token); + } + if (prompt.includes("verify") || prompt.includes("qa") || prompt.includes("lint")) { + return handleVerify(stream, cwd, token); + } + if (prompt.includes("map") || prompt.includes("dependencies") || prompt.includes("deps")) { + return handleMap(stream, cwd, token); + } + if (prompt.includes("prompt template") || prompt.includes("prompts")) { + return handlePromptTpl(stream, request.prompt, cwd, token); + } + if (prompt.includes("blog") && prompt.includes("changelog")) { + return handleChangelog(stream, cwd, token); + } + if (prompt.includes("blog") || prompt.includes("post")) { + return handleBlog(stream, request.prompt, cwd, token); + } + if (prompt.includes("changelog") || prompt.includes("release notes")) { + return handleChangelog(stream, cwd, token); + } + if (prompt.includes("link") || prompt.includes("broken") || prompt.includes("dead link")) { + return handleCheckLinks(stream, cwd, token); } - if (prompt.includes("journal")) { + if (prompt.includes("journal") || prompt.includes("log entries")) { return handleJournal(stream, request.prompt, cwd, token); } - if (prompt.includes("doctor") || prompt.includes("health")) { - return handleDoctor(stream, cwd, token); + if (prompt.includes("consolidate") || prompt.includes("merge entries") || prompt.includes("duplicate")) { + return handleConsolidate(stream, cwd, token); } - if (prompt.includes("config") || prompt.includes("configuration")) { + if (prompt.includes("memory bridge") || prompt.includes("memory sync") || prompt.includes("memory status") || prompt.includes("memory diff") || prompt.includes("memory import") || prompt.includes("memory publish")) { + return handleMemory(stream, request.prompt, cwd, token); + } + if (prompt.includes("decisions") || prompt.includes("decision list") || prompt.includes("decision reindex")) { + return handleDecisions(stream, request.prompt, cwd, token); + } + if (prompt.includes("learnings") || prompt.includes("learning list") || prompt.includes("learning reindex")) { + return handleLearnings(stream, request.prompt, cwd, token); + } + if (prompt.includes("config") || prompt.includes("profile") || prompt.includes("switch profile")) { return handleConfig(stream, request.prompt, cwd, token); } - if (prompt.includes("prompt") || prompt.includes("template")) { - return handlePrompt(stream, request.prompt, cwd, token); + if (prompt.includes("permissions") || prompt.includes("permission snapshot") || prompt.includes("permission restore")) { + return handlePermissions(stream, request.prompt, cwd, token); } - if (prompt.includes("why") || prompt.includes("rationale")) { - return handleWhy(stream, request.prompt, cwd, token); + if (prompt.includes("changes") || prompt.includes("what changed") || prompt.includes("since last session")) { + return handleChanges(stream, request.prompt, cwd, token); } - if (prompt.includes("change") || prompt.includes("recent")) { - return handleChange(stream, cwd, token); + if (prompt.includes("deps") || prompt.includes("dependency graph") || prompt.includes("package graph")) { + return handleDeps(stream, request.prompt, cwd, token); } - if (prompt.includes("dep") || prompt.includes("dependenc")) { - return handleDep(stream, cwd, token); + if (prompt.includes("guide") || prompt.includes("cheat sheet") || prompt.includes("quick reference")) { + return handleGuide(stream, request.prompt, cwd, token); } - if (prompt.includes("guide") || prompt.includes("quickstart") || prompt.includes("getting started")) { - return handleGuide(stream, cwd, token); + if (prompt.includes("reindex") || prompt.includes("rebuild index") || prompt.includes("regenerate index")) { + return handleReindex(stream, cwd, token); } - if (prompt.includes("permission")) { - return handlePermission(stream, request.prompt, cwd, token); + if (prompt.includes("why") || prompt.includes("philosophy") || prompt.includes("manifesto")) { + return handleWhy(stream, request.prompt, cwd, token); } - if (prompt.includes("site") || prompt.includes("feed")) { - return handleSite(stream, request.prompt, cwd, token); + if (prompt.includes("audit") || prompt.includes("alignment")) { + return handleAudit(stream, cwd, token); } - if (prompt.includes("loop") || prompt.includes("iterate")) { - return handleLoop(stream, request.prompt, cwd, token); + if (prompt.includes("worktree")) { + return handleWorktree(stream, request.prompt, cwd, token); } - if (prompt.includes("pause")) { + if (prompt.includes("pause") || prompt.includes("save state")) { return handlePause(stream, cwd, token); } - if (prompt.includes("resume")) { + if (prompt.includes("resume") || prompt.includes("restore state") || prompt.includes("continue session")) { return handleResume(stream, cwd, token); } - if (prompt.includes("reindex") || prompt.includes("rebuild")) { - return handleReindex(stream, cwd, token); + + // 2.5: Specs nudge — remind about specs when planning + if (prompt.includes("plan") || prompt.includes("design") || prompt.includes("architect")) { + const specsDir = path.join(cwd, "specs"); + if (fs.existsSync(specsDir)) { + const specs = fs.readdirSync(specsDir).filter((f) => f.endsWith(".md")); + if (specs.length > 0) { + stream.markdown("> **specs/** has " + specs.length + " spec(s). Review with `@ctx /spec` before designing.\n\n"); + } + } } // Default: show help with available commands stream.markdown( - "## ctx -- Persistent Context for AI\n\n" + + "## ctx — Persistent Context for AI\n\n" + "Available commands:\n\n" + "| Command | Description |\n" + "|---------|-------------|\n" + "| `/init` | Initialize `.context/` directory |\n" + "| `/status` | Show context summary |\n" + - "| `/agent` | Print AI-ready context packet |\n" + + "| `/agent [--budget N]` | Print AI-ready context packet |\n" + "| `/drift` | Detect stale or invalid context |\n" + - "| `/recall` | Browse session history |\n" + - "| `/setup` | Generate tool integration configs |\n" + - "| `/add` | Add task, decision, or learning |\n" + + "| `/recall [show\\|export\\|lock\\|unlock\\|sync]` | Browse session history |\n" + + "| `/hook` | Generate tool integration configs |\n" + + "| `/add` | Add task, decision, learning, or convention |\n" + "| `/load` | Output assembled context |\n" + "| `/compact` | Archive completed tasks |\n" + "| `/sync` | Reconcile context with codebase |\n" + - "| `/task` | Task operations (complete, archive, snapshot) |\n" + + "| `/complete` | Mark a task as completed |\n" + "| `/remind` | Manage session reminders |\n" + - "| `/pad` | Encrypted scratchpad |\n" + + "| `/tasks` | Archive or snapshot tasks |\n" + + "| `/decisions [reindex]` | List or reindex decisions |\n" + + "| `/learnings [reindex]` | List or reindex learnings |\n" + + "| `/pad [resolve\\|import\\|export\\|merge]` | Encrypted scratchpad |\n" + "| `/notify` | Webhook notifications |\n" + - "| `/system` | System diagnostics |\n" + - "| `/memory` | Memory bridge operations |\n" + - "| `/journal` | Journal management |\n" + - "| `/doctor` | Context health diagnostics |\n" + - "| `/config` | Runtime configuration |\n" + - "| `/prompt` | Prompt templates |\n" + - "| `/why` | Design rationale for context files |\n" + - "| `/change` | Recent codebase changes |\n" + - "| `/dep` | Project dependencies |\n" + - "| `/guide` | Quick start guide |\n" + - "| `/permission` | Permission snapshot/restore |\n" + - "| `/site` | Documentation site |\n" + - "| `/loop` | Generate iteration scripts |\n" + - "| `/pause` | Pause context hooks |\n" + - "| `/resume` | Resume context hooks |\n" + - "| `/reindex` | Rebuild context indices |\n\n" + + "| `/memory [sync\\|status\\|diff\\|import\\|publish]` | Claude Code memory bridge |\n" + + "| `/system [stats\\|backup\\|message]` | System diagnostics |\n" + + "| `/config [switch\\|status\\|schema]` | Config profile management |\n" + + "| `/permissions [snapshot\\|restore]` | Claude settings backup |\n" + + "| `/wrapup` | End-of-session wrap-up |\n" + + "| `/remember [--limit N]` | Recall recent sessions |\n" + + "| `/next` | Show next open task |\n" + + "| `/brainstorm` | Browse and develop ideas |\n" + + "| `/reflect` | Surface items worth persisting |\n" + + "| `/spec` | List or scaffold feature specs |\n" + + "| `/implement` | Show implementation plan |\n" + + "| `/verify` | Run verification checks |\n" + + "| `/map` | Show dependency map |\n" + + "| `/prompt [add\\|rm]` | Manage prompt templates |\n" + + "| `/blog` | Draft blog post from context |\n" + + "| `/changelog` | Recent commits for changelog |\n" + + "| `/check-links` | Audit local links in context |\n" + + "| `/journal [site\\|obsidian]` | View or export journal |\n" + + "| `/consolidate` | Find duplicate entries |\n" + + "| `/audit` | Alignment audit (drift + conventions) |\n" + + "| `/worktree` | Git worktree management |\n" + + "| `/pause` | Save session state |\n" + + "| `/resume` | Restore paused session |\n" + + "| `/changes [--since duration]` | Show what changed since last session |\n" + + "| `/deps [--format mermaid\\|table\\|json]` | Package dependency graph |\n" + + "| `/guide [--skills\\|--commands]` | Quick-reference cheat sheet |\n" + + "| `/reindex` | Regenerate decision/learning indices |\n" + + "| `/why [topic]` | Read ctx philosophy |\n\n" + "Example: `@ctx /status` or `@ctx /add task Fix login bug`" ); return { metadata: { command: "help" } }; @@ -1613,19 +2501,28 @@ const handler: vscode.ChatRequestHandler = async ( return { metadata: { command: request.command || "none" } }; } + // For non-init commands, verify .context/ exists + if (request.command !== "init" && !hasContextDir(cwd)) { + stream.markdown( + "**Not initialized.** This project doesn't have a `.context/` directory yet.\n\n" + + "Run `@ctx /init` to set up persistent context for this project." + ); + return { metadata: { command: request.command || "none" } }; + } + switch (request.command) { case "init": return handleInit(stream, cwd, token); case "status": return handleStatus(stream, cwd, token); case "agent": - return handleAgent(stream, cwd, token); + return handleAgent(stream, request.prompt, cwd, token); case "drift": return handleDrift(stream, cwd, token); case "recall": return handleRecall(stream, request.prompt, cwd, token); - case "setup": - return handleSetup(stream, request.prompt, cwd, token); + case "hook": + return handleHook(stream, request.prompt, cwd, token); case "add": return handleAdd(stream, request.prompt, cwd, token); case "load": @@ -1634,46 +2531,76 @@ const handler: vscode.ChatRequestHandler = async ( return handleCompact(stream, cwd, token); case "sync": return handleSync(stream, cwd, token); - case "task": - return handleTask(stream, request.prompt, cwd, token); + case "complete": + return handleComplete(stream, request.prompt, cwd, token); case "remind": return handleRemind(stream, request.prompt, cwd, token); + case "tasks": + return handleTasks(stream, request.prompt, cwd, token); case "pad": return handlePad(stream, request.prompt, cwd, token); case "notify": return handleNotify(stream, request.prompt, cwd, token); case "system": return handleSystem(stream, request.prompt, cwd, token); - case "memory": - return handleMemory(stream, request.prompt, cwd, token); + case "wrapup": + return handleWrapup(stream, cwd, token); + case "remember": + return handleRemember(stream, request.prompt, cwd, token); + case "next": + return handleNext(stream, cwd, token); + case "brainstorm": + return handleBrainstorm(stream, request.prompt, cwd, token); + case "reflect": + return handleReflect(stream, cwd, token); + case "spec": + return handleSpec(stream, request.prompt, cwd, token); + case "implement": + return handleImplement(stream, cwd, token); + case "verify": + return handleVerify(stream, cwd, token); + case "map": + return handleMap(stream, cwd, token); + case "prompt": + return handlePromptTpl(stream, request.prompt, cwd, token); + case "blog": + return handleBlog(stream, request.prompt, cwd, token); + case "changelog": + return handleChangelog(stream, cwd, token); + case "check-links": + return handleCheckLinks(stream, cwd, token); case "journal": return handleJournal(stream, request.prompt, cwd, token); - case "doctor": - return handleDoctor(stream, cwd, token); - case "config": - return handleConfig(stream, request.prompt, cwd, token); - case "prompt": - return handlePrompt(stream, request.prompt, cwd, token); - case "why": - return handleWhy(stream, request.prompt, cwd, token); - case "change": - return handleChange(stream, cwd, token); - case "dep": - return handleDep(stream, cwd, token); - case "guide": - return handleGuide(stream, cwd, token); - case "permission": - return handlePermission(stream, request.prompt, cwd, token); - case "site": - return handleSite(stream, request.prompt, cwd, token); - case "loop": - return handleLoop(stream, request.prompt, cwd, token); + case "consolidate": + return handleConsolidate(stream, cwd, token); + case "audit": + return handleAudit(stream, cwd, token); + case "worktree": + return handleWorktree(stream, request.prompt, cwd, token); case "pause": return handlePause(stream, cwd, token); case "resume": return handleResume(stream, cwd, token); + case "memory": + return handleMemory(stream, request.prompt, cwd, token); + case "decisions": + return handleDecisions(stream, request.prompt, cwd, token); + case "learnings": + return handleLearnings(stream, request.prompt, cwd, token); + case "config": + return handleConfig(stream, request.prompt, cwd, token); + case "permissions": + return handlePermissions(stream, request.prompt, cwd, token); + case "changes": + return handleChanges(stream, request.prompt, cwd, token); + case "deps": + return handleDeps(stream, request.prompt, cwd, token); + case "guide": + return handleGuide(stream, request.prompt, cwd, token); case "reindex": return handleReindex(stream, cwd, token); + case "why": + return handleWhy(stream, request.prompt, cwd, token); default: return handleFreeform(request, stream, cwd, token); } @@ -1711,15 +2638,14 @@ export function activate(extensionContext: vscode.ExtensionContext) { { prompt: "Show my context status", command: "status" }, { prompt: "Generate copilot integration", - command: "setup", + command: "hook", } ); break; case "status": followups.push( { prompt: "Detect context drift", command: "drift" }, - { prompt: "Load full context", command: "load" }, - { prompt: "Run health check", command: "doctor" } + { prompt: "Load full context", command: "load" } ); break; case "drift": @@ -1728,10 +2654,10 @@ export function activate(extensionContext: vscode.ExtensionContext) { { prompt: "Show context status", command: "status" } ); break; - case "task": + case "complete": followups.push( { prompt: "Show context status", command: "status" }, - { prompt: "Compact context", command: "compact" } + { prompt: "Archive completed tasks", command: "tasks" } ); break; case "remind": @@ -1739,48 +2665,163 @@ export function activate(extensionContext: vscode.ExtensionContext) { { prompt: "Show context status", command: "status" } ); break; + case "tasks": + followups.push( + { prompt: "Show context status", command: "status" }, + { prompt: "Compact context", command: "compact" } + ); + break; case "pad": followups.push( { prompt: "List scratchpad", command: "pad" } ); break; - case "memory": + case "help": followups.push( - { prompt: "Check memory status", command: "memory" }, + { prompt: "Initialize project context", command: "init" }, { prompt: "Show context status", command: "status" } ); break; - case "doctor": + case "wrapup": + followups.push( + { prompt: "Add a decision", command: "add" }, + { prompt: "Add a learning", command: "add" } + ); + break; + case "remember": followups.push( { prompt: "Show context status", command: "status" }, - { prompt: "Detect drift", command: "drift" } + { prompt: "Load full context", command: "load" } ); break; - case "config": + case "next": + followups.push( + { prompt: "Mark task completed", command: "complete" }, + { prompt: "Show all tasks", command: "status" } + ); + break; + case "brainstorm": + followups.push( + { prompt: "Show context status", command: "status" }, + { prompt: "Add a task", command: "add" } + ); + break; + case "reflect": + followups.push( + { prompt: "Add a decision", command: "add" }, + { prompt: "Add a learning", command: "add" }, + { prompt: "Wrap up session", command: "wrapup" } + ); + break; + case "spec": + followups.push( + { prompt: "Show implementation plan", command: "implement" }, + { prompt: "Run verification", command: "verify" } + ); + break; + case "implement": + followups.push( + { prompt: "Show next task", command: "next" }, + { prompt: "Run verification", command: "verify" } + ); + break; + case "verify": followups.push( - { prompt: "Show config status", command: "config" } + { prompt: "Show context status", command: "status" }, + { prompt: "Run alignment audit", command: "audit" } + ); + break; + case "map": + followups.push( + { prompt: "Show context status", command: "status" } + ); + break; + case "blog": + case "changelog": + followups.push( + { prompt: "Show context status", command: "status" } ); break; - case "change": + case "consolidate": followups.push( + { prompt: "Run alignment audit", command: "audit" }, { prompt: "Show context status", command: "status" } ); break; + case "audit": + followups.push( + { prompt: "Fix drift", command: "sync" }, + { prompt: "Add a convention", command: "add" } + ); + break; case "pause": followups.push( - { prompt: "Resume hooks", command: "resume" } + { prompt: "Resume session", command: "resume" } ); break; case "resume": followups.push( + { prompt: "Show next task", command: "next" }, { prompt: "Show context status", command: "status" } ); break; - case "help": + case "memory": + followups.push( + { prompt: "Show memory diff", command: "memory" }, + { prompt: "Show context status", command: "status" } + ); + break; + case "decisions": + followups.push( + { prompt: "Add a decision", command: "add" }, + { prompt: "Reindex decisions", command: "decisions" } + ); + break; + case "learnings": + followups.push( + { prompt: "Add a learning", command: "add" }, + { prompt: "Reindex learnings", command: "learnings" } + ); + break; + case "config": + followups.push( + { prompt: "Show config status", command: "config" }, + { prompt: "Show context status", command: "status" } + ); + break; + case "permissions": + followups.push( + { prompt: "Show context status", command: "status" } + ); + break; + case "changes": + followups.push( + { prompt: "Show context status", command: "status" }, + { prompt: "Load full context", command: "load" } + ); + break; + case "deps": + followups.push( + { prompt: "Show dependency map", command: "map" }, + { prompt: "Show context status", command: "status" } + ); + break; + case "guide": followups.push( - { prompt: "Initialize project context", command: "init" }, { prompt: "Show context status", command: "status" }, - { prompt: "Quick start guide", command: "guide" } + { prompt: "Read philosophy", command: "why" } + ); + break; + case "reindex": + followups.push( + { prompt: "List decisions", command: "decisions" }, + { prompt: "List learnings", command: "learnings" } + ); + break; + case "why": + followups.push( + { prompt: "Show guide", command: "guide" }, + { prompt: "Show context status", command: "status" } ); break; } @@ -1790,6 +2831,297 @@ export function activate(extensionContext: vscode.ExtensionContext) { }; extensionContext.subscriptions.push(participant); + + // --- Command palette entries — open chat with the right slash command --- + const paletteCommands: Array<[string, string]> = [ + ["ctx.init", "/init"], + ["ctx.status", "/status"], + ["ctx.agent", "/agent"], + ["ctx.drift", "/drift"], + ["ctx.recall", "/recall"], + ["ctx.hook", "/hook"], + ["ctx.add", "/add"], + ["ctx.load", "/load"], + ["ctx.compact", "/compact"], + ["ctx.sync", "/sync"], + ["ctx.complete", "/complete"], + ["ctx.remind", "/remind"], + ["ctx.tasks", "/tasks"], + ["ctx.pad", "/pad"], + ["ctx.notify", "/notify"], + ["ctx.system", "/system"], + ["ctx.wrapup", "/wrapup"], + ["ctx.remember", "/remember"], + ["ctx.next", "/next"], + ["ctx.brainstorm", "/brainstorm"], + ["ctx.reflect", "/reflect"], + ["ctx.spec", "/spec"], + ["ctx.implement", "/implement"], + ["ctx.verify", "/verify"], + ["ctx.map", "/map"], + ["ctx.prompt", "/prompt"], + ["ctx.blog", "/blog"], + ["ctx.changelog", "/changelog"], + ["ctx.checkLinks", "/check-links"], + ["ctx.journal", "/journal"], + ["ctx.consolidate", "/consolidate"], + ["ctx.audit", "/audit"], + ["ctx.worktree", "/worktree"], + ["ctx.pause", "/pause"], + ["ctx.resume", "/resume"], + ["ctx.memory", "/memory"], + ["ctx.decisions", "/decisions"], + ["ctx.learnings", "/learnings"], + ["ctx.config", "/config"], + ["ctx.permissions", "/permissions"], + ["ctx.changes", "/changes"], + ["ctx.deps", "/deps"], + ["ctx.guide", "/guide"], + ["ctx.reindex", "/reindex"], + ["ctx.why", "/why"], + ]; + for (const [cmdId, slash] of paletteCommands) { + extensionContext.subscriptions.push( + vscode.commands.registerCommand(cmdId, () => { + vscode.commands.executeCommand("workbench.action.chat.open", { + query: `@ctx ${slash}`, + }); + }) + ); + } + + // --- VS Code native hooks (equivalent to Claude Code hooks.json) --- + const cwd = getWorkspaceRoot(); + + // 2.18: Terminal command watcher — detect dangerous commands + if (cwd) { + const terminalWatcher = vscode.window.onDidStartTerminalShellExecution((e) => { + if (!bootstrapDone || !hasContextDir(cwd)) { + return; + } + const cmd = e.execution.commandLine?.value; + if (!cmd) { + return; + } + for (const pattern of DENY_COMMAND_PATTERNS) { + if (pattern.test(cmd)) { + recordViolation(cwd, "dangerous_command", cmd); + vscode.window.showWarningMessage( + `ctx: Dangerous command detected: ${cmd.slice(0, 80)}` + ); + return; + } + } + for (const pattern of DENY_COMMAND_SCRIPT_PATTERNS) { + if (pattern.test(cmd)) { + recordViolation(cwd, "hack_script", cmd); + vscode.window.showWarningMessage( + `ctx: Direct hack/ script execution detected. Use make targets instead.` + ); + return; + } + } + }); + extensionContext.subscriptions.push(terminalWatcher); + } + + // 2.19: Sensitive file edit watcher — detect modifications to .env, .pem, .key, credentials + // Note: only watches edits, not opens. onDidOpenTextDocument is too noisy + // (fires on search results, peek definition, git diffs, hover previews). + if (cwd) { + const fileEditWatcher = vscode.workspace.onDidChangeTextDocument((e) => { + if (!bootstrapDone || !hasContextDir(cwd)) { + return; + } + if (e.contentChanges.length === 0) { + return; + } + const rel = path.relative(cwd, e.document.uri.fsPath); + if (rel.startsWith("..")) { + return; + } + for (const pattern of SENSITIVE_FILE_PATTERNS) { + if (pattern.test(e.document.uri.fsPath)) { + recordViolation(cwd, "sensitive_file_edit", rel); + vscode.window.showWarningMessage( + `ctx: Sensitive file modified: ${rel}` + ); + return; + } + } + }); + extensionContext.subscriptions.push(fileEditWatcher); + } + + // 2.6: onDidSave → task completion check (PostToolUse Edit/Write equivalent) + const saveWatcher = vscode.workspace.onDidSaveTextDocument((doc) => { + if (!cwd || !bootstrapDone || !hasContextDir(cwd)) { + return; + } + // Only trigger for files inside the workspace, not for .context/ files themselves + const rel = path.relative(cwd, doc.uri.fsPath); + if (rel.startsWith(".context")) { + return; + } + // Fire and forget — non-blocking background check + runCtx(["system", "check-task-completion"], cwd).catch( + () => {} + ); + }); + extensionContext.subscriptions.push(saveWatcher); + + // 2.7: Git post-commit — detect commits and nudge for context capture + try { + const gitExtension = vscode.extensions.getExtension("vscode.git"); + if (gitExtension) { + const setupGitHook = (git: any) => { + try { + const api = git.getAPI(1); + if (api && api.repositories.length > 0) { + const repo = api.repositories[0]; + let lastCommit = repo.state.HEAD?.commit; + const commitListener = repo.state.onDidChange(() => { + const currentCommit = repo.state.HEAD?.commit; + if (currentCommit && currentCommit !== lastCommit) { + lastCommit = currentCommit; + if (!cwd || !bootstrapDone || !hasContextDir(cwd)) { + return; + } + vscode.window + .showInformationMessage( + "Commit succeeded. Record context or run QA?", + "Add Decision", + "Add Learning", + "Verify", + "Skip" + ) + .then((choice) => { + if (choice === "Add Decision") { + vscode.commands.executeCommand( + "workbench.action.chat.open", + { query: "@ctx /add decision " } + ); + } else if (choice === "Add Learning") { + vscode.commands.executeCommand( + "workbench.action.chat.open", + { query: "@ctx /add learning " } + ); + } else if (choice === "Verify") { + vscode.commands.executeCommand( + "workbench.action.chat.open", + { query: "@ctx /verify" } + ); + } + }); + } + }); + extensionContext.subscriptions.push(commitListener); + } + } catch { + // Git API not available + } + }; + + if (gitExtension.isActive) { + setupGitHook(gitExtension.exports); + } else { + gitExtension.activate().then(setupGitHook, () => {}); + } + } + } catch { + // Git extension not available + } + + // 2.9: Watch .context/ for external changes — refresh reminders + if (cwd) { + const contextWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(cwd, ".context/**") + ); + const onContextChange = () => { + if (hasContextDir(cwd)) { + updateReminderStatus(cwd); + // Re-generate copilot-instructions.md when context files change + runCtx(["hook", "copilot", "--write"], cwd).catch(() => {}); + } + }; + contextWatcher.onDidChange(onContextChange); + contextWatcher.onDidCreate(onContextChange); + contextWatcher.onDidDelete(onContextChange); + extensionContext.subscriptions.push(contextWatcher); + } + + // 2.17: Watch dependency files for staleness + if (cwd) { + const depWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(cwd, "{go.mod,go.sum,package.json,package-lock.json}") + ); + depWatcher.onDidChange(() => { + vscode.window.showInformationMessage( + "Dependencies changed. Review with @ctx /map?", + "View Map" + ).then((choice) => { + if (choice === "View Map") { + vscode.commands.executeCommand("workbench.action.chat.open", { query: "@ctx /map" }); + } + }); + }); + extensionContext.subscriptions.push(depWatcher); + } + + // 2.10: Status bar reminder indicator + reminderStatusBar = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 50 + ); + reminderStatusBar.name = "ctx Reminders"; + reminderStatusBar.command = undefined; // informational only + extensionContext.subscriptions.push(reminderStatusBar); + + // Check reminders periodically (every 5 minutes) + if (cwd && hasContextDir(cwd)) { + updateReminderStatus(cwd); + const reminderInterval = setInterval(() => { + updateReminderStatus(cwd); + // 2.14: Heartbeat — record session-alive timestamp + try { + const stateDir = path.join(cwd, ".context", "state"); + if (!fs.existsSync(stateDir)) { fs.mkdirSync(stateDir, { recursive: true }); } + fs.writeFileSync(path.join(stateDir, "heartbeat"), new Date().toISOString(), "utf-8"); + } catch { /* non-fatal */ } + }, 5 * 60 * 1000); + extensionContext.subscriptions.push({ + dispose: () => clearInterval(reminderInterval), + }); + + // 2.12: Session start ceremony + runCtx( + ["system", "session-event", "--type", "start", "--caller", "vscode"], + cwd + ).catch(() => {}); + } +} + +/** + * Update the status bar reminder indicator by checking due reminders. + */ +function updateReminderStatus(cwd: string): void { + if (!bootstrapDone || !reminderStatusBar) { + return; + } + runCtx(["system", "check-reminders"], cwd) + .then(({ stdout }) => { + const trimmed = stdout.trim(); + if (trimmed && !trimmed.includes("no reminders")) { + reminderStatusBar!.text = "$(bell) ctx"; + reminderStatusBar!.tooltip = trimmed; + reminderStatusBar!.show(); + } else { + reminderStatusBar!.hide(); + } + }) + .catch(() => { + reminderStatusBar!.hide(); + }); } export { @@ -1799,26 +3131,31 @@ export { ensureCtxAvailable, bootstrap, getPlatformInfo, - handleTask, + handleComplete, handleRemind, + handleTasks, handlePad, handleNotify, handleSystem, handleMemory, - handleJournal, - handleDoctor, + handleDecisions, + handleLearnings, handleConfig, - handlePrompt, - handleWhy, - handleChange, - handleDep, + handlePermissions, + handleChanges, + handleDeps, handleGuide, - handlePermission, - handleSite, - handleLoop, - handlePause, - handleResume, handleReindex, + handleWhy, }; -export function deactivate() {} +export function deactivate() { + // 2.12: Session end ceremony + const cwd = getWorkspaceRoot(); + if (cwd && hasContextDir(cwd)) { + runCtx( + ["system", "session-event", "--type", "end", "--caller", "vscode"], + cwd + ).catch(() => {}); + } +}