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.
- package/.env.example +23 -55
- package/README.md +609 -219
- package/cli.js +486 -160
- package/package.json +2 -2
- package/src/agents/BaseAgent.js +113 -0
- package/src/agents/dreamer.js +165 -0
- package/src/agents/improver.js +175 -0
- package/src/agents/specialists.js +202 -0
- package/src/agents/taskDecomposer.js +176 -0
- package/src/agents/teamCoordinator.js +153 -0
- package/src/api/routes/agents.js +172 -0
- package/src/api/routes/extras.js +115 -0
- package/src/api/routes/git.js +72 -0
- package/src/api/routes/ingest.js +60 -40
- package/src/api/routes/knowledge.js +59 -41
- package/src/api/routes/memory.js +41 -0
- package/src/api/routes/newRoutes.js +168 -0
- package/src/api/routes/pipelines.js +41 -0
- package/src/api/routes/planner.js +54 -0
- package/src/api/routes/query.js +24 -0
- package/src/api/routes/sessions.js +54 -0
- package/src/api/routes/tasks.js +67 -0
- package/src/api/routes/tools.js +85 -0
- package/src/api/routes/v5routes.js +196 -0
- package/src/api/server.js +133 -5
- package/src/context/compactor.js +151 -0
- package/src/context/contextEngineer.js +181 -0
- package/src/context/contextVisualizer.js +140 -0
- package/src/core/conversationEngine.js +231 -0
- package/src/core/indexer.js +169 -143
- package/src/core/ingester.js +141 -126
- package/src/core/queryEngine.js +286 -236
- package/src/cron/cronScheduler.js +260 -0
- package/src/dashboard/index.html +1181 -0
- package/src/lsp/symbolNavigator.js +220 -0
- package/src/memory/memoryManager.js +186 -0
- package/src/memory/teamMemory.js +111 -0
- package/src/messaging/messageBus.js +177 -0
- package/src/monitor/proactiveMonitor.js +337 -0
- package/src/pipelines/pipelineEngine.js +230 -0
- package/src/planner/plannerEngine.js +202 -0
- package/src/plugins/builtin/stats-plugin.js +29 -0
- package/src/plugins/pluginManager.js +144 -0
- package/src/prompts/promptEngineer.js +289 -0
- package/src/sessions/sessionManager.js +166 -0
- package/src/skills/skillsManager.js +263 -0
- package/src/storage/store.js +127 -105
- package/src/tasks/taskManager.js +151 -0
- package/src/tools/BashTool.js +154 -0
- package/src/tools/FileEditTool.js +280 -0
- package/src/tools/GitTool.js +212 -0
- package/src/tools/GrepTool.js +199 -0
- package/src/tools/registry.js +1380 -0
- package/src/utils/costTracker.js +69 -0
- package/src/utils/fileParser.js +176 -153
- package/src/utils/llmClient.js +355 -206
- package/src/watcher/fileWatcher.js +137 -0
- 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
|
-
.
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
72
|
-
.option('-k, --top-k <n>', '
|
|
73
|
-
.option('-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
180
|
+
if (trimmed.startsWith('/git ')) {
|
|
181
|
+
const sub = trimmed.slice(5);
|
|
182
|
+
await handleGitCommand(sub);
|
|
183
|
+
prompt(); return;
|
|
184
|
+
}
|
|
132
185
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
200
|
+
if (trimmed.startsWith('/task ')) {
|
|
201
|
+
await handleTaskCommand(trimmed.slice(6));
|
|
202
|
+
prompt(); return;
|
|
203
|
+
}
|
|
140
204
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 (
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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('
|
|
190
|
-
.description('
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
console.log(
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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('⚠
|
|
562
|
+
rl.question(chalk.red('⚠ Delete all indexed data? (y/N) '), (a) => {
|
|
222
563
|
rl.close();
|
|
223
|
-
if (
|
|
224
|
-
|
|
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);
|