daedalus-cli 0.5.9 → 0.5.11
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/CHANGELOG.md +5 -0
- package/README.md +3 -0
- package/dist/agents/orchestrator.js +3 -3
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/config/index.d.ts +6 -6
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/extraction.d.ts.map +1 -1
- package/dist/extraction.js +7 -2
- package/dist/extraction.js.map +1 -1
- package/dist/highlight.d.ts.map +1 -1
- package/dist/highlight.js +6 -3
- package/dist/highlight.js.map +1 -1
- package/dist/index.js +224 -137
- package/dist/index.js.map +1 -1
- package/dist/onboarding/wizard.js +11 -11
- package/dist/onboarding/wizard.js.map +1 -1
- package/dist/router/health.d.ts.map +1 -1
- package/dist/router/health.js +5 -1
- package/dist/router/health.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +28 -2
- package/dist/router/index.js.map +1 -1
- package/dist/tools/builtin/files.d.ts.map +1 -1
- package/dist/tools/builtin/files.js +3 -1
- package/dist/tools/builtin/files.js.map +1 -1
- package/dist/tools/builtin/indexing.js +3 -3
- package/dist/tools/builtin/indexing.js.map +1 -1
- package/dist/tools/builtin/terminal.d.ts.map +1 -1
- package/dist/tools/builtin/terminal.js +15 -7
- package/dist/tools/builtin/terminal.js.map +1 -1
- package/dist/tools/mcp/registry.d.ts.map +1 -1
- package/dist/tools/mcp/registry.js +3 -2
- package/dist/tools/mcp/registry.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -40,6 +40,13 @@ let turnStartTime = 0;
|
|
|
40
40
|
let currentAbortController = null;
|
|
41
41
|
// Compute stable projectHash once
|
|
42
42
|
const projectHash = crypto.createHash('sha256').update(path.resolve(process.cwd())).digest('hex').slice(0, 12);
|
|
43
|
+
// Max chars of tool output stored in message history (prevents context-window overflow)
|
|
44
|
+
const TOOL_RESULT_MAX_CHARS = 32_000;
|
|
45
|
+
// Single source of truth for the codebase index DB path — used by all REPL
|
|
46
|
+
// handlers AND the indexing tools so they always share the same database.
|
|
47
|
+
function getIndexDbPath() {
|
|
48
|
+
return path.join(os.homedir(), '.daedalus', 'indexing', `${projectHash}.sqlite`);
|
|
49
|
+
}
|
|
43
50
|
// Initialize session manager
|
|
44
51
|
const sessionManager = new SessionManager();
|
|
45
52
|
sessionManager.init();
|
|
@@ -144,11 +151,11 @@ A FTS5 symbol index is built automatically on startup. Use \`find_symbol\` to se
|
|
|
144
151
|
|
|
145
152
|
| Result | Meaning | What YOU must do |
|
|
146
153
|
|--------|---------|-----------------|
|
|
147
|
-
| \`Patched <file>\` |
|
|
148
|
-
|
|
|
149
|
-
| error contains \`not found\` |
|
|
150
|
-
| error contains \`multiple locations\` |
|
|
151
|
-
| error contains \`File not found\` |
|
|
154
|
+
| \`Patched <file>\` | [OK] Success — change written to disk | Continue to next step |
|
|
155
|
+
| \`PATCH_DECLINED\` | [SKIP] User reviewed the diff and said No or Skip | STOP retrying. Tell the user what you tried to change and ask how they'd like to proceed |
|
|
156
|
+
| error contains \`not found\` | [ERROR] old_string didn't match the file | Immediately call \`read_file\` on that file, find the exact text, then retry \`patch\` with the corrected old_string |
|
|
157
|
+
| error contains \`multiple locations\` | [ERROR] old_string is too generic | Add more surrounding lines to old_string to make it unique, then retry |
|
|
158
|
+
| error contains \`File not found\` | [ERROR] Wrong path | Use \`search_files\` or \`list_files\` to find the correct path |
|
|
152
159
|
|
|
153
160
|
**Never freeze or loop silently.** If a patch fails, take one corrective action and tell the user what happened.`;
|
|
154
161
|
// Build system prompt with project memory and user profile
|
|
@@ -219,7 +226,6 @@ function printBanner() {
|
|
|
219
226
|
const white = pc.white.bind(pc);
|
|
220
227
|
const bold = pc.bold.bind(pc);
|
|
221
228
|
const dim = pc.dim.bind(pc);
|
|
222
|
-
const mag = pc.magenta.bind(pc);
|
|
223
229
|
// Top border
|
|
224
230
|
console.log(hRule('╔', '╗', '═', W, cyan));
|
|
225
231
|
console.log(box(' '.repeat(W), cyan));
|
|
@@ -304,8 +310,8 @@ async function buildIndexContext(userMessage) {
|
|
|
304
310
|
if (!config.indexing.enabled || !toolContext.indexDb)
|
|
305
311
|
return '';
|
|
306
312
|
const indexDb = toolContext.indexDb;
|
|
307
|
-
// Extract likely symbol names from the user message (camelCase, snake_case, class names)
|
|
308
|
-
const symbolCandidates = userMessage.match(/\b[A-Z][a-zA-Z0-9_]*\b/g) || [];
|
|
313
|
+
// Extract likely symbol names from the user message (camelCase, PascalCase, snake_case, class names)
|
|
314
|
+
const symbolCandidates = userMessage.match(/\b(?:[a-z]+[A-Z]|[A-Z][a-z])[a-zA-Z0-9_]*\b/g) || [];
|
|
309
315
|
const words = userMessage.split(/\s+/).filter(w => w.length > 2);
|
|
310
316
|
const allTerms = [...symbolCandidates, ...words];
|
|
311
317
|
if (allTerms.length === 0)
|
|
@@ -363,10 +369,10 @@ function initializeSessionState(loaded) {
|
|
|
363
369
|
async function handleSpawn(role, task) {
|
|
364
370
|
const validRoles = ['coder', 'reviewer', 'debugger', 'researcher', 'planner'];
|
|
365
371
|
if (!validRoles.includes(role)) {
|
|
366
|
-
console.log(pc.red(
|
|
372
|
+
console.log(pc.red(`[WARN] Unknown role: ${role}. Valid: ${validRoles.join(', ')}`));
|
|
367
373
|
return;
|
|
368
374
|
}
|
|
369
|
-
console.log(pc.cyan(`\n
|
|
375
|
+
console.log(pc.cyan(`\n[SPAWN] Spawning ${role} agent for: ${task.slice(0, 80)}...`));
|
|
370
376
|
const context = `Active files: ${Array.from(activeFiles.values()).join(', ') || 'none'}`;
|
|
371
377
|
const fakeToolCall = {
|
|
372
378
|
id: `call_${Date.now()}`,
|
|
@@ -388,7 +394,7 @@ async function handleSpawn(role, task) {
|
|
|
388
394
|
}
|
|
389
395
|
// Handle /orchestrate command
|
|
390
396
|
async function handleOrchestrate(goal) {
|
|
391
|
-
console.log(pc.cyan(`\n
|
|
397
|
+
console.log(pc.cyan(`\n[ORCHESTRATE] Starting orchestration for: ${goal}`));
|
|
392
398
|
const { Orchestrator } = await import('./agents/orchestrator.js');
|
|
393
399
|
const orchestrator = new Orchestrator(router, messages, toolContext);
|
|
394
400
|
const result = await orchestrator.run(goal);
|
|
@@ -418,8 +424,6 @@ async function handleModels() {
|
|
|
418
424
|
}
|
|
419
425
|
// Handle /config command
|
|
420
426
|
function handleConfig() {
|
|
421
|
-
// Max chars of tool output stored in message history (prevents context-window overflow)
|
|
422
|
-
const TOOL_RESULT_MAX_CHARS = 32_000;
|
|
423
427
|
console.log(pc.bold('\n--- Current Configuration ---'));
|
|
424
428
|
console.log(JSON.stringify(config, null, 2));
|
|
425
429
|
console.log(pc.bold('-----------------------------'));
|
|
@@ -469,11 +473,10 @@ async function handleDoctor() {
|
|
|
469
473
|
console.log(pc.bold(' Config:') + pc.gray(` ${configDir}\\config.json`));
|
|
470
474
|
console.log(pc.bold('----------------------\n'));
|
|
471
475
|
} // end handleDoctor
|
|
472
|
-
// Handle /index command
|
|
473
476
|
async function handleIndex(opts) {
|
|
474
477
|
console.log(pc.bold('\n--- Indexing Codebase ---'));
|
|
475
478
|
console.log(pc.gray(`Project: ${process.cwd()}`));
|
|
476
|
-
const indexDbPath =
|
|
479
|
+
const indexDbPath = getIndexDbPath();
|
|
477
480
|
if (!fs.existsSync(path.dirname(indexDbPath))) {
|
|
478
481
|
fs.mkdirSync(path.dirname(indexDbPath), { recursive: true });
|
|
479
482
|
}
|
|
@@ -491,12 +494,13 @@ async function handleIndex(opts) {
|
|
|
491
494
|
return;
|
|
492
495
|
lastPct = pct;
|
|
493
496
|
const filled = Math.round((current / total) * barWidth);
|
|
494
|
-
const bar = '
|
|
497
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
495
498
|
process.stdout.write(`\r ${pc.cyan(bar)} ${pc.white(`${current}/${total}`)} ${pc.gray(file.slice(-40))}`);
|
|
496
499
|
};
|
|
497
500
|
const result = await indexCodebase(db, process.cwd(), projectHash, { ...opts, onProgress });
|
|
498
501
|
process.stdout.write('\n');
|
|
499
502
|
const elapsed = Date.now() - start;
|
|
503
|
+
toolContext.indexDb = db;
|
|
500
504
|
console.log(pc.green(`\n✔ Indexing complete in ${elapsed}ms`));
|
|
501
505
|
console.log(pc.white(` Total files: ${result.totalFiles}`));
|
|
502
506
|
console.log(pc.white(` Indexed files: ${result.indexedFiles}`));
|
|
@@ -510,14 +514,13 @@ async function handleIndex(opts) {
|
|
|
510
514
|
}
|
|
511
515
|
}
|
|
512
516
|
catch (err) {
|
|
513
|
-
console.error(pc.red(`\n
|
|
517
|
+
console.error(pc.red(`\n[ERROR] Indexing failed: ${err.message}`));
|
|
514
518
|
}
|
|
515
519
|
}
|
|
516
|
-
// Handle /find <query> [limit]
|
|
517
520
|
async function handleFindSymbol(query, limit) {
|
|
518
|
-
const indexDbPath =
|
|
521
|
+
const indexDbPath = getIndexDbPath();
|
|
519
522
|
if (!fs.existsSync(indexDbPath)) {
|
|
520
|
-
console.log(pc.yellow('
|
|
523
|
+
console.log(pc.yellow('[WARN] No index found. Run /index first.'));
|
|
521
524
|
return;
|
|
522
525
|
}
|
|
523
526
|
const { initIndexDb, searchSymbols } = await import('./indexing/fts.js');
|
|
@@ -538,11 +541,10 @@ async function handleFindSymbol(query, limit) {
|
|
|
538
541
|
}
|
|
539
542
|
}
|
|
540
543
|
}
|
|
541
|
-
// Handle /refs <symbol>
|
|
542
544
|
async function handleGetReferences(symbol) {
|
|
543
|
-
const indexDbPath =
|
|
545
|
+
const indexDbPath = getIndexDbPath();
|
|
544
546
|
if (!fs.existsSync(indexDbPath)) {
|
|
545
|
-
console.log(pc.yellow('
|
|
547
|
+
console.log(pc.yellow('[WARN] No index found. Run /index first.'));
|
|
546
548
|
return;
|
|
547
549
|
}
|
|
548
550
|
const { initIndexDb, findReferences } = await import('./indexing/fts.js');
|
|
@@ -553,7 +555,6 @@ async function handleGetReferences(symbol) {
|
|
|
553
555
|
console.log(pc.gray(' No references found.'));
|
|
554
556
|
return;
|
|
555
557
|
}
|
|
556
|
-
// Group by caller
|
|
557
558
|
const byCaller = new Map();
|
|
558
559
|
for (const r of refs) {
|
|
559
560
|
const key = `${r.caller_name} (${r.caller_file}:${r.caller_line})`;
|
|
@@ -572,11 +573,10 @@ async function handleGetReferences(symbol) {
|
|
|
572
573
|
}
|
|
573
574
|
}
|
|
574
575
|
}
|
|
575
|
-
// Handle /def <symbol>
|
|
576
576
|
async function handleGetDefinition(symbol) {
|
|
577
|
-
const indexDbPath =
|
|
577
|
+
const indexDbPath = getIndexDbPath();
|
|
578
578
|
if (!fs.existsSync(indexDbPath)) {
|
|
579
|
-
console.log(pc.yellow('
|
|
579
|
+
console.log(pc.yellow('[WARN] No index found. Run /index first.'));
|
|
580
580
|
return;
|
|
581
581
|
}
|
|
582
582
|
const { initIndexDb, findDefinitions } = await import('./indexing/fts.js');
|
|
@@ -597,8 +597,6 @@ async function handleGetDefinition(symbol) {
|
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
|
-
// Max chars of tool output stored in message history (prevents context-window overflow)
|
|
601
|
-
const TOOL_RESULT_MAX_CHARS = 32_000;
|
|
602
600
|
function truncateToolResult(content) {
|
|
603
601
|
if (content.length <= TOOL_RESULT_MAX_CHARS)
|
|
604
602
|
return content;
|
|
@@ -657,21 +655,20 @@ async function checkForUpdates() {
|
|
|
657
655
|
// Streaming response handler with tool call support — iterative, not recursive
|
|
658
656
|
const MAX_TOOL_TURNS = 40;
|
|
659
657
|
async function callModelWithTools(userContent, imageBase64) {
|
|
660
|
-
//
|
|
658
|
+
// Push the user message as-is — callers in chatLoop are responsible for
|
|
659
|
+
// pre-augmenting userContent with file context and index context.
|
|
661
660
|
if (userContent) {
|
|
662
|
-
const indexCtx = await buildIndexContext(userContent);
|
|
663
|
-
const augmentedContent = indexCtx ? indexCtx + userContent : userContent;
|
|
664
661
|
if (imageBase64) {
|
|
665
662
|
messages.push({
|
|
666
663
|
role: 'user',
|
|
667
664
|
content: [
|
|
668
|
-
{ type: 'text', text:
|
|
665
|
+
{ type: 'text', text: userContent },
|
|
669
666
|
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
|
670
667
|
],
|
|
671
668
|
});
|
|
672
669
|
}
|
|
673
670
|
else {
|
|
674
|
-
messages.push({ role: 'user', content:
|
|
671
|
+
messages.push({ role: 'user', content: userContent });
|
|
675
672
|
}
|
|
676
673
|
}
|
|
677
674
|
// Combine built-in tools with MCP tools (stable across turns)
|
|
@@ -746,7 +743,7 @@ async function callModelWithTools(userContent, imageBase64) {
|
|
|
746
743
|
if (signal.aborted) {
|
|
747
744
|
if (blockOpened)
|
|
748
745
|
closeAssistantBlock(fullContent.length, Date.now() - turnStart);
|
|
749
|
-
console.log(pc.dim('\n
|
|
746
|
+
console.log(pc.dim('\n [STOP] Stopped'));
|
|
750
747
|
currentAbortController = null;
|
|
751
748
|
return { content: fullContent, toolCalls: [] };
|
|
752
749
|
}
|
|
@@ -754,12 +751,12 @@ async function callModelWithTools(userContent, imageBase64) {
|
|
|
754
751
|
catch (error) {
|
|
755
752
|
if (signal.aborted) {
|
|
756
753
|
spinner.stop();
|
|
757
|
-
console.log(pc.dim('\n
|
|
754
|
+
console.log(pc.dim('\n [STOP] Stopped'));
|
|
758
755
|
currentAbortController = null;
|
|
759
756
|
return { content: '', toolCalls: [] };
|
|
760
757
|
}
|
|
761
758
|
spinner.stop();
|
|
762
|
-
console.error(pc.red(`\n
|
|
759
|
+
console.error(pc.red(`\n[ERROR] Error calling model: ${error.message}`));
|
|
763
760
|
throw error;
|
|
764
761
|
}
|
|
765
762
|
currentAbortController = null;
|
|
@@ -789,20 +786,20 @@ async function callModelWithTools(userContent, imageBase64) {
|
|
|
789
786
|
if (dangerousTools.includes(tc.function.name) && !turnApproved) {
|
|
790
787
|
const args = tc.function.arguments;
|
|
791
788
|
const preview = args.length > 120 ? args.slice(0, 120) + '...' : args;
|
|
792
|
-
process.stdout.write(`\n ${pc.yellow('
|
|
789
|
+
process.stdout.write(`\n ${pc.yellow('[WARN]')} ${pc.bold(tc.function.name)} ${pc.dim(preview)}\n`);
|
|
793
790
|
const line = await askLine(` ${pc.dim('Allow? [y]es / [n]o / [a]ll for this turn: ')}`);
|
|
794
791
|
const char = line.trim().toLowerCase().slice(0, 1);
|
|
795
792
|
if (char === 'a')
|
|
796
793
|
turnApproved = true;
|
|
797
794
|
if (char === 'n') {
|
|
798
|
-
console.log(` ${pc.red('
|
|
795
|
+
console.log(` ${pc.red('[FAIL]')} ${tc.function.name} ${pc.red(' — rejected')}`);
|
|
799
796
|
continue;
|
|
800
797
|
}
|
|
801
798
|
}
|
|
802
799
|
approvedCallIndices.add(i);
|
|
803
800
|
}
|
|
804
801
|
const approvedCalls = toolCallArray.filter((_, i) => approvedCallIndices.has(i));
|
|
805
|
-
console.log(`\n ${pc.dim('
|
|
802
|
+
console.log(`\n ${pc.dim('[TOOL]')} ${pc.dim(`Executing ${approvedCalls.length} tool call(s)...`)}`);
|
|
806
803
|
const results = await executeToolCalls(approvedCalls, toolContext);
|
|
807
804
|
for (const result of results) {
|
|
808
805
|
messages.push({
|
|
@@ -833,7 +830,7 @@ async function callModelWithTools(userContent, imageBase64) {
|
|
|
833
830
|
turn++;
|
|
834
831
|
}
|
|
835
832
|
// Reached max turns without a clean stop
|
|
836
|
-
console.log(`\n ${pc.yellow('
|
|
833
|
+
console.log(`\n ${pc.yellow('[WARN]')} ${pc.yellow(`Reached max tool turns (${MAX_TOOL_TURNS}). Stopping.`)}`);
|
|
837
834
|
messages.push({ role: 'assistant', content: lastContent });
|
|
838
835
|
return { content: lastContent, toolCalls: [] };
|
|
839
836
|
}
|
|
@@ -851,7 +848,7 @@ async function callModelWithFallback(userContent, imageBase64) {
|
|
|
851
848
|
else {
|
|
852
849
|
messages.push({ role: 'user', content: userContent });
|
|
853
850
|
}
|
|
854
|
-
console.log(pc.gray('
|
|
851
|
+
console.log(pc.gray('[THINK] Thinking (fallback mode)...'));
|
|
855
852
|
try {
|
|
856
853
|
const response = await router.chat.completions.create({
|
|
857
854
|
model: 'auto',
|
|
@@ -868,14 +865,38 @@ async function callModelWithFallback(userContent, imageBase64) {
|
|
|
868
865
|
return reply;
|
|
869
866
|
}
|
|
870
867
|
catch (error) {
|
|
871
|
-
console.error(pc.red(`\n
|
|
868
|
+
console.error(pc.red(`\n[ERROR] Fallback error: ${error.message}`));
|
|
872
869
|
throw error;
|
|
873
870
|
}
|
|
874
871
|
}
|
|
875
|
-
//
|
|
872
|
+
// Single-line prompt (approval gate, commit message)
|
|
876
873
|
function askLine(prompt) {
|
|
877
874
|
return new Promise((resolve) => rl.question(prompt, resolve));
|
|
878
875
|
}
|
|
876
|
+
// Multi-line input for main chat prompt — captures pasted text beyond the first newline.
|
|
877
|
+
// Uses a timing heuristic: lines arriving within 80ms are treated as part of a single paste.
|
|
878
|
+
function readMultiLineInput(prompt) {
|
|
879
|
+
return new Promise((resolve) => {
|
|
880
|
+
const lines = [];
|
|
881
|
+
let timer = null;
|
|
882
|
+
let resolved = false;
|
|
883
|
+
const onLine = (line) => {
|
|
884
|
+
if (resolved)
|
|
885
|
+
return;
|
|
886
|
+
lines.push(line);
|
|
887
|
+
if (timer)
|
|
888
|
+
clearTimeout(timer);
|
|
889
|
+
timer = setTimeout(() => {
|
|
890
|
+
resolved = true;
|
|
891
|
+
rl.off('line', onLine);
|
|
892
|
+
resolve(lines.join('\n'));
|
|
893
|
+
}, 80);
|
|
894
|
+
};
|
|
895
|
+
rl.on('line', onLine);
|
|
896
|
+
process.stdout.write(prompt);
|
|
897
|
+
rl.resume();
|
|
898
|
+
});
|
|
899
|
+
}
|
|
879
900
|
// ── Clipboard helpers ──────────────────────────────────────────────────────────
|
|
880
901
|
function getClipboardText() {
|
|
881
902
|
try {
|
|
@@ -1016,7 +1037,7 @@ async function chatLoop() {
|
|
|
1016
1037
|
const prompt = activeFiles.size > 0
|
|
1017
1038
|
? `\n${pc.cyan(' ⬡')} ${pc.dim(`[${activeFiles.size} file${activeFiles.size > 1 ? 's' : ''}]`)} ${pc.bold(pc.white('›'))} `
|
|
1018
1039
|
: `\n${pc.cyan(' ⬡')} ${pc.bold(pc.white('›'))} `;
|
|
1019
|
-
const input = await
|
|
1040
|
+
const input = await readMultiLineInput(prompt);
|
|
1020
1041
|
const trimmedInput = input.trim();
|
|
1021
1042
|
if (!trimmedInput)
|
|
1022
1043
|
continue;
|
|
@@ -1025,10 +1046,10 @@ async function chatLoop() {
|
|
|
1025
1046
|
if (lowerInput === 'exit' || lowerInput === 'quit') {
|
|
1026
1047
|
const todos = getSessionTodos(sessionId);
|
|
1027
1048
|
sessionManager.saveSessionState(messages, activeFiles, todos);
|
|
1028
|
-
console.log(pc.dim('
|
|
1049
|
+
console.log(pc.dim(' [EXTRACT] Extracting facts from session...'));
|
|
1029
1050
|
await extractAndSave(router, sessionManager, messages);
|
|
1030
1051
|
console.log(pc.gray(`Session saved: ${sessionManager.sessionId}`));
|
|
1031
|
-
console.log(pc.yellow('\nEnding session. Goodbye
|
|
1052
|
+
console.log(pc.yellow('\nEnding session. Goodbye!\n'));
|
|
1032
1053
|
rl.close();
|
|
1033
1054
|
process.exit(0);
|
|
1034
1055
|
}
|
|
@@ -1060,13 +1081,13 @@ async function chatLoop() {
|
|
|
1060
1081
|
if (trimmedInput.startsWith('/add ')) {
|
|
1061
1082
|
const fileArg = trimmedInput.substring(5).trim();
|
|
1062
1083
|
if (!fileArg) {
|
|
1063
|
-
console.log(pc.red('
|
|
1084
|
+
console.log(pc.red('[WARN] Please specify a file path. Example: /add src/App.tsx'));
|
|
1064
1085
|
}
|
|
1065
1086
|
else {
|
|
1066
1087
|
const absPath = path.resolve(fileArg);
|
|
1067
1088
|
activeFiles.set(absPath, fileArg);
|
|
1068
1089
|
toolContext.activeFiles = new Map(activeFiles);
|
|
1069
|
-
console.log(pc.green(
|
|
1090
|
+
console.log(pc.green(`[OK] Added file to context: ${pc.bold(fileArg)}`));
|
|
1070
1091
|
}
|
|
1071
1092
|
continue;
|
|
1072
1093
|
}
|
|
@@ -1074,16 +1095,16 @@ async function chatLoop() {
|
|
|
1074
1095
|
if (trimmedInput.startsWith('/remove ')) {
|
|
1075
1096
|
const fileArg = trimmedInput.substring(8).trim();
|
|
1076
1097
|
if (!fileArg) {
|
|
1077
|
-
console.log(pc.red('
|
|
1098
|
+
console.log(pc.red('[WARN] Please specify a file path. Example: /remove src/App.tsx'));
|
|
1078
1099
|
}
|
|
1079
1100
|
else {
|
|
1080
1101
|
const absPath = path.resolve(fileArg);
|
|
1081
1102
|
if (activeFiles.delete(absPath)) {
|
|
1082
1103
|
toolContext.activeFiles = new Map(activeFiles);
|
|
1083
|
-
console.log(pc.green(
|
|
1104
|
+
console.log(pc.green(`[OK] Removed file from context: ${pc.bold(fileArg)}`));
|
|
1084
1105
|
}
|
|
1085
1106
|
else {
|
|
1086
|
-
console.log(pc.yellow(
|
|
1107
|
+
console.log(pc.yellow(`[WARN] File was not in context: ${fileArg}`));
|
|
1087
1108
|
}
|
|
1088
1109
|
}
|
|
1089
1110
|
continue;
|
|
@@ -1091,6 +1112,41 @@ async function chatLoop() {
|
|
|
1091
1112
|
// Command: /paste — paste clipboard content (text or image)
|
|
1092
1113
|
if (lowerInput === '/paste' || lowerInput.startsWith('/paste ')) {
|
|
1093
1114
|
const extra = trimmedInput.startsWith('/paste ') ? trimmedInput.substring(7).trim() : '';
|
|
1115
|
+
// If extra looks like a file path, try reading it as an image
|
|
1116
|
+
if (extra && !extra.startsWith('http')) {
|
|
1117
|
+
const filePath = path.resolve(extra);
|
|
1118
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
1119
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1120
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext)) {
|
|
1121
|
+
const imgBuffer = fs.readFileSync(filePath);
|
|
1122
|
+
const base64 = imgBuffer.toString('base64');
|
|
1123
|
+
const message = 'What do you see in this image?';
|
|
1124
|
+
printUserTurn(`${path.basename(filePath)} (image)`);
|
|
1125
|
+
try {
|
|
1126
|
+
const filesContext = buildFileContext();
|
|
1127
|
+
const indexCtx = await buildIndexContext(message);
|
|
1128
|
+
const userContent = `${indexCtx}${filesContext}User Prompt: ${message}`;
|
|
1129
|
+
await callModelWithTools(userContent, base64);
|
|
1130
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
console.error(pc.red(`\n[ERROR] Error: ${error.message}`));
|
|
1134
|
+
try {
|
|
1135
|
+
const filesContext = buildFileContext();
|
|
1136
|
+
const userContent = `${filesContext}User Prompt: ${message}`;
|
|
1137
|
+
console.log(pc.yellow('\n[RETRY] Trying fallback mode...'));
|
|
1138
|
+
await callModelWithFallback(userContent, base64);
|
|
1139
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1140
|
+
}
|
|
1141
|
+
catch (fallbackErr) {
|
|
1142
|
+
console.error(pc.red(`\n[ERROR] Fallback also failed: ${fallbackErr.message}`));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
turnSeparator();
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1094
1150
|
// Try image first
|
|
1095
1151
|
const imgPath = getClipboardImage();
|
|
1096
1152
|
if (imgPath) {
|
|
@@ -1104,17 +1160,19 @@ async function chatLoop() {
|
|
|
1104
1160
|
const indexCtx = await buildIndexContext(message);
|
|
1105
1161
|
const userContent = `${indexCtx}${filesContext}User Prompt: ${message}`;
|
|
1106
1162
|
await callModelWithTools(userContent, base64);
|
|
1163
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1107
1164
|
}
|
|
1108
1165
|
catch (error) {
|
|
1109
|
-
console.error(pc.red(`\n
|
|
1166
|
+
console.error(pc.red(`\n[ERROR] Error: ${error.message}`));
|
|
1110
1167
|
try {
|
|
1111
1168
|
const filesContext = buildFileContext();
|
|
1112
1169
|
const userContent = `${filesContext}User Prompt: ${message}`;
|
|
1113
|
-
console.log(pc.yellow('\n
|
|
1170
|
+
console.log(pc.yellow('\n[RETRY] Trying fallback mode...'));
|
|
1114
1171
|
await callModelWithFallback(userContent, base64);
|
|
1172
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1115
1173
|
}
|
|
1116
1174
|
catch (fallbackErr) {
|
|
1117
|
-
console.error(pc.red(`\n
|
|
1175
|
+
console.error(pc.red(`\n[ERROR] Fallback also failed: ${fallbackErr.message}`));
|
|
1118
1176
|
}
|
|
1119
1177
|
}
|
|
1120
1178
|
turnSeparator();
|
|
@@ -1123,7 +1181,7 @@ async function chatLoop() {
|
|
|
1123
1181
|
// Fall back to text
|
|
1124
1182
|
const clipboard = getClipboardText();
|
|
1125
1183
|
if (!clipboard) {
|
|
1126
|
-
console.log(pc.red('
|
|
1184
|
+
console.log(pc.red('[WARN] Clipboard is empty or inaccessible.'));
|
|
1127
1185
|
continue;
|
|
1128
1186
|
}
|
|
1129
1187
|
const fullMessage = extra ? `${clipboard}\n\n${extra}` : clipboard;
|
|
@@ -1133,9 +1191,10 @@ async function chatLoop() {
|
|
|
1133
1191
|
const indexCtx = await buildIndexContext(fullMessage);
|
|
1134
1192
|
const userContent = `${indexCtx}${filesContext}User Prompt: ${fullMessage}`;
|
|
1135
1193
|
await callModelWithTools(userContent);
|
|
1194
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1136
1195
|
}
|
|
1137
1196
|
catch (error) {
|
|
1138
|
-
console.error(pc.red(`\n
|
|
1197
|
+
console.error(pc.red(`\n[ERROR] Error: ${error.message}`));
|
|
1139
1198
|
}
|
|
1140
1199
|
turnSeparator();
|
|
1141
1200
|
continue;
|
|
@@ -1176,7 +1235,7 @@ async function chatLoop() {
|
|
|
1176
1235
|
else if (subCmd === 'load') {
|
|
1177
1236
|
const targetId = parts[1]?.trim();
|
|
1178
1237
|
if (!targetId) {
|
|
1179
|
-
console.log(pc.red('
|
|
1238
|
+
console.log(pc.red('[WARN] Usage: /session load <id>'));
|
|
1180
1239
|
}
|
|
1181
1240
|
else {
|
|
1182
1241
|
try {
|
|
@@ -1184,10 +1243,10 @@ async function chatLoop() {
|
|
|
1184
1243
|
sessionManager.saveSessionState(messages, activeFiles, todos);
|
|
1185
1244
|
const loaded = sessionManager.startSession(targetId);
|
|
1186
1245
|
initializeSessionState(loaded);
|
|
1187
|
-
console.log(pc.green(
|
|
1246
|
+
console.log(pc.green(`[OK] Loaded session: ${sessionManager.sessionTitle} (${sessionManager.sessionId})`));
|
|
1188
1247
|
}
|
|
1189
1248
|
catch (err) {
|
|
1190
|
-
console.log(pc.red(
|
|
1249
|
+
console.log(pc.red(`[WARN] Failed to load session: ${err.message}`));
|
|
1191
1250
|
}
|
|
1192
1251
|
}
|
|
1193
1252
|
}
|
|
@@ -1197,20 +1256,20 @@ async function chatLoop() {
|
|
|
1197
1256
|
sessionManager.saveSessionState(messages, activeFiles, todos);
|
|
1198
1257
|
const loaded = sessionManager.startSession(undefined, title || undefined);
|
|
1199
1258
|
initializeSessionState(loaded);
|
|
1200
|
-
console.log(pc.green(
|
|
1259
|
+
console.log(pc.green(`[OK] Started new session: ${sessionManager.sessionTitle} (${sessionManager.sessionId})`));
|
|
1201
1260
|
}
|
|
1202
1261
|
else if (subCmd === 'delete') {
|
|
1203
1262
|
const targetId = parts[1]?.trim();
|
|
1204
1263
|
if (!targetId) {
|
|
1205
|
-
console.log(pc.red('
|
|
1264
|
+
console.log(pc.red('[WARN] Usage: /session delete <id>'));
|
|
1206
1265
|
}
|
|
1207
1266
|
else {
|
|
1208
1267
|
sessionManager.deleteSession(targetId);
|
|
1209
|
-
console.log(pc.green(
|
|
1268
|
+
console.log(pc.green(`[OK] Deleted session: ${targetId}`));
|
|
1210
1269
|
}
|
|
1211
1270
|
}
|
|
1212
1271
|
else {
|
|
1213
|
-
console.log(pc.red('
|
|
1272
|
+
console.log(pc.red('[WARN] Usage: /session list | load <id> | new [title] | delete <id>'));
|
|
1214
1273
|
}
|
|
1215
1274
|
continue;
|
|
1216
1275
|
}
|
|
@@ -1244,13 +1303,13 @@ async function chatLoop() {
|
|
|
1244
1303
|
const argStr = trimmedInput.substring(6).trim();
|
|
1245
1304
|
const eqIdx = argStr.indexOf('=');
|
|
1246
1305
|
if (eqIdx < 0) {
|
|
1247
|
-
console.log(pc.red('
|
|
1306
|
+
console.log(pc.red('[WARN] Usage: /fact <key> = <value>'));
|
|
1248
1307
|
}
|
|
1249
1308
|
else {
|
|
1250
1309
|
const key = argStr.slice(0, eqIdx).trim();
|
|
1251
1310
|
const value = argStr.slice(eqIdx + 1).trim();
|
|
1252
1311
|
sessionManager.addFact(key, value, 'user');
|
|
1253
|
-
console.log(pc.green(
|
|
1312
|
+
console.log(pc.green(`[OK] Saved fact: ${key} = ${value}`));
|
|
1254
1313
|
}
|
|
1255
1314
|
continue;
|
|
1256
1315
|
}
|
|
@@ -1259,19 +1318,19 @@ async function chatLoop() {
|
|
|
1259
1318
|
const argStr = trimmedInput.substring(12).trim();
|
|
1260
1319
|
const eqIdx = argStr.indexOf('=');
|
|
1261
1320
|
if (eqIdx < 0) {
|
|
1262
|
-
console.log(pc.red('
|
|
1321
|
+
console.log(pc.red('[WARN] Usage: /convention <key> = <value>'));
|
|
1263
1322
|
}
|
|
1264
1323
|
else {
|
|
1265
1324
|
const key = argStr.slice(0, eqIdx).trim();
|
|
1266
1325
|
const value = argStr.slice(eqIdx + 1).trim();
|
|
1267
1326
|
sessionManager.setConvention(key, value);
|
|
1268
|
-
console.log(pc.green(
|
|
1327
|
+
console.log(pc.green(`[OK] Saved convention: ${key} = ${value}`));
|
|
1269
1328
|
}
|
|
1270
1329
|
continue;
|
|
1271
1330
|
}
|
|
1272
1331
|
// Command: /extract — manually trigger fact extraction
|
|
1273
1332
|
if (lowerInput === '/extract') {
|
|
1274
|
-
console.log(pc.dim('
|
|
1333
|
+
console.log(pc.dim(' [EXTRACT] Extracting facts from conversation...'));
|
|
1275
1334
|
await extractAndSave(router, sessionManager, messages);
|
|
1276
1335
|
continue;
|
|
1277
1336
|
}
|
|
@@ -1292,16 +1351,16 @@ async function chatLoop() {
|
|
|
1292
1351
|
if (rest.startsWith('name ')) {
|
|
1293
1352
|
userProfile.name = rest.substring(5).trim();
|
|
1294
1353
|
saveProfile(userProfile);
|
|
1295
|
-
console.log(pc.green(
|
|
1354
|
+
console.log(pc.green(`[OK] Profile name set: ${userProfile.name}`));
|
|
1296
1355
|
continue;
|
|
1297
1356
|
}
|
|
1298
1357
|
if (rest.startsWith('bio ')) {
|
|
1299
1358
|
userProfile.bio = rest.substring(4).trim();
|
|
1300
1359
|
saveProfile(userProfile);
|
|
1301
|
-
console.log(pc.green(
|
|
1360
|
+
console.log(pc.green('[OK] Profile bio set.'));
|
|
1302
1361
|
continue;
|
|
1303
1362
|
}
|
|
1304
|
-
console.log(pc.red('
|
|
1363
|
+
console.log(pc.red('[WARN] Usage: /profile view | /profile name = <name> | /profile bio = <bio>'));
|
|
1305
1364
|
continue;
|
|
1306
1365
|
}
|
|
1307
1366
|
// Command: /style
|
|
@@ -1316,14 +1375,14 @@ async function chatLoop() {
|
|
|
1316
1375
|
}
|
|
1317
1376
|
userProfile.style = rest;
|
|
1318
1377
|
saveProfile(userProfile);
|
|
1319
|
-
console.log(pc.green('
|
|
1378
|
+
console.log(pc.green('[OK] Coding style saved. It will be injected into every session.'));
|
|
1320
1379
|
continue;
|
|
1321
1380
|
}
|
|
1322
1381
|
// Command: /clear
|
|
1323
1382
|
if (lowerInput === '/clear') {
|
|
1324
1383
|
messages.length = 0;
|
|
1325
1384
|
messages.push({ role: 'system', content: getSystemPromptWithMemory() });
|
|
1326
|
-
console.log(pc.green('
|
|
1385
|
+
console.log(pc.green('[OK] Conversation history cleared!'));
|
|
1327
1386
|
continue;
|
|
1328
1387
|
}
|
|
1329
1388
|
// Command: /tools
|
|
@@ -1346,7 +1405,7 @@ async function chatLoop() {
|
|
|
1346
1405
|
if (lowerInput.startsWith('/spawn ')) {
|
|
1347
1406
|
const parts = trimmedInput.substring(7).trim().split(' ');
|
|
1348
1407
|
if (parts.length < 2) {
|
|
1349
|
-
console.log(pc.red('
|
|
1408
|
+
console.log(pc.red('[WARN] Usage: /spawn <role> <task>'));
|
|
1350
1409
|
console.log(pc.gray(' Roles: coder, reviewer, debugger, researcher, planner'));
|
|
1351
1410
|
}
|
|
1352
1411
|
else {
|
|
@@ -1360,7 +1419,7 @@ async function chatLoop() {
|
|
|
1360
1419
|
if (lowerInput.startsWith('/delegate ')) {
|
|
1361
1420
|
const match = trimmedInput.substring(10).match(/^(.+)\s+to\s+(\w+)$/i);
|
|
1362
1421
|
if (!match) {
|
|
1363
|
-
console.log(pc.red('
|
|
1422
|
+
console.log(pc.red('[WARN] Usage: /delegate <task> to <role>'));
|
|
1364
1423
|
}
|
|
1365
1424
|
else {
|
|
1366
1425
|
const task = match[1].trim();
|
|
@@ -1373,7 +1432,7 @@ async function chatLoop() {
|
|
|
1373
1432
|
if (lowerInput.startsWith('/orchestrate ')) {
|
|
1374
1433
|
const goal = trimmedInput.substring(13).trim();
|
|
1375
1434
|
if (!goal) {
|
|
1376
|
-
console.log(pc.red('
|
|
1435
|
+
console.log(pc.red('[WARN] Usage: /orchestrate <goal>'));
|
|
1377
1436
|
}
|
|
1378
1437
|
else {
|
|
1379
1438
|
await handleOrchestrate(goal);
|
|
@@ -1397,7 +1456,7 @@ async function chatLoop() {
|
|
|
1397
1456
|
}
|
|
1398
1457
|
// Command: /onboard
|
|
1399
1458
|
if (lowerInput === '/onboard') {
|
|
1400
|
-
console.log(pc.cyan('\n
|
|
1459
|
+
console.log(pc.cyan('\n[RESTART] Re-running onboarding wizard...\n'));
|
|
1401
1460
|
await runOnboarding(true);
|
|
1402
1461
|
continue;
|
|
1403
1462
|
}
|
|
@@ -1405,7 +1464,7 @@ async function chatLoop() {
|
|
|
1405
1464
|
if (lowerInput === '/undo') {
|
|
1406
1465
|
const history = toolContext.patchHistory;
|
|
1407
1466
|
if (!history || history.length === 0) {
|
|
1408
|
-
console.log(pc.yellow('
|
|
1467
|
+
console.log(pc.yellow('[WARN] No patches to undo.'));
|
|
1409
1468
|
}
|
|
1410
1469
|
else {
|
|
1411
1470
|
const last = history[history.length - 1];
|
|
@@ -1413,22 +1472,22 @@ async function chatLoop() {
|
|
|
1413
1472
|
const currentContent = fs.readFileSync(last.filePath, 'utf8');
|
|
1414
1473
|
if (currentContent === last.newContent) {
|
|
1415
1474
|
fs.writeFileSync(last.filePath, last.oldContent, 'utf8');
|
|
1416
|
-
console.log(pc.green(
|
|
1475
|
+
console.log(pc.green(`[OK] Undid patch to ${last.filePath} (${last.description})`));
|
|
1417
1476
|
}
|
|
1418
1477
|
else {
|
|
1419
|
-
console.log(pc.yellow(
|
|
1478
|
+
console.log(pc.yellow(`[WARN] File ${last.filePath} has been modified since last patch. Cannot auto-undo.`));
|
|
1420
1479
|
}
|
|
1421
1480
|
history.pop();
|
|
1422
1481
|
}
|
|
1423
1482
|
catch (err) {
|
|
1424
|
-
console.log(pc.red(
|
|
1483
|
+
console.log(pc.red(`[WARN] Failed to undo: ${err.message}`));
|
|
1425
1484
|
}
|
|
1426
1485
|
}
|
|
1427
1486
|
continue;
|
|
1428
1487
|
}
|
|
1429
1488
|
// Command: /commit — stage and commit changes
|
|
1430
1489
|
if (lowerInput === '/commit' || lowerInput.startsWith('/commit ')) {
|
|
1431
|
-
const
|
|
1490
|
+
const forcedMsg = trimmedInput.startsWith('/commit ') ? trimmedInput.substring(8).trim() : '';
|
|
1432
1491
|
try {
|
|
1433
1492
|
const { execute: termExec } = await import('./tools/builtin/terminal.js');
|
|
1434
1493
|
const statusResult = await termExec({ command: 'git status --short', timeout: 10, workdir: process.cwd() }, toolContext);
|
|
@@ -1443,28 +1502,65 @@ async function chatLoop() {
|
|
|
1443
1502
|
console.log(pc.red(`Stage failed: ${addResult.error}`));
|
|
1444
1503
|
continue;
|
|
1445
1504
|
}
|
|
1446
|
-
let commitMsg =
|
|
1505
|
+
let commitMsg = forcedMsg;
|
|
1447
1506
|
if (!commitMsg) {
|
|
1448
1507
|
const diffResult = await termExec({ command: 'git diff --cached --stat', timeout: 10, workdir: process.cwd() }, toolContext);
|
|
1508
|
+
const diffFull = await termExec({ command: 'git diff --cached', timeout: 10, workdir: process.cwd() }, toolContext);
|
|
1509
|
+
const diffContent = diffFull.content?.slice(0, 6000) || '';
|
|
1449
1510
|
if (diffResult.content)
|
|
1450
1511
|
console.log(pc.gray(diffResult.content));
|
|
1451
|
-
|
|
1512
|
+
if (diffContent) {
|
|
1513
|
+
console.log(pc.dim(' Generating commit message...'));
|
|
1514
|
+
try {
|
|
1515
|
+
const aiResponse = await router.chat.completions.create({
|
|
1516
|
+
model: 'auto',
|
|
1517
|
+
messages: [
|
|
1518
|
+
{ role: 'system', content: 'You write concise git commit messages following the Conventional Commits spec (type(scope): description). Output only the commit message — no explanation, no quotes, no extra text.' },
|
|
1519
|
+
{ role: 'user', content: `Write a commit message for this diff:\n\n${diffContent}` }
|
|
1520
|
+
],
|
|
1521
|
+
temperature: 0.2,
|
|
1522
|
+
max_tokens: 80,
|
|
1523
|
+
});
|
|
1524
|
+
const suggested = (aiResponse.choices[0]?.message?.content || '').trim().split('\n')[0].trim();
|
|
1525
|
+
if (suggested) {
|
|
1526
|
+
console.log(`\n ${pc.dim('Suggested:')} ${pc.cyan(suggested)}`);
|
|
1527
|
+
const choice = await askLine(pc.dim(' [Enter] accept [e] edit [n] cancel: '));
|
|
1528
|
+
if (choice.trim().toLowerCase() === 'n') {
|
|
1529
|
+
console.log(pc.yellow('Commit cancelled.'));
|
|
1530
|
+
await termExec({ command: 'git restore --staged .', timeout: 10, workdir: process.cwd() }, toolContext);
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
else if (choice.trim().toLowerCase() === 'e') {
|
|
1534
|
+
commitMsg = await askLine(pc.cyan(' Commit message: '));
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
commitMsg = suggested;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch {
|
|
1542
|
+
// Model unavailable — fall back to manual
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (!commitMsg) {
|
|
1546
|
+
commitMsg = await askLine(pc.cyan(' Commit message: '));
|
|
1547
|
+
}
|
|
1452
1548
|
if (!commitMsg.trim()) {
|
|
1453
1549
|
console.log(pc.yellow('Commit cancelled — empty message.'));
|
|
1454
|
-
await termExec({ command: 'git
|
|
1550
|
+
await termExec({ command: 'git restore --staged .', timeout: 10, workdir: process.cwd() }, toolContext);
|
|
1455
1551
|
continue;
|
|
1456
1552
|
}
|
|
1457
1553
|
}
|
|
1458
1554
|
const commitResult = await termExec({ command: `git commit -m ${JSON.stringify(commitMsg)}`, timeout: 10, workdir: process.cwd() }, toolContext);
|
|
1459
1555
|
if (commitResult.success) {
|
|
1460
|
-
console.log(pc.green(`\n
|
|
1556
|
+
console.log(pc.green(`\n[OK] Commit: ${commitMsg.slice(0, 60)}`));
|
|
1461
1557
|
}
|
|
1462
1558
|
else {
|
|
1463
1559
|
console.log(pc.red(`Commit failed: ${commitResult.error}`));
|
|
1464
1560
|
}
|
|
1465
1561
|
}
|
|
1466
1562
|
catch (err) {
|
|
1467
|
-
console.log(pc.red(
|
|
1563
|
+
console.log(pc.red(`[WARN] Commit error: ${err.message}`));
|
|
1468
1564
|
}
|
|
1469
1565
|
continue;
|
|
1470
1566
|
}
|
|
@@ -1492,14 +1588,14 @@ async function chatLoop() {
|
|
|
1492
1588
|
value = parts.slice(1).join(' ');
|
|
1493
1589
|
}
|
|
1494
1590
|
if (!key || !value) {
|
|
1495
|
-
console.log(pc.red('
|
|
1591
|
+
console.log(pc.red('[WARN] Usage: /project set <key> = <value>'));
|
|
1496
1592
|
}
|
|
1497
1593
|
else {
|
|
1498
1594
|
const { loadProjectConfig, saveProjectConfig } = await import('./tools/builtin/project-config.js');
|
|
1499
1595
|
const cfg = loadProjectConfig(process.cwd());
|
|
1500
1596
|
cfg[key] = value;
|
|
1501
1597
|
saveProjectConfig(cfg);
|
|
1502
|
-
console.log(pc.green(
|
|
1598
|
+
console.log(pc.green(`[OK] Set ${key} = ${value}`));
|
|
1503
1599
|
}
|
|
1504
1600
|
continue;
|
|
1505
1601
|
}
|
|
@@ -1510,24 +1606,23 @@ async function chatLoop() {
|
|
|
1510
1606
|
const { execute: termExec } = await import('./tools/builtin/terminal.js');
|
|
1511
1607
|
const cfg = loadProjectConfig(process.cwd());
|
|
1512
1608
|
const testCmd = cfg.testCommand || 'npm test';
|
|
1513
|
-
console.log(pc.bold(`\
|
|
1609
|
+
console.log(pc.bold(`\nTest-Run-Fix Loop (max ${maxLoops} iterations)`));
|
|
1514
1610
|
console.log(pc.gray(`Test command: ${testCmd}\n`));
|
|
1515
1611
|
for (let i = 0; i < maxLoops; i++) {
|
|
1516
|
-
console.log(pc.cyan(`\n
|
|
1612
|
+
console.log(pc.cyan(`\n--- Run ${i + 1}/${maxLoops} ---`));
|
|
1517
1613
|
const result = await termExec({ command: testCmd, timeout: 120, workdir: process.cwd() }, toolContext);
|
|
1518
1614
|
console.log(result.content?.slice(0, 2000) || pc.gray('(no output)'));
|
|
1519
1615
|
if (result.success) {
|
|
1520
|
-
console.log(pc.green('\n
|
|
1616
|
+
console.log(pc.green('\n[OK] All tests passed!'));
|
|
1521
1617
|
break;
|
|
1522
1618
|
}
|
|
1523
1619
|
if (i === maxLoops - 1) {
|
|
1524
|
-
console.log(pc.yellow(`\n
|
|
1620
|
+
console.log(pc.yellow(`\n[WARN] Max loops (${maxLoops}) reached. Tests still failing.`));
|
|
1525
1621
|
break;
|
|
1526
1622
|
}
|
|
1527
|
-
const failureCtx = `Tests failed (run ${i + 1}/${maxLoops}).
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
await callModelWithTools(userContent);
|
|
1623
|
+
const failureCtx = `Tests failed (run ${i + 1}/${maxLoops}). Output:\n\n${result.content?.slice(0, 8000) || 'Unknown failure'}\n\nAnalyze the failures and fix the code. Do not re-read files you already have in context.`;
|
|
1624
|
+
await callModelWithTools(`User Prompt: ${failureCtx}`);
|
|
1625
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1531
1626
|
}
|
|
1532
1627
|
continue;
|
|
1533
1628
|
}
|
|
@@ -1550,13 +1645,13 @@ async function chatLoop() {
|
|
|
1550
1645
|
if (lowerInput.startsWith('/find ')) {
|
|
1551
1646
|
const parts = trimmedInput.substring(6).trim().split(/\s+/);
|
|
1552
1647
|
if (parts.length === 0) {
|
|
1553
|
-
console.log(pc.red('
|
|
1648
|
+
console.log(pc.red('[WARN] Usage: /find <query> [limit]'));
|
|
1554
1649
|
}
|
|
1555
1650
|
else {
|
|
1556
1651
|
const query = parts[0];
|
|
1557
1652
|
const limit = parts[1] ? parseInt(parts[1], 10) : 30;
|
|
1558
1653
|
if (isNaN(limit)) {
|
|
1559
|
-
console.log(pc.red('
|
|
1654
|
+
console.log(pc.red('[WARN] Invalid limit'));
|
|
1560
1655
|
}
|
|
1561
1656
|
else {
|
|
1562
1657
|
await handleFindSymbol(query, limit);
|
|
@@ -1568,7 +1663,7 @@ async function chatLoop() {
|
|
|
1568
1663
|
if (lowerInput.startsWith('/refs ')) {
|
|
1569
1664
|
const symbol = trimmedInput.substring(6).trim();
|
|
1570
1665
|
if (!symbol) {
|
|
1571
|
-
console.log(pc.red('
|
|
1666
|
+
console.log(pc.red('[WARN] Usage: /refs <symbol>'));
|
|
1572
1667
|
}
|
|
1573
1668
|
else {
|
|
1574
1669
|
await handleGetReferences(symbol);
|
|
@@ -1579,7 +1674,7 @@ async function chatLoop() {
|
|
|
1579
1674
|
if (lowerInput.startsWith('/def ')) {
|
|
1580
1675
|
const symbol = trimmedInput.substring(5).trim();
|
|
1581
1676
|
if (!symbol) {
|
|
1582
|
-
console.log(pc.red('
|
|
1677
|
+
console.log(pc.red('[WARN] Usage: /def <symbol>'));
|
|
1583
1678
|
}
|
|
1584
1679
|
else {
|
|
1585
1680
|
await handleGetDefinition(symbol);
|
|
@@ -1593,22 +1688,26 @@ async function chatLoop() {
|
|
|
1593
1688
|
const userContent = `${indexCtx}${filesContext}User Prompt: ${trimmedInput}`;
|
|
1594
1689
|
printUserTurn(trimmedInput);
|
|
1595
1690
|
await callModelWithTools(userContent);
|
|
1691
|
+
// Persist session incrementally after every turn
|
|
1692
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1596
1693
|
// Fact extraction — learns from tool results and reasoning
|
|
1597
1694
|
await extractAndSave(router, sessionManager, messages);
|
|
1598
1695
|
}
|
|
1599
1696
|
catch (error) {
|
|
1600
|
-
console.error(pc.red(`\n
|
|
1697
|
+
console.error(pc.red(`\n[ERROR] Error: ${error.message}`));
|
|
1601
1698
|
try {
|
|
1602
1699
|
const filesContext = buildFileContext();
|
|
1603
1700
|
const userContent = `${filesContext}User Prompt: ${trimmedInput}`;
|
|
1604
|
-
console.log(pc.yellow('\n
|
|
1701
|
+
console.log(pc.yellow('\n[RETRY] Trying fallback mode...'));
|
|
1605
1702
|
const fallbackResult = await callModelWithFallback(userContent);
|
|
1606
1703
|
if (fallbackResult) {
|
|
1704
|
+
// Persist session after fallback too
|
|
1705
|
+
sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
|
|
1607
1706
|
await extractAndSave(router, sessionManager, messages);
|
|
1608
1707
|
}
|
|
1609
1708
|
}
|
|
1610
1709
|
catch (fallbackErr) {
|
|
1611
|
-
console.error(pc.red(`\n
|
|
1710
|
+
console.error(pc.red(`\n[ERROR] Fallback also failed: ${fallbackErr.message}`));
|
|
1612
1711
|
console.error(pc.gray('Check that at least one local server is running.'));
|
|
1613
1712
|
}
|
|
1614
1713
|
}
|
|
@@ -1617,7 +1716,6 @@ async function chatLoop() {
|
|
|
1617
1716
|
}
|
|
1618
1717
|
// Start health checks and REPL
|
|
1619
1718
|
async function main() {
|
|
1620
|
-
// SIGINT handler — cancel generation if streaming, otherwise exit
|
|
1621
1719
|
process.on('SIGINT', () => {
|
|
1622
1720
|
if (currentAbortController) {
|
|
1623
1721
|
currentAbortController.abort();
|
|
@@ -1638,10 +1736,10 @@ async function main() {
|
|
|
1638
1736
|
printConfigInfo();
|
|
1639
1737
|
try {
|
|
1640
1738
|
await router.startHealthChecks();
|
|
1641
|
-
console.log(pc.green('\n
|
|
1739
|
+
console.log(pc.green('\n[OK] Router started. Health checks running every 30s.'));
|
|
1642
1740
|
}
|
|
1643
1741
|
catch (err) {
|
|
1644
|
-
console.error(pc.yellow(`\n
|
|
1742
|
+
console.error(pc.yellow(`\n[WARN] Router health checks failed: ${err.message}`));
|
|
1645
1743
|
}
|
|
1646
1744
|
// Initialize MCP registry
|
|
1647
1745
|
try {
|
|
@@ -1649,52 +1747,41 @@ async function main() {
|
|
|
1649
1747
|
const servers = mcpRegistry.getConnectedServers();
|
|
1650
1748
|
if (servers.length > 0) {
|
|
1651
1749
|
const mcpToolCount = mcpRegistry.getToolDefinitions().length;
|
|
1652
|
-
console.log(pc.green(`\n
|
|
1750
|
+
console.log(pc.green(`\n[OK] MCP connected: ${servers.join(', ')}`));
|
|
1653
1751
|
console.log(pc.dim(` ${mcpToolCount} MCP tool(s) registered — I'll ask before using them on your behalf.`));
|
|
1654
1752
|
}
|
|
1655
1753
|
}
|
|
1656
1754
|
catch (err) {
|
|
1657
|
-
console.error(pc.yellow(`\n
|
|
1755
|
+
console.error(pc.yellow(`\n[WARN] MCP initialization failed: ${err.message}`));
|
|
1658
1756
|
}
|
|
1659
1757
|
// Check for updates — non-blocking
|
|
1660
1758
|
if (config.updateCheck !== false) {
|
|
1661
1759
|
setTimeout(() => checkForUpdates(), 2000);
|
|
1662
1760
|
}
|
|
1663
|
-
// Auto-index at startup — deferred so the REPL appears immediately
|
|
1664
1761
|
if (config.indexing.enabled) {
|
|
1665
1762
|
setTimeout(() => {
|
|
1666
1763
|
(async () => {
|
|
1667
1764
|
try {
|
|
1668
|
-
const indexDbPath =
|
|
1669
|
-
if (!fs.existsSync(indexDbPath)) {
|
|
1670
|
-
|
|
1671
|
-
const { initIndexDb } = await import('./indexing/fts.js');
|
|
1672
|
-
const { indexCodebase } = await import('./indexing/indexer.js');
|
|
1673
|
-
const db = initIndexDb(indexDbPath);
|
|
1674
|
-
const result = await indexCodebase(db, process.cwd(), projectHash, {
|
|
1675
|
-
exclude: config.indexing.exclude,
|
|
1676
|
-
});
|
|
1677
|
-
console.log(pc.green(` ✔ Indexed ${result.indexedFiles} files (${result.skippedFiles} unchanged)`));
|
|
1678
|
-
if (result.errors.length > 0) {
|
|
1679
|
-
console.log(pc.yellow(` ⚠ ${result.errors.length} file(s) had errors`));
|
|
1680
|
-
}
|
|
1681
|
-
toolContext.indexDb = db;
|
|
1765
|
+
const indexDbPath = getIndexDbPath();
|
|
1766
|
+
if (!fs.existsSync(path.dirname(indexDbPath))) {
|
|
1767
|
+
fs.mkdirSync(path.dirname(indexDbPath), { recursive: true });
|
|
1682
1768
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1769
|
+
const { initIndexDb } = await import('./indexing/fts.js');
|
|
1770
|
+
const { indexCodebase } = await import('./indexing/indexer.js');
|
|
1771
|
+
const db = initIndexDb(indexDbPath);
|
|
1772
|
+
const result = await indexCodebase(db, process.cwd(), projectHash, {
|
|
1773
|
+
exclude: config.indexing.exclude,
|
|
1774
|
+
});
|
|
1775
|
+
if (result.indexedFiles > 0) {
|
|
1776
|
+
console.log(pc.cyan(` [INDEX] Indexed ${result.indexedFiles} file(s) (${result.skippedFiles} unchanged)`));
|
|
1777
|
+
}
|
|
1778
|
+
if (result.errors.length > 0) {
|
|
1779
|
+
console.log(pc.yellow(` [WARN] ${result.errors.length} file(s) had index errors`));
|
|
1694
1780
|
}
|
|
1781
|
+
toolContext.indexDb = db;
|
|
1695
1782
|
}
|
|
1696
1783
|
catch (err) {
|
|
1697
|
-
console.error(pc.yellow(`
|
|
1784
|
+
console.error(pc.yellow(` [WARN] Auto-index failed: ${err.message}`));
|
|
1698
1785
|
}
|
|
1699
1786
|
})();
|
|
1700
1787
|
}, 100);
|