dev-mcp-server 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.env.example +23 -55
  2. package/README.md +609 -219
  3. package/cli.js +486 -160
  4. package/package.json +2 -2
  5. package/src/agents/BaseAgent.js +113 -0
  6. package/src/agents/dreamer.js +165 -0
  7. package/src/agents/improver.js +175 -0
  8. package/src/agents/specialists.js +202 -0
  9. package/src/agents/taskDecomposer.js +176 -0
  10. package/src/agents/teamCoordinator.js +153 -0
  11. package/src/api/routes/agents.js +172 -0
  12. package/src/api/routes/extras.js +115 -0
  13. package/src/api/routes/git.js +72 -0
  14. package/src/api/routes/ingest.js +60 -40
  15. package/src/api/routes/knowledge.js +59 -41
  16. package/src/api/routes/memory.js +41 -0
  17. package/src/api/routes/newRoutes.js +168 -0
  18. package/src/api/routes/pipelines.js +41 -0
  19. package/src/api/routes/planner.js +54 -0
  20. package/src/api/routes/query.js +24 -0
  21. package/src/api/routes/sessions.js +54 -0
  22. package/src/api/routes/tasks.js +67 -0
  23. package/src/api/routes/tools.js +85 -0
  24. package/src/api/routes/v5routes.js +196 -0
  25. package/src/api/server.js +133 -5
  26. package/src/context/compactor.js +151 -0
  27. package/src/context/contextEngineer.js +181 -0
  28. package/src/context/contextVisualizer.js +140 -0
  29. package/src/core/conversationEngine.js +231 -0
  30. package/src/core/indexer.js +169 -143
  31. package/src/core/ingester.js +141 -126
  32. package/src/core/queryEngine.js +286 -236
  33. package/src/cron/cronScheduler.js +260 -0
  34. package/src/dashboard/index.html +1181 -0
  35. package/src/lsp/symbolNavigator.js +220 -0
  36. package/src/memory/memoryManager.js +186 -0
  37. package/src/memory/teamMemory.js +111 -0
  38. package/src/messaging/messageBus.js +177 -0
  39. package/src/monitor/proactiveMonitor.js +337 -0
  40. package/src/pipelines/pipelineEngine.js +230 -0
  41. package/src/planner/plannerEngine.js +202 -0
  42. package/src/plugins/builtin/stats-plugin.js +29 -0
  43. package/src/plugins/pluginManager.js +144 -0
  44. package/src/prompts/promptEngineer.js +289 -0
  45. package/src/sessions/sessionManager.js +166 -0
  46. package/src/skills/skillsManager.js +263 -0
  47. package/src/storage/store.js +127 -105
  48. package/src/tasks/taskManager.js +151 -0
  49. package/src/tools/BashTool.js +154 -0
  50. package/src/tools/FileEditTool.js +280 -0
  51. package/src/tools/GitTool.js +212 -0
  52. package/src/tools/GrepTool.js +199 -0
  53. package/src/tools/registry.js +1380 -0
  54. package/src/utils/costTracker.js +69 -0
  55. package/src/utils/fileParser.js +176 -153
  56. package/src/utils/llmClient.js +355 -206
  57. package/src/watcher/fileWatcher.js +137 -0
  58. package/src/worktrees/worktreeManager.js +176 -0
package/cli.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
-
3
- const path = require('path');
4
- require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
2
+ require('dotenv').config();
5
3
 
6
4
  const { Command } = require('commander');
7
5
  const chalk = require('chalk');
@@ -12,237 +10,565 @@ const ingester = require('./src/core/ingester');
12
10
  const indexer = require('./src/core/indexer');
13
11
  const { QueryEngine, detectMode, QUERY_MODES } = require('./src/core/queryEngine');
14
12
  const store = require('./src/storage/store');
13
+ const GitTool = require('./src/tools/GitTool');
14
+ const BashTool = require('./src/tools/BashTool');
15
+ const GrepTool = require('./src/tools/GrepTool');
16
+ const { MemoryManager, MEMORY_TYPES } = require('./src/memory/memoryManager');
17
+ const { TaskManager, STATUS, PRIORITY } = require('./src/tasks/taskManager');
18
+ const sessionMgr = require('./src/sessions/sessionManager');
19
+ const plannerEngine = require('./src/planner/plannerEngine');
20
+ const costTracker = require('./src/utils/costTracker');
15
21
 
16
22
  const program = new Command();
17
23
 
18
24
  const banner = chalk.cyan(`
19
- ╔══════════════════════════════════════════════════════╗
20
- ║ Dev MCP Server — Model Context Platform
21
- ║ AI that understands YOUR codebase
22
- ╚══════════════════════════════════════════════════════╝
25
+ ╔══════════════════════════════════════════════════════════════╗
26
+ ║ Dev MCP Server v1.0 — Model Context Platform
27
+ ║ AI that understands YOUR codebase
28
+ ╚══════════════════════════════════════════════════════════════╝
23
29
  `);
24
30
 
31
+ // ── Helpers ────────────────────────────────────────────────────
32
+
33
+ const modeColor = { debug: chalk.red, usage: chalk.blue, impact: chalk.yellow, general: chalk.cyan };
34
+ const modeEmoji = { debug: '🐛', usage: '🔍', impact: '💥', general: '💬' };
35
+
36
+ async function askQuestion(question, opts = {}) {
37
+ const mode = opts.mode || detectMode(question);
38
+ const topK = parseInt(opts.topK || 8);
39
+ const sessionId = opts.sessionId || 'default';
40
+
41
+ const colorFn = modeColor[mode] || chalk.cyan;
42
+ console.log(colorFn(`\n${modeEmoji[mode]} Mode: ${mode.toUpperCase()}`));
43
+ console.log(chalk.bold(`Q: ${question}\n`));
44
+
45
+ const spinner = ora('Retrieving context and thinking...').start();
46
+ try {
47
+ const result = await QueryEngine.query(question, { mode, topK, sessionId });
48
+ spinner.stop();
49
+
50
+ console.log(chalk.gray('─'.repeat(64)));
51
+ console.log(chalk.gray(`Sources (${result.sources.length}) | Memories used: ${result.memoriesUsed}`));
52
+ result.sources.forEach((s, i) =>
53
+ console.log(chalk.gray(` [${i + 1}] ${s.file} (${s.kind}) — ${s.relevanceScore}`))
54
+ );
55
+ console.log(chalk.gray('─'.repeat(64)));
56
+ console.log('\n' + chalk.bold('Answer:\n'));
57
+ console.log(result.answer);
58
+ if (result.usage) {
59
+ const cost = costTracker.formatCost(result.usage.costUsd || 0);
60
+ console.log(chalk.gray(`\n[Tokens: ${result.usage.inputTokens}↑ ${result.usage.outputTokens}↓ | Cost: ${cost}]`));
61
+ }
62
+
63
+ // Persist to session
64
+ if (opts.sessionId) {
65
+ sessionMgr.addMessage(opts.sessionId, { role: 'user', content: question });
66
+ sessionMgr.addMessage(opts.sessionId, {
67
+ role: 'assistant', content: result.answer, mode,
68
+ sources: result.sources,
69
+ tokens: (result.usage?.inputTokens || 0) + (result.usage?.outputTokens || 0),
70
+ costUsd: result.usage?.costUsd || 0,
71
+ });
72
+ }
73
+ return result;
74
+ } catch (err) {
75
+ spinner.fail(chalk.red(`Error: ${err.message}`));
76
+ }
77
+ }
78
+
79
+ // ── INGEST ─────────────────────────────────────────────────────
25
80
  program
26
81
  .command('ingest <path>')
27
82
  .description('Ingest a file or directory into the knowledge base')
28
- .option('-t, --type <type>', 'Force type: code | config | documentation | log | schema')
29
- .action(async (inputPath, opts) => {
83
+ .action(async (inputPath) => {
30
84
  console.log(banner);
31
85
  const fs = require('fs');
32
- const stat = fs.statSync(inputPath);
33
86
  const spinner = ora();
34
-
87
+ const stat = fs.statSync(inputPath);
35
88
  if (stat.isDirectory()) {
36
- spinner.start(chalk.blue(`Scanning directory: ${inputPath}`));
37
- try {
38
- const result = await ingester.ingestDirectory(inputPath);
39
- spinner.succeed(chalk.green('Ingestion complete'));
40
- console.log('\n' + chalk.bold('Results:'));
41
- console.log(` ${chalk.green('✓')} Ingested: ${result.ingested} files`);
42
- console.log(` ${chalk.yellow('⚠')} Skipped: ${result.skipped} files`);
43
- console.log(` ${chalk.red('✗')} Failed: ${result.failed} files`);
44
- console.log(` ${chalk.cyan('◈')} Chunks: ${result.totalChunks} total`);
45
- if (result.errors.length > 0) {
46
- console.log('\n' + chalk.red('Errors:'));
47
- result.errors.slice(0, 5).forEach(e =>
48
- console.log(` ${e.file}: ${e.error}`)
49
- );
50
- }
51
- } catch (err) {
52
- spinner.fail(chalk.red(`Failed: ${err.message}`));
53
- process.exit(1);
54
- }
89
+ spinner.start(chalk.blue(`Scanning: ${inputPath}`));
90
+ const result = await ingester.ingestDirectory(inputPath);
91
+ spinner.succeed(chalk.green('Done'));
92
+ console.log(` ${chalk.green('')} ${result.ingested} files | ${result.totalChunks} chunks | ${result.skipped} skipped | ${result.failed} failed`);
55
93
  } else {
56
- spinner.start(chalk.blue(`Ingesting file: ${inputPath}`));
57
- try {
58
- const result = await ingester.ingestFile(inputPath);
59
- indexer.build();
60
- spinner.succeed(chalk.green(`Ingested: ${result.chunks} chunks`));
61
- } catch (err) {
62
- spinner.fail(chalk.red(`Failed: ${err.message}`));
63
- process.exit(1);
64
- }
94
+ spinner.start(chalk.blue(`Ingesting: ${inputPath}`));
95
+ const result = await ingester.ingestFile(inputPath);
96
+ indexer.build();
97
+ spinner.succeed(chalk.green(`${result.chunks} chunks indexed`));
65
98
  }
66
99
  });
67
100
 
101
+ // ── QUERY ──────────────────────────────────────────────────────
68
102
  program
69
103
  .command('query [question]')
70
104
  .description('Ask a question about your codebase')
71
- .option('-m, --mode <mode>', 'Force mode: debug | usage | impact | general')
72
- .option('-k, --top-k <n>', 'Number of context chunks', '8')
73
- .option('-i, --interactive', 'Start interactive REPL session')
105
+ .option('-m, --mode <mode>', 'Force mode: debug|usage|impact|general')
106
+ .option('-k, --top-k <n>', 'Context chunks to retrieve', '8')
107
+ .option('-s, --session <id>', 'Session ID for persistence')
108
+ .option('-i, --interactive', 'Start interactive REPL')
74
109
  .action(async (question, opts) => {
75
110
  console.log(banner);
76
-
77
111
  const stats = store.getStats();
78
112
  if (stats.totalDocs === 0) {
79
- console.log(chalk.yellow('⚠ Knowledge base is empty!'));
80
- console.log(chalk.gray(' Run: node cli.js ingest <path>'));
113
+ console.log(chalk.yellow('⚠ Knowledge base is empty — run: node cli.js ingest <path>'));
81
114
  process.exit(1);
82
115
  }
83
-
84
- console.log(chalk.gray(`📚 Knowledge base: ${stats.totalDocs} docs from ${stats.totalFiles} files\n`));
85
-
86
- if (opts.interactive || !question) {
87
- await startRepl();
88
- return;
89
- }
90
-
116
+ console.log(chalk.gray(`📚 ${stats.totalDocs} docs | 🧠 ${MemoryManager.getStats().total} memories\n`));
117
+ if (opts.interactive || !question) { await startRepl(opts); return; }
91
118
  await askQuestion(question, opts);
92
119
  });
93
120
 
94
- async function askQuestion(question, opts = {}) {
95
- const mode = opts.mode || detectMode(question);
96
- const topK = parseInt(opts.topK || 8);
121
+ // ── INTERACTIVE REPL ───────────────────────────────────────────
122
+ async function startRepl(opts = {}) {
123
+ let sessionId = opts.session;
124
+ if (!sessionId) {
125
+ const sess = sessionMgr.create({ name: `REPL ${new Date().toLocaleString()}` });
126
+ sessionId = sess.id;
127
+ console.log(chalk.gray(`📝 New session: ${sessionId}`));
128
+ } else {
129
+ console.log(chalk.gray(`📂 Resuming session: ${sessionId}`));
130
+ }
97
131
 
98
- const modeColors = {
99
- [QUERY_MODES.DEBUG]: chalk.red,
100
- [QUERY_MODES.USAGE]: chalk.blue,
101
- [QUERY_MODES.IMPACT]: chalk.yellow,
102
- [QUERY_MODES.GENERAL]: chalk.cyan,
103
- };
132
+ console.log(chalk.cyan('Interactive mode. Commands: /debug /usage /impact /plan /git /task /memory /cost /doctor /compact /exit\n'));
104
133
 
105
- const modeEmoji = {
106
- [QUERY_MODES.DEBUG]: '🐛',
107
- [QUERY_MODES.USAGE]: '🔍',
108
- [QUERY_MODES.IMPACT]: '💥',
109
- [QUERY_MODES.GENERAL]: '💬',
110
- };
134
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
135
+ const prompt = () => {
136
+ rl.question(chalk.bold.cyan('❯ '), async (input) => {
137
+ const trimmed = input.trim();
138
+ if (!trimmed) { prompt(); return; }
111
139
 
112
- const colorFn = modeColors[mode] || chalk.cyan;
113
- console.log(colorFn(`${modeEmoji[mode]} Mode: ${mode.toUpperCase()}`));
114
- console.log(chalk.bold(`\nQ: ${question}\n`));
140
+ // Slash commands
141
+ if (trimmed === '/exit' || trimmed === '/quit') {
142
+ console.log(chalk.gray(`\nSession saved: ${sessionId}`));
143
+ rl.close(); process.exit(0);
144
+ }
115
145
 
116
- const spinner = ora('Retrieving context and thinking...').start();
146
+ if (trimmed === '/cost') {
147
+ const s = costTracker.getSummary(sessionId);
148
+ console.log(chalk.cyan('\n💰 Cost Summary:'));
149
+ if (s.session) console.log(` Session: ${s.session.calls} calls | ${s.session.inputTokens + s.session.outputTokens} tokens | ${costTracker.formatCost(s.session.costUsd)}`);
150
+ console.log(` All-time: ${s.allTime.calls} calls | ${costTracker.formatCost(s.allTime.costUsd)}`);
151
+ prompt(); return;
152
+ }
117
153
 
118
- try {
119
- const result = await QueryEngine.query(question, { mode, topK });
120
- spinner.stop();
154
+ if (trimmed === '/doctor') {
155
+ const spinner = ora('Running diagnostics...').start();
156
+ const result = await plannerEngine.doctor();
157
+ spinner.stop();
158
+ console.log(chalk.bold('\n🩺 Doctor Report:'));
159
+ for (const c of result.checks) {
160
+ const icon = c.status === 'ok' ? chalk.green('✓') : c.status === 'warn' ? chalk.yellow('⚠') : chalk.red('✗');
161
+ console.log(` ${icon} ${c.name.padEnd(22)} ${chalk.gray(c.detail)}`);
162
+ }
163
+ console.log(chalk.gray(`\n ${result.summary.passed}/${result.summary.total} checks passed`));
164
+ prompt(); return;
165
+ }
121
166
 
122
- console.log(chalk.gray(''.repeat(60)));
123
- console.log(chalk.gray('Sources used:'));
124
- result.sources.forEach((s, i) => {
125
- console.log(chalk.gray(` [${i + 1}] ${s.file} (${s.kind}) — score: ${s.relevanceScore}`));
126
- });
127
- console.log(chalk.gray('─'.repeat(60)));
167
+ if (trimmed.startsWith('/plan ')) {
168
+ const task = trimmed.slice(6);
169
+ const spinner = ora('Generating plan...').start();
170
+ try {
171
+ const context = indexer.search(task, 5);
172
+ const plan = await plannerEngine.generatePlan(task, context, sessionId);
173
+ spinner.stop();
174
+ console.log(chalk.bold('\n📋 Plan:\n'));
175
+ console.log(plan.plan);
176
+ } catch (err) { spinner.fail(err.message); }
177
+ prompt(); return;
178
+ }
128
179
 
129
- console.log('\n' + chalk.bold('Answer:\n'));
130
- console.log(result.answer);
131
- console.log(chalk.gray(`\n[Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out]`));
180
+ if (trimmed.startsWith('/git ')) {
181
+ const sub = trimmed.slice(5);
182
+ await handleGitCommand(sub);
183
+ prompt(); return;
184
+ }
132
185
 
133
- } catch (err) {
134
- spinner.fail(chalk.red(`Error: ${err.message}`));
135
- }
136
- }
186
+ if (trimmed === '/git') {
187
+ const status = await GitTool.status();
188
+ console.log(chalk.bold('\n📦 Git Status:'));
189
+ console.log(` Branch: ${chalk.cyan(status.branch)} | ↑${status.ahead} ↓${status.behind}`);
190
+ if (status.staged.length) console.log(` Staged: ${status.staged.map(f => chalk.green(f.file)).join(', ')}`);
191
+ if (status.unstaged.length) console.log(` Modified: ${status.unstaged.map(f => chalk.yellow(f.file)).join(', ')}`);
192
+ if (status.untracked.length) console.log(` Untracked: ${status.untracked.slice(0, 5).map(f => chalk.gray(f)).join(', ')}`);
193
+ if (status.recentCommits.length) {
194
+ console.log(chalk.gray(' Recent commits:'));
195
+ status.recentCommits.slice(0, 3).forEach(c => console.log(chalk.gray(` ${c}`)));
196
+ }
197
+ prompt(); return;
198
+ }
137
199
 
138
- async function startRepl() {
139
- console.log(chalk.cyan('Starting interactive session. Type "exit" to quit, "help" for tips.\n'));
200
+ if (trimmed.startsWith('/task ')) {
201
+ await handleTaskCommand(trimmed.slice(6));
202
+ prompt(); return;
203
+ }
140
204
 
141
- const rl = readline.createInterface({
142
- input: process.stdin,
143
- output: process.stdout,
144
- });
205
+ if (trimmed === '/tasks') {
206
+ const tasks = TaskManager.list();
207
+ if (tasks.length === 0) { console.log(chalk.gray('No open tasks.')); }
208
+ else {
209
+ console.log(chalk.bold('\n📋 Open Tasks:'));
210
+ tasks.forEach(t => {
211
+ const p = t.priority === 'critical' ? chalk.red : t.priority === 'high' ? chalk.yellow : chalk.gray;
212
+ console.log(` ${p(`[${t.priority}]`)} #${t.id} ${t.title} ${chalk.gray(`(${t.status})`)}`);
213
+ });
214
+ }
215
+ prompt(); return;
216
+ }
145
217
 
146
- const prompt = () => {
147
- rl.question(chalk.bold.cyan('\n❯ '), async (input) => {
148
- const trimmed = input.trim();
218
+ if (trimmed === '/memory') {
219
+ const mems = MemoryManager.list().slice(0, 10);
220
+ console.log(chalk.bold('\n🧠 Recent Memories:'));
221
+ if (mems.length === 0) console.log(chalk.gray(' None yet.'));
222
+ mems.forEach(m => console.log(` [${chalk.cyan(m.type)}] ${m.content.slice(0, 90)}`));
223
+ prompt(); return;
224
+ }
149
225
 
150
- if (!trimmed) {
151
- prompt();
152
- return;
226
+ if (trimmed.startsWith('/memory add ')) {
227
+ const mem = MemoryManager.add(trimmed.slice(12));
228
+ console.log(chalk.green(`✓ Memory saved: ${mem.id}`));
229
+ prompt(); return;
153
230
  }
154
231
 
155
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
156
- console.log(chalk.cyan('\nGoodbye!\n'));
157
- rl.close();
158
- process.exit(0);
232
+ if (trimmed === '/compact') {
233
+ const history = sessionMgr.getHistory(sessionId, 30);
234
+ if (history.length < 4) { console.log(chalk.gray('Not enough history to compact.')); prompt(); return; }
235
+ const spinner = ora('Compacting conversation...').start();
236
+ const result = await plannerEngine.compact(history, sessionId);
237
+ spinner.succeed(`Compacted ${result.savedMessages} messages → summary`);
238
+ prompt(); return;
159
239
  }
160
240
 
161
- if (trimmed.toLowerCase() === 'help') {
162
- console.log(chalk.cyan(`
163
- Tips:
164
- 🐛 Debug: "Why is ClassCastException happening in UserService?"
165
- 🔍 Usage: "Where is getUserById used?"
166
- 💥 Impact: "If I change the User model, what breaks?"
167
- 💬 General: Any question about your codebase
168
- `));
169
- prompt();
170
- return;
241
+ if (trimmed.startsWith('/grep ')) {
242
+ const pattern = trimmed.slice(6);
243
+ const spinner = ora(`Searching for: ${pattern}`).start();
244
+ const result = await GrepTool.search(pattern, { maxResults: 20 });
245
+ spinner.stop();
246
+ console.log(chalk.bold(`\n🔎 ${result.total} matches (${result.tool}):`));
247
+ result.matches.slice(0, 15).forEach(m =>
248
+ console.log(` ${chalk.cyan(m.file)}:${chalk.yellow(m.lineNumber)} ${m.line.trim().slice(0, 80)}`)
249
+ );
250
+ prompt(); return;
171
251
  }
172
252
 
173
- if (trimmed.toLowerCase() === 'stats') {
174
- const stats = store.getStats();
175
- console.log(chalk.cyan(JSON.stringify(stats, null, 2)));
176
- prompt();
177
- return;
253
+ if (trimmed === '/help') {
254
+ console.log(chalk.cyan(`
255
+ Query modes (auto-detected):
256
+ 🐛 debug — "Why is X failing?"
257
+ 🔍 usage — "Where is X used?"
258
+ 💥 impact — "If I change X, what breaks?"
259
+ 💬 general — any other question
260
+
261
+ Slash commands:
262
+ /plan <task> Generate an execution plan
263
+ /git Git status
264
+ /git commit Auto-commit with AI message
265
+ /git review AI code review of changes
266
+ /git diff Show current diff
267
+ /tasks List open tasks
268
+ /task add <title> Create a task
269
+ /task done <id> Mark task complete
270
+ /memory Show stored memories
271
+ /memory add <text> Add a memory manually
272
+ /grep <pattern> Search codebase with ripgrep
273
+ /compact Compress conversation history
274
+ /cost Show token usage & cost
275
+ /doctor Check environment health
276
+ /exit Save & exit
277
+ `));
278
+ prompt(); return;
178
279
  }
179
280
 
180
- await askQuestion(trimmed);
281
+ // Default: treat as a query
282
+ await askQuestion(trimmed, { sessionId });
181
283
  prompt();
182
284
  });
183
285
  };
184
-
185
286
  prompt();
186
287
  }
187
288
 
289
+ async function handleGitCommand(sub) {
290
+ try {
291
+ if (sub === 'commit' || sub === 'commit --auto') {
292
+ const spinner = ora('Creating AI commit message...').start();
293
+ const result = await GitTool.commit({ autoMessage: true });
294
+ spinner.stop();
295
+ if (result.success) console.log(chalk.green(`\n✓ Committed: "${result.message}"`));
296
+ else console.log(chalk.yellow(`⚠ ${result.message}`));
297
+ } else if (sub === 'review') {
298
+ const spinner = ora('Running AI code review...').start();
299
+ const result = await GitTool.review({});
300
+ spinner.stop();
301
+ console.log(chalk.bold('\n📝 Code Review:\n'));
302
+ console.log(result.review);
303
+ if (result.hasIssues) console.log(chalk.red('\n⚠ Issues found — review before merging'));
304
+ } else if (sub === 'diff') {
305
+ const result = await GitTool.diff({});
306
+ if (!result.hasChanges) { console.log(chalk.gray('No changes.')); return; }
307
+ console.log(result.diff.slice(0, 3000));
308
+ } else if (sub === 'log') {
309
+ const commits = await GitTool.log({ oneline: true, limit: 10 });
310
+ console.log(chalk.bold('\n📜 Recent Commits:'));
311
+ commits.forEach(c => console.log(chalk.gray(` ${c}`)));
312
+ } else {
313
+ console.log(chalk.gray(`Unknown git subcommand: ${sub}. Try: commit, review, diff, log`));
314
+ }
315
+ } catch (err) {
316
+ console.log(chalk.red(`Git error: ${err.message}`));
317
+ }
318
+ }
319
+
320
+ async function handleTaskCommand(args) {
321
+ const parts = args.split(' ');
322
+ const sub = parts[0];
323
+ const rest = parts.slice(1).join(' ');
324
+ try {
325
+ if (sub === 'add') {
326
+ const task = TaskManager.create({ title: rest });
327
+ console.log(chalk.green(`✓ Task #${task.id} created: ${task.title}`));
328
+ } else if (sub === 'done') {
329
+ const task = TaskManager.update(parseInt(rest), { status: STATUS.DONE });
330
+ console.log(chalk.green(`✓ Task #${task.id} marked done`));
331
+ } else if (sub === 'list') {
332
+ const tasks = TaskManager.list();
333
+ tasks.forEach(t => console.log(` #${t.id} [${t.priority}] ${t.title} (${t.status})`));
334
+ } else {
335
+ console.log(chalk.gray('Usage: /task add <title> | /task done <id> | /task list'));
336
+ }
337
+ } catch (err) {
338
+ console.log(chalk.red(`Task error: ${err.message}`));
339
+ }
340
+ }
341
+
342
+ // ── STANDALONE COMMANDS ────────────────────────────────────────
343
+
188
344
  program
189
- .command('stats')
190
- .description('Show knowledge base statistics')
191
- .action(() => {
192
- const stats = store.getStats();
193
- const files = store.getIngestedFiles();
345
+ .command('debug <error>')
346
+ .description('Quick debug: explain an error in context of your codebase')
347
+ .option('-s, --stack <trace>', 'Stack trace')
348
+ .action(async (error, opts) => {
349
+ console.log(banner);
350
+ await askQuestion(`Why is this error happening?\nError: ${error}${opts.stack ? '\nStack:\n' + opts.stack : ''}`, { mode: QUERY_MODES.DEBUG });
351
+ });
194
352
 
353
+ program
354
+ .command('plan <task>')
355
+ .description('Generate a step-by-step execution plan before making changes')
356
+ .action(async (task) => {
195
357
  console.log(banner);
196
- console.log(chalk.bold('Knowledge Base Stats:'));
197
- console.log(` Total documents: ${chalk.green(stats.totalDocs)}`);
198
- console.log(` Total files: ${chalk.green(stats.totalFiles)}`);
199
- console.log(` Last ingested: ${chalk.gray(stats.lastIngested || 'Never')}`);
200
- console.log('\n' + chalk.bold('By type:'));
201
- Object.entries(stats.fileTypes || {}).forEach(([type, count]) => {
202
- console.log(` ${type.padEnd(15)} ${chalk.cyan(count)} docs`);
203
- });
358
+ const spinner = ora('Analysing codebase and generating plan...').start();
359
+ try {
360
+ const context = indexer.search(task, 6);
361
+ const plan = await plannerEngine.generatePlan(task, context);
362
+ spinner.stop();
363
+ console.log(chalk.bold('\n📋 Execution Plan:\n'));
364
+ console.log(plan.plan);
365
+ } catch (err) { spinner.fail(err.message); }
366
+ });
204
367
 
205
- if (files.length > 0) {
206
- console.log('\n' + chalk.bold(`Ingested files (${files.length}):`));
207
- files.slice(0, 20).forEach(f => console.log(` ${chalk.gray(f)}`));
208
- if (files.length > 20) {
209
- console.log(chalk.gray(` ... and ${files.length - 20} more`));
210
- }
368
+ program
369
+ .command('git <subcommand>')
370
+ .description('Git operations: status | diff | commit | review | log | branches')
371
+ .option('-f, --focus <areas>', 'Review focus areas')
372
+ .action(async (sub, opts) => {
373
+ console.log(banner);
374
+ if (sub === 'status') {
375
+ const status = await GitTool.status();
376
+ console.log(JSON.stringify(status, null, 2));
377
+ } else {
378
+ await handleGitCommand(sub);
379
+ }
380
+ });
381
+
382
+ program
383
+ .command('grep <pattern>')
384
+ .description('Search codebase with ripgrep (or native fallback)')
385
+ .option('-d, --dir <path>', 'Directory to search')
386
+ .option('-g, --glob <glob>', 'File glob filter')
387
+ .option('-i, --ignore-case', 'Case insensitive')
388
+ .option('-n, --max <n>', 'Max results', '50')
389
+ .action(async (pattern, opts) => {
390
+ console.log(banner);
391
+ const spinner = ora(`Searching: ${pattern}`).start();
392
+ try {
393
+ const result = await GrepTool.search(pattern, {
394
+ cwd: opts.dir || process.cwd(),
395
+ glob: opts.glob,
396
+ ignoreCase: opts.ignoreCase,
397
+ maxResults: parseInt(opts.max),
398
+ });
399
+ spinner.stop();
400
+ console.log(chalk.bold(`\n🔎 ${result.matches.length} of ${result.total} matches (${result.tool}):\n`));
401
+ result.matches.forEach(m =>
402
+ console.log(`${chalk.cyan(m.file)}:${chalk.yellow(String(m.lineNumber).padStart(4))} ${m.line.trim().slice(0, 100)}`)
403
+ );
404
+ } catch (err) { spinner.fail(err.message); }
405
+ });
406
+
407
+ program
408
+ .command('bash <command>')
409
+ .description('Execute a shell command with permission checks')
410
+ .option('-y, --yes', 'Auto-approve')
411
+ .action(async (command, opts) => {
412
+ const perm = BashTool.checkPermission(command);
413
+ if (perm === 'dangerous') { console.log(chalk.red(`⛔ Blocked: dangerous command`)); return; }
414
+ if (perm === 'needs-approval' && !opts.yes) {
415
+ console.log(chalk.yellow(`⚠ Requires approval. Re-run with --yes to execute.`));
416
+ return;
211
417
  }
418
+ const result = await BashTool.execute(command, { approved: true });
419
+ if (result.stdout) process.stdout.write(result.stdout);
420
+ if (result.stderr) process.stderr.write(chalk.yellow(result.stderr));
421
+ if (result.exitCode !== 0) process.exit(result.exitCode);
422
+ });
423
+
424
+ program
425
+ .command('tasks [subcommand]')
426
+ .description('Manage tasks: list | add <title> | done <id> | stats')
427
+ .action((sub = 'list', opts, cmd) => {
428
+ console.log(banner);
429
+ const args = cmd.args || [];
430
+ if (sub === 'list') {
431
+ const tasks = TaskManager.list({ includeDone: args.includes('--all') });
432
+ if (tasks.length === 0) { console.log(chalk.gray('No open tasks.')); return; }
433
+ console.log(chalk.bold('📋 Tasks:\n'));
434
+ tasks.forEach(t => {
435
+ const p = { critical: chalk.red, high: chalk.yellow, medium: chalk.white, low: chalk.gray }[t.priority] || chalk.white;
436
+ console.log(` ${p(`[${t.priority.padEnd(8)}]`)} #${String(t.id).padStart(3)} ${t.title}`);
437
+ if (t.description) console.log(chalk.gray(` ${t.description.slice(0, 60)}`));
438
+ });
439
+ const stats = TaskManager.getStats();
440
+ console.log(chalk.gray(`\n Total: ${stats.total} | Todo: ${stats.byStatus.todo || 0} | In Progress: ${stats.byStatus.in_progress || 0} | Done: ${stats.byStatus.done || 0}`));
441
+ } else if (sub === 'stats') {
442
+ console.log(JSON.stringify(TaskManager.getStats(), null, 2));
443
+ }
444
+ });
445
+
446
+ program
447
+ .command('memory [subcommand]')
448
+ .description('Manage memories: list | add <text> | clear | stats')
449
+ .action((sub = 'list', opts, cmd) => {
450
+ console.log(banner);
451
+ const rest = cmd.args?.slice(1).join(' ') || '';
452
+ if (sub === 'list') {
453
+ const mems = MemoryManager.list();
454
+ if (mems.length === 0) { console.log(chalk.gray('No memories yet.')); return; }
455
+ console.log(chalk.bold('🧠 Memories:\n'));
456
+ mems.forEach(m => {
457
+ console.log(` [${chalk.cyan(m.type)}] ${m.content.slice(0, 100)}`);
458
+ console.log(chalk.gray(` id:${m.id} | used:${m.useCount}x | ${m.createdAt.slice(0, 10)}`));
459
+ });
460
+ } else if (sub === 'add' && rest) {
461
+ const mem = MemoryManager.add(rest);
462
+ console.log(chalk.green(`✓ Memory added: ${mem.id}`));
463
+ } else if (sub === 'stats') {
464
+ console.log(JSON.stringify(MemoryManager.getStats(), null, 2));
465
+ } else if (sub === 'clear') {
466
+ MemoryManager.clear();
467
+ console.log(chalk.green('✓ All memories cleared'));
468
+ }
469
+ });
470
+
471
+ program
472
+ .command('sessions [subcommand]')
473
+ .description('Manage sessions: list | resume <id> | export <id>')
474
+ .action((sub = 'list', opts, cmd) => {
475
+ console.log(banner);
476
+ const id = cmd.args?.[1];
477
+ if (sub === 'list') {
478
+ const sessions = sessionMgr.list();
479
+ if (sessions.length === 0) { console.log(chalk.gray('No sessions.')); return; }
480
+ console.log(chalk.bold('💾 Sessions:\n'));
481
+ sessions.forEach(s => {
482
+ console.log(` ${chalk.cyan(s.id)}`);
483
+ console.log(` ${s.name} | ${s.messageCount} messages | ${s.updatedAt.slice(0, 16)}`);
484
+ });
485
+ } else if (sub === 'export' && id) {
486
+ const md = sessionMgr.exportMarkdown(id);
487
+ console.log(md);
488
+ }
489
+ });
490
+
491
+ program
492
+ .command('doctor')
493
+ .description('Check environment health')
494
+ .action(async () => {
495
+ console.log(banner);
496
+ const spinner = ora('Running diagnostics...').start();
497
+ const result = await plannerEngine.doctor();
498
+ spinner.stop();
499
+ console.log(chalk.bold('🩺 Doctor Report:\n'));
500
+ for (const c of result.checks) {
501
+ const icon = c.status === 'ok' ? chalk.green('✓') : c.status === 'warn' ? chalk.yellow('⚠') : chalk.red('✗');
502
+ console.log(` ${icon} ${c.name.padEnd(22)} ${chalk.gray(c.detail)}`);
503
+ }
504
+ const allOk = result.healthy;
505
+ console.log(`\n ${allOk ? chalk.green('✅ All systems healthy') : chalk.red('❌ Issues found — check above')}`);
506
+ });
507
+
508
+ program
509
+ .command('cost')
510
+ .description('Show token usage and estimated API cost')
511
+ .option('-s, --session <id>', 'Session ID')
512
+ .action((opts) => {
513
+ console.log(banner);
514
+ const summary = costTracker.getSummary(opts.session || 'default');
515
+ console.log(chalk.bold('💰 Cost Summary:\n'));
516
+ if (summary.session) {
517
+ console.log(chalk.bold(' Current Session:'));
518
+ console.log(` Calls: ${summary.session.calls}`);
519
+ console.log(` Tokens: ${summary.session.inputTokens + summary.session.outputTokens} (${summary.session.inputTokens}↑ ${summary.session.outputTokens}↓)`);
520
+ console.log(` Cost: ${chalk.yellow(costTracker.formatCost(summary.session.costUsd))}`);
521
+ }
522
+ console.log(chalk.bold('\n All-Time:'));
523
+ console.log(` Calls: ${summary.allTime.calls}`);
524
+ console.log(` Tokens: ${summary.allTime.inputTokens + summary.allTime.outputTokens}`);
525
+ console.log(` Cost: ${chalk.yellow(costTracker.formatCost(summary.allTime.costUsd))}`);
526
+ });
527
+
528
+ program
529
+ .command('stats')
530
+ .description('Show knowledge base statistics')
531
+ .action(() => {
532
+ console.log(banner);
533
+ const kbStats = store.getStats();
534
+ const memStats = MemoryManager.getStats();
535
+ const taskStats = TaskManager.getStats();
536
+ const costSummary = costTracker.getSummary();
537
+
538
+ console.log(chalk.bold('📊 System Overview:\n'));
539
+ console.log(chalk.bold(' Knowledge Base:'));
540
+ console.log(` Documents: ${chalk.green(kbStats.totalDocs)} from ${chalk.green(kbStats.totalFiles)} files`);
541
+ Object.entries(kbStats.fileTypes || {}).forEach(([type, count]) =>
542
+ console.log(` ${(' ' + type).padEnd(18)} ${chalk.cyan(count)} chunks`)
543
+ );
544
+ console.log(chalk.bold('\n Memory:'));
545
+ console.log(` Total: ${chalk.green(memStats.total)} memories`);
546
+ Object.entries(memStats.byType || {}).forEach(([type, count]) =>
547
+ console.log(` ${(' ' + type).padEnd(18)} ${chalk.cyan(count)}`)
548
+ );
549
+ console.log(chalk.bold('\n Tasks:'));
550
+ console.log(` Total: ${chalk.green(taskStats.total)} | Open: ${taskStats.byStatus?.todo || 0}`);
551
+ console.log(chalk.bold('\n Cost (all-time):'));
552
+ console.log(` ${costTracker.formatCost(costSummary.allTime.costUsd)} across ${costSummary.allTime.calls} calls`);
212
553
  });
213
554
 
214
555
  program
215
556
  .command('clear')
216
- .description('Clear the entire knowledge base')
557
+ .description('Clear the knowledge base')
217
558
  .option('-y, --yes', 'Skip confirmation')
218
559
  .action(async (opts) => {
219
560
  if (!opts.yes) {
220
561
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
221
- rl.question(chalk.red('⚠ This will delete all indexed data. Continue? (y/N) '), (answer) => {
562
+ rl.question(chalk.red('⚠ Delete all indexed data? (y/N) '), (a) => {
222
563
  rl.close();
223
- if (answer.toLowerCase() === 'y') {
224
- store.clear();
225
- indexer.invalidate();
226
- console.log(chalk.green('✓ Knowledge base cleared'));
227
- } else {
228
- console.log('Cancelled.');
229
- }
564
+ if (a.toLowerCase() === 'y') { store.clear(); indexer.invalidate(); console.log(chalk.green('✓ Cleared')); }
565
+ else console.log('Cancelled.');
230
566
  process.exit(0);
231
567
  });
232
568
  } else {
233
- store.clear();
234
- indexer.invalidate();
569
+ store.clear(); indexer.invalidate();
235
570
  console.log(chalk.green('✓ Knowledge base cleared'));
236
571
  }
237
572
  });
238
573
 
239
- program
240
- .command('debug <error>')
241
- .description('Quick debug: explain an error in context of your codebase')
242
- .option('-s, --stack <trace>', 'Stack trace')
243
- .action(async (error, opts) => {
244
- console.log(banner);
245
- await askQuestion(`Why is this error happening and how do I fix it?\nError: ${error}${opts.stack ? '\nStack:\n' + opts.stack : ''}`, { mode: QUERY_MODES.DEBUG });
246
- });
247
-
248
574
  program.parse(process.argv);