daedalus-cli 1.0.0 → 1.1.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 (114) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +90 -215
  3. package/dist/agents/ensemble.js +1 -1
  4. package/dist/agents/ensemble.js.map +1 -1
  5. package/dist/agents/orchestrator.js +4 -4
  6. package/dist/agents/orchestrator.js.map +1 -1
  7. package/dist/banner.d.ts +3 -0
  8. package/dist/banner.d.ts.map +1 -0
  9. package/dist/banner.js +66 -0
  10. package/dist/banner.js.map +1 -0
  11. package/dist/clipboard.d.ts +3 -0
  12. package/dist/clipboard.d.ts.map +1 -0
  13. package/dist/clipboard.js +63 -0
  14. package/dist/clipboard.js.map +1 -0
  15. package/dist/commands.d.ts +44 -0
  16. package/dist/commands.d.ts.map +1 -0
  17. package/dist/commands.js +1044 -0
  18. package/dist/commands.js.map +1 -0
  19. package/dist/config/index.d.ts +47 -42
  20. package/dist/config/index.d.ts.map +1 -1
  21. package/dist/config/index.js +1 -0
  22. package/dist/config/index.js.map +1 -1
  23. package/dist/extraction.js +2 -2
  24. package/dist/extraction.js.map +1 -1
  25. package/dist/formatting.d.ts +13 -0
  26. package/dist/formatting.d.ts.map +1 -0
  27. package/dist/formatting.js +195 -0
  28. package/dist/formatting.js.map +1 -0
  29. package/dist/index.d.ts +1 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +59 -1953
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.test.js +1 -1
  34. package/dist/index.test.js.map +1 -1
  35. package/dist/indexing/indexer.d.ts +7 -0
  36. package/dist/indexing/indexer.d.ts.map +1 -1
  37. package/dist/indexing/indexer.js +2 -2
  38. package/dist/indexing/indexer.js.map +1 -1
  39. package/dist/indexing/watcher.d.ts +10 -0
  40. package/dist/indexing/watcher.d.ts.map +1 -0
  41. package/dist/indexing/watcher.js +143 -0
  42. package/dist/indexing/watcher.js.map +1 -0
  43. package/dist/indexing/watcher.test.d.ts +2 -0
  44. package/dist/indexing/watcher.test.d.ts.map +1 -0
  45. package/dist/indexing/watcher.test.js +81 -0
  46. package/dist/indexing/watcher.test.js.map +1 -0
  47. package/dist/model.d.ts +20 -0
  48. package/dist/model.d.ts.map +1 -0
  49. package/dist/model.js +278 -0
  50. package/dist/model.js.map +1 -0
  51. package/dist/onboarding/wizard.js +1 -1
  52. package/dist/onboarding/wizard.js.map +1 -1
  53. package/dist/project-hash.d.ts +2 -0
  54. package/dist/project-hash.d.ts.map +1 -0
  55. package/dist/project-hash.js +6 -0
  56. package/dist/project-hash.js.map +1 -0
  57. package/dist/repl.d.ts +25 -0
  58. package/dist/repl.d.ts.map +1 -0
  59. package/dist/repl.js +203 -0
  60. package/dist/repl.js.map +1 -0
  61. package/dist/router/index.d.ts +1 -1
  62. package/dist/router/index.d.ts.map +1 -1
  63. package/dist/router/index.js +17 -6
  64. package/dist/router/index.js.map +1 -1
  65. package/dist/session/manager.d.ts.map +1 -1
  66. package/dist/session/manager.js +6 -5
  67. package/dist/session/manager.js.map +1 -1
  68. package/dist/tools/builtin/files.d.ts.map +1 -1
  69. package/dist/tools/builtin/files.js +1 -3
  70. package/dist/tools/builtin/files.js.map +1 -1
  71. package/dist/tools/builtin/indexing.d.ts +0 -1
  72. package/dist/tools/builtin/indexing.d.ts.map +1 -1
  73. package/dist/tools/builtin/indexing.js +1 -5
  74. package/dist/tools/builtin/indexing.js.map +1 -1
  75. package/dist/tools/builtin/indexing.test.js +2 -1
  76. package/dist/tools/builtin/indexing.test.js.map +1 -1
  77. package/dist/tools/builtin/process-watcher.d.ts.map +1 -1
  78. package/dist/tools/builtin/process-watcher.js +11 -9
  79. package/dist/tools/builtin/process-watcher.js.map +1 -1
  80. package/dist/tools/builtin/project-config.js +3 -3
  81. package/dist/tools/builtin/project-config.js.map +1 -1
  82. package/dist/tools/builtin/screenshot.d.ts.map +1 -1
  83. package/dist/tools/builtin/screenshot.js +18 -12
  84. package/dist/tools/builtin/screenshot.js.map +1 -1
  85. package/dist/tools/builtin/terminal.d.ts +9 -0
  86. package/dist/tools/builtin/terminal.d.ts.map +1 -1
  87. package/dist/tools/builtin/terminal.js +74 -16
  88. package/dist/tools/builtin/terminal.js.map +1 -1
  89. package/dist/tools/builtin/terminal.test.d.ts +2 -0
  90. package/dist/tools/builtin/terminal.test.d.ts.map +1 -0
  91. package/dist/tools/builtin/terminal.test.js +161 -0
  92. package/dist/tools/builtin/terminal.test.js.map +1 -0
  93. package/dist/tools/builtin/todo.d.ts.map +1 -1
  94. package/dist/tools/builtin/todo.js +7 -5
  95. package/dist/tools/builtin/todo.js.map +1 -1
  96. package/dist/tools/mcp/http.test.d.ts +2 -0
  97. package/dist/tools/mcp/http.test.d.ts.map +1 -0
  98. package/dist/tools/mcp/http.test.js +128 -0
  99. package/dist/tools/mcp/http.test.js.map +1 -0
  100. package/dist/tools/mcp/registry.d.ts.map +1 -1
  101. package/dist/tools/mcp/registry.js +1 -6
  102. package/dist/tools/mcp/registry.js.map +1 -1
  103. package/dist/tools/mcp/stdio.d.ts.map +1 -1
  104. package/dist/tools/mcp/stdio.js +4 -1
  105. package/dist/tools/mcp/stdio.js.map +1 -1
  106. package/dist/tools/mcp/stdio.test.d.ts +2 -0
  107. package/dist/tools/mcp/stdio.test.d.ts.map +1 -0
  108. package/dist/tools/mcp/stdio.test.js +121 -0
  109. package/dist/tools/mcp/stdio.test.js.map +1 -0
  110. package/dist/update-check.d.ts +2 -0
  111. package/dist/update-check.d.ts.map +1 -0
  112. package/dist/update-check.js +48 -0
  113. package/dist/update-check.js.map +1 -0
  114. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,25 +3,21 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { createRequire } from 'module';
6
- import { execSync } from 'child_process';
7
6
  import readline from 'readline';
8
7
  import os from 'os';
9
8
  import pc from 'picocolors';
10
- import { BUILTIN_TOOLS, POWER_TOOLS } from './tools/definitions.js';
11
- import { executeToolCalls } from './tools/executor.js';
12
9
  import { setRouterClient } from './tools/builtin/delegation.js';
13
10
  import { mcpRegistry } from './tools/mcp/registry.js';
14
11
  import { createRouter } from './router/index.js';
15
- import { loadConfig, getConfigDirPath, discoverLocalServers } from './config/index.js';
16
- import crypto from 'crypto';
17
- import { getSessionTodos, setSessionTodos } from './tools/builtin/todo.js';
18
- import { searchSymbols as ftsSearch } from './indexing/fts.js';
12
+ import { loadConfig, getConfigDirPath } from './config/index.js';
13
+ import { getProjectHash } from './project-hash.js';
14
+ import { setSessionTodos } from './tools/builtin/todo.js';
19
15
  import { SessionManager } from './session/manager.js';
20
- import { runOnboarding } from './onboarding/wizard.js';
21
- import { DaedalusSpinner } from './tools/daedalus-spinner.js';
22
- import { loadProfile, saveProfile, getProfilePrompt } from './profile.js';
23
- import { extractAndSave } from './extraction.js';
24
- import { calculateSessionTokens, pruneMessages } from './session/tokens.js';
16
+ import { loadProfile, getProfilePrompt } from './profile.js';
17
+ import { printBanner, printConfigInfo } from './banner.js';
18
+ import { checkForUpdates } from './update-check.js';
19
+ import { createModelFunctions, currentAbortController } from './model.js';
20
+ import { createRepl } from './repl.js';
25
21
  const __filename = fileURLToPath(import.meta.url);
26
22
  const __dirname = path.dirname(__filename);
27
23
  const _require = createRequire(import.meta.url);
@@ -34,20 +30,9 @@ const userProfile = loadProfile();
34
30
  // Session state
35
31
  const activeFiles = new Map(); // Absolute path -> filename key
36
32
  const messages = [];
37
- // Token tracking for current turn
38
- let currentTurnInputTokens = 0;
39
- let currentTurnOutputTokens = 0;
40
- let turnStartTime = 0;
41
- let currentAbortController = null;
33
+ let indexWatcher = null;
42
34
  // Compute stable projectHash once
43
- const projectHash = crypto.createHash('sha256').update(path.resolve(process.cwd())).digest('hex').slice(0, 12);
44
- // Max chars of tool output stored in message history (prevents context-window overflow)
45
- const TOOL_RESULT_MAX_CHARS = 32_000;
46
- // Single source of truth for the codebase index DB path — used by all REPL
47
- // handlers AND the indexing tools so they always share the same database.
48
- function getIndexDbPath() {
49
- return path.join(os.homedir(), '.daedalus', 'indexing', `${projectHash}.sqlite`);
50
- }
35
+ const projectHash = getProjectHash(process.cwd());
51
36
  // Initialize session manager
52
37
  const sessionManager = new SessionManager();
53
38
  sessionManager.init();
@@ -69,7 +54,7 @@ if (initialSession.todos.length > 0) {
69
54
  setSessionTodos(sessionId, initialSession.todos);
70
55
  }
71
56
  // Ensure CLI temp directory exists
72
- const cliTempDir = path.join(os.homedir(), '.daedalus', 'temp');
57
+ const cliTempDir = path.join(os.tmpdir(), 'daedalus');
73
58
  if (!fs.existsSync(cliTempDir)) {
74
59
  fs.mkdirSync(cliTempDir, { recursive: true });
75
60
  }
@@ -82,7 +67,7 @@ const toolContext = {
82
67
  projectHash,
83
68
  activeFiles,
84
69
  agentRole: config.agents.default,
85
- abortSignal: new AbortController().signal,
70
+ get abortSignal() { return currentAbortController?.signal ?? new AbortController().signal; },
86
71
  autoApplyEdits: 'prompt',
87
72
  patchHistory: [],
88
73
  pauseSpinner: () => { },
@@ -180,111 +165,6 @@ function getSystemPromptWithMemory() {
180
165
  return prompt;
181
166
  }
182
167
  messages.push({ role: 'system', content: getSystemPromptWithMemory() });
183
- // Setup Readline Interface with tab completion
184
- const COMMANDS = [
185
- '/add', '/remove', '/context', '/clear',
186
- '/spawn', '/delegate', '/orchestrate',
187
- '/memory', '/fact', '/convention',
188
- '/extract',
189
- '/profile', '/style',
190
- '/index', '/find', '/refs', '/def',
191
- '/commit', '/undo', '/test', '/paste',
192
- '/models', '/tools', '/config', '/project', '/doctor', '/session', '/onboard',
193
- '/help', 'exit', 'quit', '?', '/prune', '/branch', '/pr', '/debug', '/ensemble',
194
- ];
195
- const rl = readline.createInterface({
196
- input: process.stdin,
197
- output: process.stdout,
198
- completer: (line) => {
199
- const prefix = line.toLowerCase();
200
- const hits = prefix.startsWith('/') || prefix.startsWith('?') || prefix.startsWith('exit') || prefix.startsWith('quit')
201
- ? COMMANDS.filter(c => c.startsWith(prefix))
202
- : [];
203
- return [hits.length ? hits : COMMANDS, prefix];
204
- },
205
- });
206
- // ─────────────────────────────────────────────────────────────────────────────
207
- // B A N N E R
208
- // ─────────────────────────────────────────────────────────────────────────────
209
- // Each row of the block-letter art, rendered as segments so we can colour
210
- // the left half cyan and the right half white for a pseudo-gradient effect.
211
- const LOGO_ROWS = [
212
- ' ██████╗ █████╗ ███████╗ ██████╗ █████╗ ██╗ ██╗ ██╗ ███████╗ ',
213
- ' ██╔══██╗ ██╔══██╗ ██╔════╝ ██╔══██╗ ██╔══██╗ ██║ ██║ ██║ ██╔════╝ ',
214
- ' ██║ ██║ ███████║ █████╗ ██║ ██║ ███████║ ██║ ██║ ██║ ███████╗ ',
215
- ' ██║ ██║ ██╔══██║ ██╔══╝ ██║ ██║ ██╔══██║ ██║ ██║ ██║ ╚════██║ ',
216
- ' ██████╔╝ ██║ ██║ ███████╗ ██████╔╝ ██║ ██║ ███████╗╚██████╔╝ ███████║ ',
217
- ' ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝ ',
218
- ];
219
- const W = 76; // inner width (between the border pipes)
220
- function box(inner, borderColor) {
221
- return borderColor('║') + inner + borderColor('║');
222
- }
223
- function hRule(l, r, fill, len, color) {
224
- return color(l + fill.repeat(len) + r);
225
- }
226
- function centred(text, width, color) {
227
- const pad = Math.max(0, width - text.length);
228
- const left = Math.floor(pad / 2);
229
- const right = pad - left;
230
- return ' '.repeat(left) + color(text) + ' '.repeat(right);
231
- }
232
- function printBanner() {
233
- const cyan = pc.cyan.bind(pc);
234
- const white = pc.white.bind(pc);
235
- const bold = pc.bold.bind(pc);
236
- const dim = pc.dim.bind(pc);
237
- // Top border
238
- console.log(hRule('╔', '╗', '═', W, cyan));
239
- console.log(box(' '.repeat(W), cyan));
240
- // Logo rows — left half cyan, right half white, fading across
241
- LOGO_ROWS.forEach((row, i) => {
242
- const mid = Math.floor(row.length * (0.4 + i * 0.04)); // gradient cut shifts right each row
243
- const left = cyan(row.slice(0, mid));
244
- const right = white(row.slice(mid));
245
- const inner = left + right;
246
- // Pad to exactly W chars
247
- const visLen = row.length;
248
- const padded = inner + ' '.repeat(Math.max(0, W - visLen));
249
- console.log(box(padded, cyan));
250
- });
251
- console.log(box(' '.repeat(W), cyan));
252
- // Tagline strip
253
- const tagline = '⬡ local-first · embedded router · multi-agent · not sentient ⬡';
254
- console.log(box(centred(tagline, W, dim), cyan));
255
- // Bottom border
256
- console.log(hRule('╚', '╝', '═', W, cyan));
257
- // Version / author badge — pill style
258
- const badge = ` v${APP_VERSION}`;
259
- const author = `bgill55_dev `;
260
- const divider = ` · `;
261
- console.log('');
262
- console.log(' ' +
263
- pc.bgCyan(pc.black(bold(` DAEDALUS `))) +
264
- pc.bgBlack(pc.cyan(bold(badge))) +
265
- pc.bgBlack(dim(divider)) +
266
- pc.bgBlack(pc.white(author)));
267
- console.log('');
268
- }
269
- // ─────────────────────────────────────────────────────────────────────────────
270
- // S T A R T U P I N F O
271
- // ─────────────────────────────────────────────────────────────────────────────
272
- function printConfigInfo() {
273
- const enabledCount = config.router.chain.filter(m => m.enabled).length;
274
- const strategy = config.router.strategy;
275
- const configPath = configDir + '/config.json';
276
- const contentLine = `strategy ${strategy} models ${enabledCount} config ${configPath}`;
277
- const top = pc.dim(' ┌─ ') + pc.cyan('router') + pc.dim(' ' + '─'.repeat(Math.max(0, contentLine.length - 7)) + '┐');
278
- const mid = pc.dim(' │ ') + pc.white(contentLine) + pc.dim(' │');
279
- const bot = pc.dim(' └' + '─'.repeat(contentLine.length + 2) + '┘');
280
- console.log(top);
281
- console.log(mid);
282
- console.log(bot);
283
- console.log('');
284
- // ── Quick tip ────────────────────────────────────────────────────────────
285
- console.log(` ${pc.dim('Type')} ${pc.cyan('?')} ${pc.dim('for commands · Tab completes · Be nice to your AI')}`);
286
- console.log('');
287
- }
288
168
  // Parse initial arguments (e.g. if started as `daedalus src/index.ts`)
289
169
  const initialArgs = process.argv.slice(2);
290
170
  if (initialArgs.length > 0) {
@@ -313,1832 +193,47 @@ function buildFileContext() {
313
193
  ctx += '----------------------------\n\n';
314
194
  return ctx;
315
195
  }
316
- // Build index contextauto-inject relevant symbols from FTS5 index
317
- async function buildIndexContext(userMessage) {
318
- if (!config.indexing.enabled || !toolContext.indexDb)
319
- return '';
320
- const indexDb = toolContext.indexDb;
321
- // Extract likely symbol names from the user message (camelCase, PascalCase, snake_case, class names)
322
- const symbolCandidates = userMessage.match(/\b(?:[a-z]+[A-Z]|[A-Z][a-z])[a-zA-Z0-9_]*\b/g) || [];
323
- const words = userMessage.split(/\s+/).filter(w => w.length > 2);
324
- const allTerms = [...symbolCandidates, ...words];
325
- if (allTerms.length === 0)
326
- return '';
327
- let ctx = '\n--- RELEVANT CODE SYMBOLS (from FTS5 index) ---\n';
328
- let count = 0;
329
- const seen = new Set();
330
- try {
331
- for (const term of allTerms) {
332
- if (count >= 8)
333
- break;
334
- const results = ftsSearch(indexDb, term, projectHash, 3);
335
- for (const s of results) {
336
- const key = `${s.name}:${s.file_path}`;
337
- if (seen.has(key))
338
- continue;
339
- seen.add(key);
340
- ctx += ` [${s.kind}] ${s.name} → ${s.file_path}:${s.line_start}`;
341
- if (s.signature)
342
- ctx += ` (${s.signature.slice(0, 80)})`;
343
- ctx += '\n';
344
- count++;
345
- }
346
- }
347
- }
348
- catch {
349
- // Index not available — skip
350
- }
351
- if (count === 0)
352
- return '';
353
- ctx += '------------------------------------------\n\n';
354
- return ctx;
355
- }
356
- // Initialize session state from a loaded session snapshot
357
- function initializeSessionState(loaded) {
358
- sessionId = loaded.sessionId;
359
- toolContext.sessionId = loaded.sessionId;
360
- activeFiles.clear();
361
- for (const [k, v] of loaded.activeFiles.entries()) {
362
- activeFiles.set(k, v);
363
- }
364
- toolContext.activeFiles = new Map(activeFiles);
365
- messages.length = 0;
366
- const sysPrompt = getSystemPromptWithMemory();
367
- messages.push({ role: 'system', content: sysPrompt });
368
- if (loaded.turns.length > 0) {
369
- const userOrAssistantTurns = loaded.turns.filter(t => t.role !== 'system');
370
- messages.push(...userOrAssistantTurns);
371
- }
372
- setSessionTodos(loaded.sessionId, loaded.todos);
373
- console.log(pc.gray(`Active files in context: ${activeFiles.size}`));
374
- console.log(pc.gray(`Loaded ${loaded.turns.length} message turn(s)`));
375
- }
376
- // Handle /spawn and /delegate commands
377
- async function handleSpawn(role, task) {
378
- const validRoles = ['coder', 'reviewer', 'debugger', 'researcher', 'planner'];
379
- if (!validRoles.includes(role)) {
380
- console.log(pc.red(`[WARN] Unknown role: ${role}. Valid: ${validRoles.join(', ')}`));
381
- return;
382
- }
383
- console.log(pc.cyan(`\n[SPAWN] Spawning ${role} agent for: ${task.slice(0, 80)}...`));
384
- const context = `Active files: ${Array.from(activeFiles.values()).join(', ') || 'none'}`;
385
- const fakeToolCall = {
386
- id: `call_${Date.now()}`,
387
- type: 'function',
388
- function: {
389
- name: 'delegate_task',
390
- arguments: JSON.stringify({ goal: task, context, role }),
391
- },
392
- };
393
- const results = await executeToolCalls([fakeToolCall], toolContext);
394
- for (const result of results) {
395
- const status = result.success ? pc.green('✔') : pc.red('✗');
396
- console.log(`\n${status} ${role} agent completed`);
397
- console.log(pc.white(result.content));
398
- if (!result.success && result.error) {
399
- console.log(pc.red(`Error: ${result.error}`));
400
- }
401
- }
402
- }
403
- // Handle /orchestrate command
404
- async function handleOrchestrate(goal) {
405
- console.log(pc.cyan(`\n[ORCHESTRATE] Starting orchestration for: ${goal}`));
406
- const { Orchestrator } = await import('./agents/orchestrator.js');
407
- const orchestrator = new Orchestrator(router, messages, toolContext);
408
- const result = await orchestrator.run(goal);
409
- console.log(pc.white(`\n${result}`));
410
- }
411
- // Handle /models command
412
- async function handleModels() {
413
- console.log(pc.bold('\n--- Available Models ---'));
414
- const models = await router.listModels();
415
- if (models.length === 0) {
416
- console.log(pc.yellow(' No models found. Check your local servers (LM Studio, Ollama, etc.)'));
417
- }
418
- else {
419
- for (const model of models) {
420
- console.log(` • ${pc.cyan(model)}`);
421
- }
422
- }
423
- const { checkModelHealth } = await import('./router/health.js');
424
- const healthyModels = router.getHealthyModels();
425
- console.log(pc.bold('\n--- Healthy Models ---'));
426
- for (const model of healthyModels) {
427
- const health = await checkModelHealth(model, 5000);
428
- const status = health?.healthy ? pc.green('●') : pc.red('●');
429
- console.log(` ${status} ${pc.cyan(model.name)} (${model.endpoint}) - ${model.model}`);
430
- }
431
- console.log(pc.bold('----------------------\n'));
432
- }
433
- // Handle /config command
434
- function handleConfig() {
435
- console.log(pc.bold('\n--- Current Configuration ---'));
436
- console.log(JSON.stringify(config, null, 2));
437
- console.log(pc.bold('-----------------------------'));
438
- console.log(pc.gray(`\nEdit ${configDir}/config.json to modify settings.`));
439
- console.log(pc.gray('Run `daedalus /doctor` to auto-discover local servers.'));
440
- }
441
- // Handle /doctor command
442
- async function handleDoctor() {
443
- console.log(pc.bold('\n--- Daedalus Doctor ---'));
444
- console.log(pc.gray('Checking local server connections...\n'));
445
- const discovered = await discoverLocalServers();
446
- if (discovered.length === 0) {
447
- console.log(pc.yellow(' No local servers detected.'));
448
- console.log(pc.gray(' Start one of:'));
449
- console.log(pc.gray(' • LM Studio (http://localhost:1234)'));
450
- console.log(pc.gray(' • Ollama (http://localhost:11434)'));
451
- console.log(pc.gray(' • llama.cpp server (--server, default :8080)'));
452
- console.log(pc.gray(' • vLLM (http://localhost:8000)'));
453
- }
454
- else {
455
- console.log(pc.green(` Found ${discovered.length} running server(s):\n`));
456
- for (const server of discovered) {
457
- console.log(` ${pc.green('●')} ${server.name} at ${server.endpoint}`);
458
- for (const model of server.models.slice(0, 5)) {
459
- console.log(` - ${model}`);
460
- }
461
- if (server.models.length > 5) {
462
- console.log(pc.gray(` ... and ${server.models.length - 5} more`));
463
- }
464
- }
465
- }
466
- console.log(pc.bold('\n--- Router Health ---'));
467
- const enabledModels = router.getEnabledModels();
468
- if (enabledModels.length === 0) {
469
- console.log(pc.yellow(' No models configured. Run /onboard to set one up.'));
470
- }
471
- else {
472
- for (const model of enabledModels) {
473
- const { checkModelHealth } = await import('./router/health.js');
474
- const health = await checkModelHealth(model, 5000);
475
- const status = health.healthy ? pc.green('●') : pc.red('●');
476
- const latency = health.latencyMs ? ` (${health.latencyMs}ms)` : '';
477
- const err = health.error ? ` ${pc.red(health.error)}` : '';
478
- console.log(` ${status} ${model.name}: ${model.endpoint}${latency}${err}`);
479
- }
480
- }
481
- console.log(pc.bold(' Config:') + pc.gray(` ${configDir}\\config.json`));
482
- console.log(pc.bold('----------------------\n'));
483
- } // end handleDoctor
484
- async function handleIndex(opts) {
485
- console.log(pc.bold('\n--- Indexing Codebase ---'));
486
- console.log(pc.gray(`Project: ${process.cwd()}`));
487
- const indexDbPath = getIndexDbPath();
488
- if (!fs.existsSync(path.dirname(indexDbPath))) {
489
- fs.mkdirSync(path.dirname(indexDbPath), { recursive: true });
490
- }
491
- const { initIndexDb } = await import('./indexing/fts.js');
492
- const { indexCodebase } = await import('./indexing/indexer.js');
493
- const db = initIndexDb(indexDbPath);
494
- console.log(pc.gray('\nScanning files...'));
495
- const start = Date.now();
496
- try {
497
- const barWidth = 20;
498
- let lastPct = -1;
499
- const onProgress = ({ current, total, file }) => {
500
- const pct = Math.round((current / total) * 100);
501
- if (pct === lastPct)
502
- return;
503
- lastPct = pct;
504
- const filled = Math.round((current / total) * barWidth);
505
- const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
506
- process.stdout.write(`\r ${pc.cyan(bar)} ${pc.white(`${current}/${total}`)} ${pc.gray(file.slice(-40))}`);
507
- };
508
- const result = await indexCodebase(db, process.cwd(), projectHash, { ...opts, onProgress });
509
- process.stdout.write('\n');
510
- const elapsed = Date.now() - start;
511
- toolContext.indexDb = db;
512
- console.log(pc.green(`\n✔ Indexing complete in ${elapsed}ms`));
513
- console.log(pc.white(` Total files: ${result.totalFiles}`));
514
- console.log(pc.white(` Indexed files: ${result.indexedFiles}`));
515
- console.log(pc.white(` Skipped (unchanged): ${result.skippedFiles}`));
516
- if (result.errors.length > 0) {
517
- console.log(pc.yellow(`\nErrors (${result.errors.length}):`));
518
- result.errors.slice(0, 10).forEach(e => console.log(pc.red(` - ${e}`)));
519
- if (result.errors.length > 10) {
520
- console.log(pc.gray(` ... and ${result.errors.length - 10} more`));
521
- }
522
- }
523
- }
524
- catch (err) {
525
- console.error(pc.red(`\n[ERROR] Indexing failed: ${err.message}`));
526
- }
527
- }
528
- async function handleFindSymbol(query, limit) {
529
- const indexDbPath = getIndexDbPath();
530
- if (!fs.existsSync(indexDbPath)) {
531
- console.log(pc.yellow('[WARN] No index found. Run /index first.'));
532
- return;
533
- }
534
- const { initIndexDb, searchSymbols } = await import('./indexing/fts.js');
535
- const db = initIndexDb(indexDbPath);
536
- console.log(pc.bold(`\n--- Symbol Search: "${query}" ---`));
537
- const symbols = searchSymbols(db, query, projectHash, limit);
538
- if (symbols.length === 0) {
539
- console.log(pc.gray(' No symbols found.'));
540
- return;
541
- }
542
- console.log(pc.white(`\nFound ${symbols.length} symbol(s):`));
543
- for (const s of symbols) {
544
- const kindColor = s.kind === 'function' ? pc.cyan : s.kind === 'class' ? pc.green : s.kind === 'interface' ? pc.blue : pc.white;
545
- const loc = `${s.file_path}:${s.line_start}${s.line_end !== s.line_start ? '-' + s.line_end : ''}`;
546
- console.log(` ${kindColor(`[${s.kind}]`)} ${pc.bold(s.name)} ${pc.dim(`(${loc})`)}`);
547
- if (s.signature) {
548
- console.log(pc.dim(` ${s.signature.slice(0, 100)}${s.signature.length > 100 ? '...' : ''}`));
549
- }
550
- }
551
- }
552
- async function handleGetReferences(symbol) {
553
- const indexDbPath = getIndexDbPath();
554
- if (!fs.existsSync(indexDbPath)) {
555
- console.log(pc.yellow('[WARN] No index found. Run /index first.'));
556
- return;
557
- }
558
- const { initIndexDb, findReferences } = await import('./indexing/fts.js');
559
- const db = initIndexDb(indexDbPath);
560
- console.log(pc.bold(`\n--- References to: ${symbol} ---`));
561
- const refs = findReferences(db, symbol, projectHash);
562
- if (refs.length === 0) {
563
- console.log(pc.gray(' No references found.'));
564
- return;
565
- }
566
- const byCaller = new Map();
567
- for (const r of refs) {
568
- const key = `${r.caller_name} (${r.caller_file}:${r.caller_line})`;
569
- if (!byCaller.has(key))
570
- byCaller.set(key, []);
571
- byCaller.get(key).push(r);
572
- }
573
- console.log(pc.white(`\nFound ${refs.length} reference(s) from ${byCaller.size} caller(s):`));
574
- for (const [caller, refs] of byCaller) {
575
- console.log(pc.cyan(`\n ${caller}:`));
576
- for (const r of refs.slice(0, 5)) {
577
- console.log(pc.dim(` ${r.callee_name} at ${r.callee_file}:${r.callee_line}`));
578
- }
579
- if (refs.length > 5) {
580
- console.log(pc.dim(` ... and ${refs.length - 5} more`));
581
- }
582
- }
583
- }
584
- async function handleGetDefinition(symbol) {
585
- const indexDbPath = getIndexDbPath();
586
- if (!fs.existsSync(indexDbPath)) {
587
- console.log(pc.yellow('[WARN] No index found. Run /index first.'));
588
- return;
589
- }
590
- const { initIndexDb, findDefinitions } = await import('./indexing/fts.js');
591
- const db = initIndexDb(indexDbPath);
592
- console.log(pc.bold(`\n--- Definition: ${symbol} ---`));
593
- const defs = findDefinitions(db, symbol, projectHash);
594
- if (defs.length === 0) {
595
- console.log(pc.gray(' No definitions found.'));
596
- return;
597
- }
598
- console.log(pc.white(`\nFound ${defs.length} definition(s):`));
599
- for (const d of defs) {
600
- const kindColor = d.kind === 'function' ? pc.cyan : d.kind === 'class' ? pc.green : d.kind === 'interface' ? pc.blue : pc.white;
601
- const loc = `${d.file_path}:${d.line_start}${d.line_end !== d.line_start ? '-' + d.line_end : ''}`;
602
- console.log(` ${kindColor(`[${d.kind}]`)} ${pc.bold(d.name)} ${pc.dim(`(${loc})`)}`);
603
- if (d.signature) {
604
- console.log(pc.dim(` ${d.signature.slice(0, 120)}${d.signature.length > 120 ? '...' : ''}`));
605
- }
606
- }
607
- }
608
- function truncateToolResult(content) {
609
- if (content.length <= TOOL_RESULT_MAX_CHARS)
610
- return content;
611
- const kept = content.slice(0, TOOL_RESULT_MAX_CHARS);
612
- const dropped = content.length - TOOL_RESULT_MAX_CHARS;
613
- return `${kept}\n... [truncated ${dropped} chars — use read_file with offset/limit to see more]`;
614
- }
615
- // ── Version update check ──────────────────────────────────────────────────────
616
- const UPDATE_CACHE_PATH = path.join(os.homedir(), '.daedalus', 'temp', 'version-check.json');
617
- function parseVersion(v) {
618
- return v.replace(/^v/, '').split('.').map(Number);
619
- }
620
- function isNewerVersion(latest, current) {
621
- const l = parseVersion(latest);
622
- const c = parseVersion(current);
623
- for (let i = 0; i < Math.max(l.length, c.length); i++) {
624
- const lv = l[i] || 0;
625
- const cv = c[i] || 0;
626
- if (lv > cv)
627
- return true;
628
- if (lv < cv)
629
- return false;
630
- }
631
- return false;
632
- }
633
- async function checkForUpdates() {
634
- try {
635
- // Check cache — only re-fetch every 24 hours
636
- if (fs.existsSync(UPDATE_CACHE_PATH)) {
637
- const cached = JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf8'));
638
- if (Date.now() - cached.timestamp < 86_400_000) {
639
- if (cached.latest && isNewerVersion(cached.latest, APP_VERSION)) {
640
- console.log(pc.cyan(`\n ⬆ Update available: ${pc.bold(cached.latest)} (you have ${APP_VERSION})`));
641
- console.log(pc.dim(` Run ${pc.cyan('npm update -g daedalus-cli')} to upgrade`));
642
- }
643
- return;
644
- }
645
- }
646
- const res = await fetch('https://registry.npmjs.org/daedalus-cli/latest');
647
- if (!res.ok)
648
- return;
649
- const data = await res.json();
650
- const latest = data.version;
651
- // Cache result
652
- fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true });
653
- fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify({ latest, timestamp: Date.now() }), 'utf8');
654
- if (isNewerVersion(latest, APP_VERSION)) {
655
- console.log(pc.cyan(`\n ⬆ Update available: ${pc.bold(latest)} (you have ${APP_VERSION})`));
656
- console.log(pc.dim(` Run ${pc.cyan('npm update -g daedalus-cli')} to upgrade`));
657
- }
658
- }
659
- catch {
660
- // Network failure, stale cache, etc. — silently ignore
661
- }
662
- }
663
- // Streaming response handler with tool call support — iterative, not recursive
664
- const MAX_TOOL_TURNS = 40;
665
- async function callModelWithTools(userContent, imageBase64) {
666
- // Push the user message as-is — callers in chatLoop are responsible for
667
- // pre-augmenting userContent with file context and index context.
668
- if (userContent) {
669
- if (imageBase64) {
670
- messages.push({
671
- role: 'user',
672
- content: [
673
- { type: 'text', text: userContent },
674
- { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
675
- ],
676
- });
677
- }
678
- else {
679
- messages.push({ role: 'user', content: userContent });
680
- }
681
- }
682
- // Auto-pruning check based on config context limit
683
- if (config.ui.showTokens) {
684
- const maxT = config.context.maxTokens ?? 128000;
685
- const summarizeAt = config.context.summarizeAt ?? 0.8;
686
- const threshold = Math.floor(maxT * summarizeAt);
687
- const fileCtx = buildFileContext();
688
- const tokens = calculateSessionTokens(messages, fileCtx);
689
- if (tokens.total > threshold) {
690
- console.log(pc.yellow(`\n ${pc.bold('[WARN]')} Context budget threshold exceeded (${Math.round(summarizeAt * 100)}%). Auto-pruning older turns...`));
691
- const target = Math.floor(maxT * 0.6); // Prune down to 60%
692
- const pruneResult = pruneMessages(messages, fileCtx, target);
693
- if (pruneResult.prunedTurns > 0 || pruneResult.truncatedTools > 0) {
694
- console.log(` ${pc.green('✔')} Pruned ${pruneResult.prunedTurns} older cycles and truncated ${pruneResult.truncatedTools} tool outputs (saved ~${Math.round(pruneResult.savedTokens / 1000)}kt).`);
695
- }
696
- }
697
- }
698
- // Combine built-in tools with MCP tools (stable across turns)
699
- const allTools = [...BUILTIN_TOOLS, ...POWER_TOOLS, ...mcpRegistry.getToolDefinitions()];
700
- turnStartTime = Date.now();
701
- let lastContent = '';
702
- let turn = 0;
703
- while (turn < MAX_TOOL_TURNS) {
704
- const spinner = new DaedalusSpinner({ text: 'Daedalus thinking', color: (s) => pc.cyan(s) });
705
- spinner.start();
706
- let fullContent = '';
707
- const toolCallMap = new Map();
708
- let blockOpened = false;
709
- const turnStart = Date.now();
710
- const openBlock = () => {
711
- if (!blockOpened) {
712
- blockOpened = true;
713
- spinner.stop();
714
- openAssistantBlock();
715
- }
716
- };
717
- currentAbortController = new AbortController();
718
- const signal = currentAbortController.signal;
719
- try {
720
- const stream = await router.chatStream({
721
- model: 'auto',
722
- messages: messages,
723
- temperature: 0.1,
724
- tools: allTools,
725
- tool_choice: 'auto',
726
- stream: true,
727
- signal,
728
- });
729
- for await (const chunk of stream) {
730
- if (signal.aborted)
731
- break;
732
- const choice = chunk.choices[0];
733
- if (!choice)
734
- continue;
735
- const delta = choice.delta;
736
- if (delta.content) {
737
- openBlock();
738
- fullContent += delta.content;
739
- writeAssistantChunk(delta.content);
740
- }
741
- if (delta.tool_calls) {
742
- openBlock();
743
- for (const tc of delta.tool_calls) {
744
- const index = tc.index ?? 0;
745
- if (!toolCallMap.has(index)) {
746
- toolCallMap.set(index, {
747
- id: tc.id ?? `call_${Date.now()}_${index}`,
748
- type: 'function',
749
- function: { name: '', arguments: '' },
750
- });
751
- }
752
- const call = toolCallMap.get(index);
753
- if (tc.function?.name)
754
- call.function.name = tc.function.name;
755
- if (tc.function?.arguments)
756
- call.function.arguments += tc.function.arguments;
757
- }
758
- }
759
- if (choice.finish_reason === 'tool_calls' || choice.finish_reason === 'stop' || choice.finish_reason === 'length') {
760
- break;
761
- }
762
- }
763
- // Ensure spinner is stopped even if no output was produced
764
- if (!blockOpened)
765
- spinner.stop();
766
- // Check for user cancellation
767
- if (signal.aborted) {
768
- if (blockOpened)
769
- closeAssistantBlock(fullContent.length, Date.now() - turnStart);
770
- console.log(pc.dim('\n [STOP] Stopped'));
771
- currentAbortController = null;
772
- return { content: fullContent, toolCalls: [] };
773
- }
774
- }
775
- catch (error) {
776
- if (signal.aborted) {
777
- spinner.stop();
778
- console.log(pc.dim('\n [STOP] Stopped'));
779
- currentAbortController = null;
780
- return { content: '', toolCalls: [] };
781
- }
782
- spinner.stop();
783
- const firstLine = (error.message || '').split('\n')[0];
784
- console.log(pc.yellow(`\n ${pc.bold('[WARN]')} Error calling model: ${firstLine}`));
785
- console.log(pc.dim(` (Tip: Run /doctor to diagnose connection or loading issues)`));
786
- throw error;
787
- }
788
- currentAbortController = null;
789
- let toolCallArray = Array.from(toolCallMap.values()).filter(tc => tc.function.name);
790
- if (toolCallArray.length === 0) {
791
- const parsedCalls = parseTextToolCalls(fullContent);
792
- if (parsedCalls.length > 0) {
793
- toolCallArray = parsedCalls;
794
- }
795
- }
796
- lastContent = fullContent;
797
- // Close assistant block after streaming, before tool results
798
- if (blockOpened) {
799
- closeAssistantBlock(fullContent.length, Date.now() - turnStart, toolCallArray.length);
800
- }
801
- if (toolCallArray.length === 0) {
802
- // No tool calls — model is done
803
- messages.push({ role: 'assistant', content: fullContent });
804
- return { content: fullContent, toolCalls: [] };
805
- }
806
- // Push assistant turn with tool calls
807
- messages.push({
808
- role: 'assistant',
809
- content: fullContent || '',
810
- tool_calls: toolCallArray,
811
- });
812
- // Ask user before executing dangerous tools
813
- const dangerousTools = ['terminal', 'write_file'];
814
- let turnApproved = false;
815
- const approvedCallIndices = new Set();
816
- for (let i = 0; i < toolCallArray.length; i++) {
817
- const tc = toolCallArray[i];
818
- if (dangerousTools.includes(tc.function.name) && !turnApproved) {
819
- const args = tc.function.arguments;
820
- const preview = args.length > 120 ? args.slice(0, 120) + '...' : args;
821
- process.stdout.write(`\n ${pc.yellow('[WARN]')} ${pc.bold(tc.function.name)} ${pc.dim(preview)}\n`);
822
- const line = await askLine(` ${pc.dim('Allow? [y]es / [n]o / [a]ll for this turn: ')}`);
823
- const char = line.trim().toLowerCase().slice(0, 1);
824
- if (char === 'a')
825
- turnApproved = true;
826
- if (char === 'n') {
827
- console.log(` ${pc.red('[FAIL]')} ${tc.function.name} ${pc.red(' — rejected')}`);
828
- continue;
829
- }
830
- }
831
- approvedCallIndices.add(i);
832
- }
833
- const approvedCalls = toolCallArray.filter((_, i) => approvedCallIndices.has(i));
834
- console.log(`\n ${pc.dim('[TOOL]')} ${pc.dim(`Executing ${approvedCalls.length} tool call(s)...`)}`);
835
- const results = await executeToolCalls(approvedCalls, toolContext);
836
- for (const result of results) {
837
- messages.push({
838
- role: 'tool',
839
- content: truncateToolResult(result.content),
840
- tool_call_id: result.toolCallId,
841
- });
842
- const status = result.success ? pc.green('✔') : pc.red('✗');
843
- console.log(` ${status} ${result.name}`);
844
- if (!result.success && result.error) {
845
- console.log(pc.red(` Error: ${result.error}`));
846
- }
847
- // Show inline preview of successful tool results
848
- if (result.success && result.content) {
849
- const contentStr = result.content;
850
- const lines = contentStr.split('\n');
851
- const previewLines = lines.slice(0, 3);
852
- for (const line of previewLines) {
853
- const truncated = line.length > 120 ? line.slice(0, 120) + '…' : line;
854
- if (truncated.trim())
855
- console.log(` ${pc.dim('┃')} ${pc.gray(truncated)}`);
856
- }
857
- if (lines.length > 3) {
858
- console.log(` ${pc.dim('┃')} ${pc.dim(`… ${lines.length - 3} more line${lines.length - 3 > 1 ? 's' : ''}`)}`);
859
- }
860
- }
861
- if (result.success && result.name === 'screenshot_page') {
862
- try {
863
- const parsed = JSON.parse(result.content);
864
- if (parsed.type === 'vision' && parsed.base64) {
865
- messages.push({
866
- role: 'user',
867
- content: [
868
- { type: 'text', text: `[Screenshot of ${parsed.url}${parsed.selector ? ` > ${parsed.selector}` : ''}]` },
869
- { type: 'image_url', image_url: { url: `data:image/png;base64,${parsed.base64}` } },
870
- ],
871
- });
872
- console.log(` ${pc.cyan('[VISION]')} Screenshot injected into context (${Math.round(parsed.base64.length * 0.75 / 1024)}KB)`);
873
- }
874
- }
875
- catch { }
876
- }
877
- }
878
- turn++;
879
- }
880
- // Reached max turns without a clean stop
881
- console.log(`\n ${pc.yellow('[WARN]')} ${pc.yellow(`Reached max tool turns (${MAX_TOOL_TURNS}). Stopping.`)}`);
882
- messages.push({ role: 'assistant', content: lastContent });
883
- return { content: lastContent, toolCalls: [] };
884
- }
885
- // Fallback: Structured output for models without tool support
886
- async function callModelWithFallback(userContent, imageBase64) {
887
- if (imageBase64) {
888
- messages.push({
889
- role: 'user',
890
- content: [
891
- { type: 'text', text: userContent },
892
- { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
893
- ],
894
- });
895
- }
896
- else {
897
- messages.push({ role: 'user', content: userContent });
898
- }
899
- console.log(pc.gray(' [THINK] Thinking (fallback mode)...'));
900
- try {
901
- const response = await router.chat.completions.create({
902
- model: 'auto',
903
- messages: messages,
904
- temperature: 0.1,
905
- });
906
- const reply = response.choices[0].message?.content || '';
907
- messages.push({ role: 'assistant', content: reply });
908
- openAssistantBlock();
909
- writeAssistantChunk(reply);
910
- const usage = response.usage;
911
- const elapsed = Date.now() - turnStartTime;
912
- closeAssistantBlock(reply.length, elapsed);
913
- return reply;
914
- }
915
- catch (error) {
916
- const firstLine = (error.message || '').split('\n')[0];
917
- console.log(pc.yellow(`\n ${pc.bold('[WARN]')} Fallback error: ${firstLine}`));
918
- console.log(pc.dim(` (Tip: Run /doctor to diagnose connection or loading issues)`));
919
- throw error;
920
- }
921
- }
922
- // Single-line prompt (approval gate, commit message)
196
+ // Single-line prompt (approval gate, commit message) standalone to avoid rl conflict with REPL
923
197
  function askLine(prompt) {
924
- return new Promise((resolve) => rl.question(prompt, resolve));
925
- }
926
- // Multi-line input for main chat prompt — captures pasted text beyond the first newline.
927
- // Uses a timing heuristic: lines arriving within 80ms are treated as part of a single paste.
928
- function readMultiLineInput(prompt) {
929
198
  return new Promise((resolve) => {
930
- const lines = [];
931
- let timer = null;
932
- let resolved = false;
933
- const onLine = (line) => {
934
- if (resolved)
935
- return;
936
- lines.push(line);
937
- if (timer)
938
- clearTimeout(timer);
939
- timer = setTimeout(() => {
940
- resolved = true;
941
- rl.off('line', onLine);
942
- resolve(lines.join('\n'));
943
- }, 80);
944
- };
945
- rl.on('line', onLine);
946
- process.stdout.write(prompt);
947
- rl.resume();
199
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
200
+ rl.question(prompt, (answer) => { rl.close(); resolve(answer); });
948
201
  });
949
202
  }
950
- // ── Clipboard helpers ──────────────────────────────────────────────────────────
951
- function getClipboardText() {
952
- try {
953
- if (process.platform === 'win32') {
954
- return execSync('powershell -noprofile -command "Get-Clipboard"', { timeout: 5000, encoding: 'utf8' }).trim().slice(0, 100_000);
955
- }
956
- else if (process.platform === 'darwin') {
957
- return execSync('pbpaste', { timeout: 5000, encoding: 'utf8' }).trim().slice(0, 100_000);
958
- }
959
- else {
960
- return execSync('xclip -o -selection clipboard', { timeout: 5000, encoding: 'utf8' }).trim().slice(0, 100_000);
961
- }
962
- }
963
- catch {
964
- return '';
965
- }
966
- }
967
- function getClipboardImage() {
968
- const timestamp = Date.now();
969
- const random = crypto.randomBytes(4).toString('hex');
970
- const outPath = path.join(cliTempDir, `paste-${timestamp}-${random}.png`);
971
- try {
972
- if (process.platform === 'win32') {
973
- const safePath = outPath.replace(/[^a-zA-Z0-9_:.\\-]/g, '');
974
- const psScript = `
975
- Add-Type -AssemblyName System.Windows.Forms
976
- $img = [System.Windows.Forms.Clipboard]::GetImage()
977
- if ($img) {
978
- $img.Save('${safePath.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png)
979
- Write-Output 'ok'
980
- }`;
981
- const scriptPath = path.join(cliTempDir, `clip-img-${timestamp}-${random}.ps1`);
982
- fs.writeFileSync(scriptPath, psScript, 'utf8');
983
- const result = execSync(`powershell -noprofile -ExecutionPolicy Bypass -File "${scriptPath}"`, { timeout: 8000, encoding: 'utf8' }).trim();
984
- try {
985
- fs.unlinkSync(scriptPath);
986
- }
987
- catch { }
988
- if (result === 'ok' && fs.existsSync(outPath))
989
- return outPath;
990
- }
991
- else if (process.platform === 'darwin') {
992
- execSync(`pngpaste "${outPath}"`, { timeout: 5000 });
993
- if (fs.existsSync(outPath))
994
- return outPath;
995
- }
996
- else {
997
- const imgData = execSync(`xclip -selection clipboard -t image/png -o`, { timeout: 5000, encoding: 'buffer' });
998
- if (imgData && imgData.length > 0) {
999
- fs.writeFileSync(outPath, imgData);
1000
- return outPath;
1001
- }
1002
- }
1003
- }
1004
- catch {
1005
- // No image in clipboard
1006
- }
1007
- return null;
1008
- }
1009
- // ── Visual formatting helpers ──────────────────────────────────────────────────
1010
- const termW = Math.max(50, (process.stdout.columns ?? 80) - 5);
1011
- function wrapLine(line, maxW) {
1012
- if (line.length <= maxW)
1013
- return [line];
1014
- const words = line.split(' ');
1015
- const result = [];
1016
- let cur = '';
1017
- for (const word of words) {
1018
- if (cur && cur.length + 1 + word.length <= maxW) {
1019
- cur += ' ' + word;
1020
- }
1021
- else {
1022
- if (cur)
1023
- result.push(cur);
1024
- cur = word;
1025
- while (cur.length > maxW) {
1026
- result.push(cur.slice(0, maxW));
1027
- cur = cur.slice(maxW);
1028
- }
1029
- }
1030
- }
1031
- if (cur)
1032
- result.push(cur);
1033
- return result;
1034
- }
1035
- function printUserTurn(userMessage) {
1036
- const bdr = (s) => pc.dim(pc.yellow(s));
1037
- const lines = userMessage.split('\n');
1038
- let maxLineLen = 0;
1039
- const allWrappedLines = [];
1040
- for (const line of lines) {
1041
- const wrapped = wrapLine(line, termW);
1042
- allWrappedLines.push(...wrapped);
1043
- for (const part of wrapped) {
1044
- if (part.length > maxLineLen) {
1045
- maxLineLen = part.length;
1046
- }
1047
- }
1048
- }
1049
- const w = Math.max(15, maxLineLen);
1050
- console.log(`\n ${bdr('╭─')} ${pc.yellow(pc.bold('⬡ You'))} ${bdr('─'.repeat(Math.max(0, w - 7)))}${bdr('╮')}`);
1051
- for (const part of allWrappedLines) {
1052
- console.log(` ${bdr('│')} ${pc.white(part)}${' '.repeat(Math.max(0, w - part.length))}${bdr('│')}`);
1053
- }
1054
- console.log(` ${bdr('╰')}${bdr('─'.repeat(w + 1))}${bdr('╯')}`);
1055
- console.log();
1056
- }
1057
- let _assistantLineBuf = '';
1058
- let _inCodeBlock = false;
1059
- function stripAnsi(str) {
1060
- return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
1061
- }
1062
- function wrapAnsiLine(line, maxW) {
1063
- if (stripAnsi(line).length <= maxW)
1064
- return [line];
1065
- const words = line.split(' ');
1066
- const result = [];
1067
- let cur = '';
1068
- for (const word of words) {
1069
- const curVisible = stripAnsi(cur).length;
1070
- const wordVisible = stripAnsi(word).length;
1071
- if (cur && curVisible + 1 + wordVisible <= maxW) {
1072
- cur += ' ' + word;
1073
- }
1074
- else {
1075
- if (cur)
1076
- result.push(cur);
1077
- cur = word;
1078
- while (stripAnsi(cur).length > maxW) {
1079
- result.push(cur);
1080
- cur = '';
1081
- }
1082
- }
1083
- }
1084
- if (cur)
1085
- result.push(cur);
1086
- return result;
1087
- }
1088
- export function formatMarkdownLine(line) {
1089
- if (line.startsWith('# ')) {
1090
- return pc.bold(pc.cyan(line.substring(2)));
1091
- }
1092
- if (line.startsWith('## ')) {
1093
- return pc.bold(pc.cyan(line.substring(3)));
1094
- }
1095
- if (line.startsWith('### ')) {
1096
- return pc.bold(pc.cyan(line.substring(4)));
1097
- }
1098
- if (line.startsWith('> ')) {
1099
- return `${pc.gray('│')} ${pc.italic(line.substring(2))}`;
1100
- }
1101
- let prefix = '';
1102
- let content = line;
1103
- const listMatch = line.match(/^(\s*)([\*\-\+•])\s+(.*)$/);
1104
- if (listMatch) {
1105
- const indent = listMatch[1];
1106
- content = listMatch[3];
1107
- prefix = `${indent}${pc.gray('•')} `;
1108
- }
1109
- content = content.replace(/\`(.*?)\`/g, (_, p1) => pc.yellow(p1));
1110
- content = content.replace(/\*\*(.*?)\*\*/g, (_, p1) => pc.bold(p1));
1111
- content = content.replace(/\*(.*?)\*/g, (_, p1) => pc.italic(p1));
1112
- content = content.replace(/_(.*?)_/g, (_, p1) => pc.italic(p1));
1113
- return prefix + content;
1114
- }
1115
- function openAssistantBlock() {
1116
- console.log(`\n ${pc.cyan(pc.bold('◇ Daedalus'))}`);
1117
- }
1118
- function writeAssistantChunk(chunk) {
1119
- _assistantLineBuf += chunk;
1120
- const lines = _assistantLineBuf.split('\n');
1121
- _assistantLineBuf = lines.pop() || '';
1122
- for (const line of lines) {
1123
- const isCodeBlockDelimiter = line.trim().startsWith('```');
1124
- if (isCodeBlockDelimiter) {
1125
- _inCodeBlock = !_inCodeBlock;
1126
- }
1127
- if (_inCodeBlock || isCodeBlockDelimiter) {
1128
- for (const part of wrapLine(line, termW)) {
1129
- console.log(` ${pc.gray(part)}`);
1130
- }
1131
- }
1132
- else {
1133
- const formatted = formatMarkdownLine(line);
1134
- for (const part of wrapAnsiLine(formatted, termW)) {
1135
- console.log(` ${part}`);
1136
- }
1137
- }
1138
- }
1139
- }
1140
- export function parseTextToolCalls(text) {
1141
- const toolCalls = [];
1142
- const regex = /<(longcat_)?tool_call>([\s\S]*?)<\/(longcat_)?tool_call>/g;
1143
- let match;
1144
- while ((match = regex.exec(text)) !== null) {
1145
- const blockContent = match[2].trim();
1146
- const lines = blockContent.split('\n').map(l => l.trim()).filter(Boolean);
1147
- if (lines.length === 0)
1148
- continue;
1149
- const toolName = lines[0].replace(/<[^>]+>/g, '').trim();
1150
- const args = {};
1151
- const keyRegex = /<(longcat_)?arg_key>([\s\S]*?)<\/(longcat_)?arg_key>/g;
1152
- const valRegex = /<(longcat_)?arg_value>([\s\S]*?)<\/(longcat_)?arg_value>/g;
1153
- const keys = [];
1154
- const values = [];
1155
- let keyMatch;
1156
- while ((keyMatch = keyRegex.exec(blockContent)) !== null) {
1157
- keys.push(keyMatch[2].trim());
1158
- }
1159
- let valMatch;
1160
- while ((valMatch = valRegex.exec(blockContent)) !== null) {
1161
- values.push(valMatch[2].trim());
1162
- }
1163
- for (let i = 0; i < Math.min(keys.length, values.length); i++) {
1164
- const k = keys[i];
1165
- const v = values[i];
1166
- try {
1167
- args[k] = JSON.parse(v);
1168
- }
1169
- catch {
1170
- args[k] = v;
1171
- }
1172
- }
1173
- toolCalls.push({
1174
- id: `call_parsed_${Date.now()}_${toolCalls.length}`,
1175
- type: 'function',
1176
- function: {
1177
- name: toolName,
1178
- arguments: JSON.stringify(args),
1179
- }
1180
- });
1181
- }
1182
- return toolCalls;
1183
- }
1184
- function closeAssistantBlock(tokens, elapsedMs, toolCount) {
1185
- if (_assistantLineBuf) {
1186
- const isCodeBlockDelimiter = _assistantLineBuf.trim().startsWith('```');
1187
- if (isCodeBlockDelimiter) {
1188
- _inCodeBlock = !_inCodeBlock;
1189
- }
1190
- if (_inCodeBlock || isCodeBlockDelimiter) {
1191
- for (const part of wrapLine(_assistantLineBuf, termW)) {
1192
- console.log(` ${pc.gray(part)}`);
1193
- }
1194
- }
1195
- else {
1196
- const formatted = formatMarkdownLine(_assistantLineBuf);
1197
- for (const part of wrapAnsiLine(formatted, termW)) {
1198
- console.log(` ${part}`);
1199
- }
1200
- }
1201
- _assistantLineBuf = '';
1202
- }
1203
- _inCodeBlock = false;
1204
- const modelStr = router.lastRoutedModel ? `${router.lastRoutedModel} · ` : '';
1205
- const meta = toolCount !== undefined
1206
- ? `${modelStr}${toolCount} tool(s) · ~${Math.round(tokens / 4)}t out · ${elapsedMs}ms`
1207
- : `${modelStr}~${Math.round(tokens / 4)}t out · ${elapsedMs}ms`;
1208
- console.log(` ${pc.dim(meta)}\n`);
1209
- }
1210
- function turnSeparator() {
1211
- const ts = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
1212
- console.log(` ${pc.dim('─'.repeat(58))} ${pc.dim(ts)}`);
1213
- }
1214
- // Main chat loop — iterative, no recursive Promise chains
1215
- async function chatLoop() {
1216
- while (true) {
1217
- let prompt = `\n${pc.cyan(' ⬡')} `;
1218
- if (activeFiles.size > 0 || (config.ui.showTokens && messages.length > 1)) {
1219
- const fileStr = activeFiles.size > 0 ? `${activeFiles.size} file${activeFiles.size > 1 ? 's' : ''}` : '';
1220
- let tokenStr = '';
1221
- if (config.ui.showTokens) {
1222
- const tokens = calculateSessionTokens(messages, buildFileContext());
1223
- const total = tokens.total;
1224
- tokenStr = total >= 1000 ? `${(total / 1000).toFixed(1)}kt` : `${total}t`;
1225
- }
1226
- const separator = fileStr && tokenStr ? ' · ' : '';
1227
- prompt += pc.dim(`[${fileStr}${separator}${tokenStr}] `);
1228
- }
1229
- prompt += `${pc.bold(pc.white('›'))} `;
1230
- const input = await readMultiLineInput(prompt);
1231
- const trimmedInput = input.trim();
1232
- if (!trimmedInput)
1233
- continue;
1234
- const lowerInput = trimmedInput.toLowerCase();
1235
- // Handle Exit
1236
- if (lowerInput === 'exit' || lowerInput === 'quit') {
1237
- const todos = getSessionTodos(sessionId);
1238
- sessionManager.saveSessionState(messages, activeFiles, todos);
1239
- console.log(pc.dim(' [EXTRACT] Extracting facts from session...'));
1240
- await extractAndSave(router, sessionManager, messages);
1241
- console.log(pc.gray(`Session saved: ${sessionManager.sessionId}`));
1242
- console.log(pc.yellow('\nEnding session. Goodbye!\n'));
1243
- rl.close();
1244
- process.exit(0);
1245
- }
1246
- // Command quickref
1247
- if (lowerInput === '?' || lowerInput === 'help' || lowerInput === '/help') {
1248
- console.log(`\n ${pc.bold('Commands')}`);
1249
- console.log(` ${pc.dim('────────────────────────────────────')}`);
1250
- console.log(` ${pc.cyan('/add')} Add file to context`);
1251
- console.log(` ${pc.cyan('/remove')} Remove file from context`);
1252
- console.log(` ${pc.cyan('/context')} Show active file context`);
1253
- console.log(` ${pc.cyan('/prune')} Prune old conversation history & save budget`);
1254
- console.log(` ${pc.cyan('/commit')} Stage and commit changes`);
1255
- console.log(` ${pc.cyan('/branch')} View or create Git branches`);
1256
- console.log(` ${pc.cyan('/pr')} Generate Pull Request description`);
1257
- console.log(` ${pc.cyan('/undo')} Undo last file patch`);
1258
- console.log(` ${pc.cyan('/paste')} Paste clipboard text/image as message`);
1259
- console.log(` ${pc.cyan('/test')} Run tests and auto-fix failures`);
1260
- console.log(` ${pc.cyan('/debug')} Run a command and autonomously fix failures`);
1261
- console.log(` ${pc.cyan('/ensemble')} Run multi-model ensemble drafting pipeline`);
1262
- console.log(` ${pc.cyan('/index')} Index codebase for symbol search`);
1263
- console.log(` ${pc.cyan('/find')} Search indexed symbols`);
1264
- console.log(` ${pc.cyan('/refs')} Find symbol references`);
1265
- console.log(` ${pc.cyan('/def')} Get symbol definition`);
1266
- console.log(` ${pc.cyan('/project')} View or set project config`);
1267
- console.log(` ${pc.cyan('/extract')} Manually extract facts from session`);
1268
- console.log(` ${pc.cyan('/profile')} View or set your user profile`);
1269
- console.log(` ${pc.cyan('/style')} Set your coding style/preferences`);
1270
- console.log(` ${pc.cyan('/session')} Manage chat sessions`);
1271
- console.log(` ${pc.cyan('?')} Show this help menu`);
1272
- console.log(` ${pc.cyan('exit')} Save and quit\n`);
1273
- continue;
1274
- }
1275
- // Command: /add [file]
1276
- if (lowerInput === '/add' || lowerInput.startsWith('/add ')) {
1277
- const fileArg = trimmedInput.substring(4).trim();
1278
- if (!fileArg) {
1279
- const { runInteractiveFileSelector } = await import('./session/selector.js');
1280
- rl.pause();
1281
- const result = await runInteractiveFileSelector(process.cwd(), config.indexing.exclude, new Set(activeFiles.keys()));
1282
- rl.resume();
1283
- if (result !== null) {
1284
- activeFiles.clear();
1285
- for (const absPath of result) {
1286
- const rel = path.relative(process.cwd(), absPath);
1287
- activeFiles.set(absPath, rel);
1288
- }
1289
- toolContext.activeFiles = new Map(activeFiles);
1290
- console.log(pc.green(`\n[OK] Active context files updated: ${activeFiles.size} file(s)`));
1291
- }
1292
- }
1293
- else {
1294
- const absPath = path.resolve(fileArg);
1295
- activeFiles.set(absPath, fileArg);
1296
- toolContext.activeFiles = new Map(activeFiles);
1297
- console.log(pc.green(`[OK] Added file to context: ${pc.bold(fileArg)}`));
1298
- }
1299
- continue;
1300
- }
1301
- // Command: /remove <file>
1302
- if (trimmedInput.startsWith('/remove ')) {
1303
- const fileArg = trimmedInput.substring(8).trim();
1304
- if (!fileArg) {
1305
- console.log(pc.red('[WARN] Please specify a file path. Example: /remove src/App.tsx'));
1306
- }
1307
- else {
1308
- const absPath = path.resolve(fileArg);
1309
- if (activeFiles.delete(absPath)) {
1310
- toolContext.activeFiles = new Map(activeFiles);
1311
- console.log(pc.green(`[OK] Removed file from context: ${pc.bold(fileArg)}`));
1312
- }
1313
- else {
1314
- console.log(pc.yellow(`[WARN] File was not in context: ${fileArg}`));
1315
- }
1316
- }
1317
- continue;
1318
- }
1319
- // Command: /paste — paste clipboard content (text or image)
1320
- if (lowerInput === '/paste' || lowerInput.startsWith('/paste ')) {
1321
- const extra = trimmedInput.startsWith('/paste ') ? trimmedInput.substring(7).trim() : '';
1322
- // If extra looks like a file path, try reading it as an image
1323
- if (extra && !extra.startsWith('http')) {
1324
- const filePath = path.resolve(extra);
1325
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
1326
- const ext = path.extname(filePath).toLowerCase();
1327
- if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext)) {
1328
- const imgBuffer = fs.readFileSync(filePath);
1329
- const base64 = imgBuffer.toString('base64');
1330
- const message = 'What do you see in this image?';
1331
- printUserTurn(`${path.basename(filePath)} (image)`);
1332
- try {
1333
- const filesContext = buildFileContext();
1334
- const indexCtx = await buildIndexContext(message);
1335
- const userContent = `${indexCtx}${filesContext}User Prompt: ${message}`;
1336
- await callModelWithTools(userContent, base64);
1337
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
1338
- }
1339
- catch (error) {
1340
- try {
1341
- const filesContext = buildFileContext();
1342
- const userContent = `${filesContext}User Prompt: ${message}`;
1343
- console.log(pc.yellow('\n [RETRY] Trying fallback mode...'));
1344
- await callModelWithFallback(userContent, base64);
1345
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
1346
- }
1347
- catch (fallbackErr) {
1348
- const firstLine = (fallbackErr.message || '').split('\n')[0];
1349
- console.log(pc.red(`\n ${pc.bold('[ERROR]')} Fallback also failed: ${firstLine}`));
1350
- }
1351
- }
1352
- turnSeparator();
1353
- continue;
1354
- }
1355
- }
1356
- }
1357
- // Try image first
1358
- const imgPath = getClipboardImage();
1359
- if (imgPath) {
1360
- const imgBuffer = fs.readFileSync(imgPath);
1361
- fs.unlinkSync(imgPath); // Clean up temp file
1362
- const base64 = imgBuffer.toString('base64');
1363
- const message = extra || 'What do you see in this image?';
1364
- printUserTurn(`${message} (image)`);
1365
- try {
1366
- const filesContext = buildFileContext();
1367
- const indexCtx = await buildIndexContext(message);
1368
- const userContent = `${indexCtx}${filesContext}User Prompt: ${message}`;
1369
- await callModelWithTools(userContent, base64);
1370
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
1371
- }
1372
- catch (error) {
1373
- try {
1374
- const filesContext = buildFileContext();
1375
- const userContent = `${filesContext}User Prompt: ${message}`;
1376
- console.log(pc.yellow('\n [RETRY] Trying fallback mode...'));
1377
- await callModelWithFallback(userContent, base64);
1378
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
1379
- }
1380
- catch (fallbackErr) {
1381
- const firstLine = (fallbackErr.message || '').split('\n')[0];
1382
- console.log(pc.red(`\n ${pc.bold('[ERROR]')} Fallback also failed: ${firstLine}`));
1383
- }
1384
- }
1385
- turnSeparator();
1386
- continue;
1387
- }
1388
- // Fall back to text
1389
- const clipboard = getClipboardText();
1390
- if (!clipboard) {
1391
- console.log(pc.red('[WARN] Clipboard is empty or inaccessible.'));
1392
- continue;
1393
- }
1394
- const fullMessage = extra ? `${clipboard}\n\n${extra}` : clipboard;
1395
- printUserTurn(fullMessage);
1396
- try {
1397
- const filesContext = buildFileContext();
1398
- const indexCtx = await buildIndexContext(fullMessage);
1399
- const userContent = `${indexCtx}${filesContext}User Prompt: ${fullMessage}`;
1400
- await callModelWithTools(userContent);
1401
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
1402
- }
1403
- catch (error) {
1404
- }
1405
- turnSeparator();
1406
- continue;
1407
- }
1408
- // Command: /context
1409
- if (lowerInput === '/context') {
1410
- console.log(pc.bold('\n--- Monitored Files in Context ---'));
1411
- if (activeFiles.size === 0) {
1412
- console.log(pc.gray(' (No active files. Use "/add <filepath>" to add files)'));
1413
- }
1414
- else {
1415
- activeFiles.forEach((filename) => {
1416
- console.log(` • ${pc.cyan(filename)}`);
1417
- });
1418
- }
1419
- console.log(pc.bold('----------------------------------'));
1420
- continue;
1421
- }
1422
- // Command: /prune
1423
- if (lowerInput.startsWith('/prune')) {
1424
- const parts = trimmedInput.substring(6).trim().split(/\s+/);
1425
- const arg = parts[0]?.trim();
1426
- const maxT = config.context.maxTokens ?? 128000;
1427
- let target = Math.floor(maxT * 0.5); // Default to 50%
1428
- if (arg) {
1429
- if (arg.endsWith('%')) {
1430
- const pct = parseFloat(arg.slice(0, -1)) / 100;
1431
- if (!isNaN(pct) && pct > 0 && pct <= 1) {
1432
- target = Math.floor(maxT * pct);
1433
- }
1434
- }
1435
- else {
1436
- const num = parseInt(arg, 10);
1437
- if (!isNaN(num) && num > 0) {
1438
- target = num;
1439
- }
1440
- }
1441
- }
1442
- const fileCtx = buildFileContext();
1443
- const beforeTokens = calculateSessionTokens(messages, fileCtx);
1444
- console.log(pc.bold('\n--- Context Budget Status ---'));
1445
- console.log(` System Prompt: ${(beforeTokens.system / 1000).toFixed(1)}kt`);
1446
- console.log(` Message History: ${(beforeTokens.history / 1000).toFixed(1)}kt`);
1447
- console.log(` Active Files: ${(beforeTokens.activeFiles / 1000).toFixed(1)}kt`);
1448
- console.log(` Total Active: ${(beforeTokens.total / 1000).toFixed(1)}kt / ${(maxT / 1000).toFixed(0)}kt (${Math.round((beforeTokens.total / maxT) * 100)}%)`);
1449
- console.log(` Target Limit: ${(target / 1000).toFixed(1)}kt`);
1450
- console.log(pc.bold('-----------------------------'));
1451
- const pruneResult = pruneMessages(messages, fileCtx, target);
1452
- if (pruneResult.prunedTurns > 0 || pruneResult.truncatedTools > 0) {
1453
- console.log(pc.green(`\n[OK] Pruned ${pruneResult.prunedTurns} older turns and truncated ${pruneResult.truncatedTools} large tool outputs.`));
1454
- console.log(pc.green(`Reclaimed ~${Math.round(pruneResult.savedTokens / 1000)}kt.`));
1455
- }
1456
- else {
1457
- console.log(pc.gray('\nContext is already within target budget. No pruning needed.'));
1458
- }
1459
- continue;
1460
- }
1461
- // Command: /session list|load|new|delete
1462
- if (lowerInput.startsWith('/session')) {
1463
- const parts = trimmedInput.substring(9).trim().split(/\s+/);
1464
- const subCmd = parts[0]?.toLowerCase();
1465
- if (subCmd === 'list') {
1466
- const list = sessionManager.getSessionsForProject();
1467
- console.log(pc.bold('\n--- Sessions for this Project ---'));
1468
- if (list.length === 0) {
1469
- console.log(pc.gray(' No saved sessions.'));
1470
- }
1471
- else {
1472
- list.forEach((s) => {
1473
- const dateStr = new Date(s.updated_at).toLocaleString();
1474
- const activeMarker = s.id === sessionManager.sessionId ? pc.green(' * ') : ' ';
1475
- console.log(`${activeMarker}${s.title} (ID: ${pc.cyan(s.id)}) - Last updated: ${dateStr}`);
1476
- });
1477
- }
1478
- console.log(pc.bold('---------------------------------'));
1479
- }
1480
- else if (subCmd === 'load') {
1481
- const targetId = parts[1]?.trim();
1482
- if (!targetId) {
1483
- console.log(pc.red('[WARN] Usage: /session load <id>'));
1484
- }
1485
- else {
1486
- try {
1487
- const todos = getSessionTodos(sessionId);
1488
- sessionManager.saveSessionState(messages, activeFiles, todos);
1489
- const loaded = sessionManager.startSession(targetId);
1490
- initializeSessionState(loaded);
1491
- console.log(pc.green(`[OK] Loaded session: ${sessionManager.sessionTitle} (${sessionManager.sessionId})`));
1492
- }
1493
- catch (err) {
1494
- console.log(pc.red(`[WARN] Failed to load session: ${err.message}`));
1495
- }
1496
- }
1497
- }
1498
- else if (subCmd === 'new') {
1499
- const title = parts.slice(1).join(' ').trim();
1500
- const todos = getSessionTodos(sessionId);
1501
- sessionManager.saveSessionState(messages, activeFiles, todos);
1502
- const loaded = sessionManager.startSession(undefined, title || undefined);
1503
- initializeSessionState(loaded);
1504
- console.log(pc.green(`[OK] Started new session: ${sessionManager.sessionTitle} (${sessionManager.sessionId})`));
1505
- }
1506
- else if (subCmd === 'delete') {
1507
- const targetId = parts[1]?.trim();
1508
- if (!targetId) {
1509
- console.log(pc.red('[WARN] Usage: /session delete <id>'));
1510
- }
1511
- else {
1512
- sessionManager.deleteSession(targetId);
1513
- console.log(pc.green(`[OK] Deleted session: ${targetId}`));
1514
- }
1515
- }
1516
- else {
1517
- console.log(pc.red('[WARN] Usage: /session list | load <id> | new [title] | delete <id>'));
1518
- }
1519
- continue;
1520
- }
1521
- // Command: /memory
1522
- if (lowerInput === '/memory') {
1523
- const mem = sessionManager.loadMemory();
1524
- console.log(pc.bold('\n--- Project Facts & Conventions (Memory) ---'));
1525
- console.log(pc.bold('Conventions:'));
1526
- if (Object.keys(mem.conventions).length === 0) {
1527
- console.log(pc.gray(' No conventions saved.'));
1528
- }
1529
- else {
1530
- for (const [k, v] of Object.entries(mem.conventions)) {
1531
- console.log(` • ${pc.cyan(k)}: ${v}`);
1532
- }
1533
- }
1534
- console.log(pc.bold('\nFacts:'));
1535
- if (mem.facts.length === 0) {
1536
- console.log(pc.gray(' No facts saved.'));
1537
- }
1538
- else {
1539
- mem.facts.forEach(f => {
1540
- console.log(` • ${pc.cyan(f.key)}: ${f.value} (source: ${f.source})`);
1541
- });
1542
- }
1543
- console.log(pc.bold('------------------------------------------'));
1544
- continue;
1545
- }
1546
- // Command: /fact <key> = <value>
1547
- if (lowerInput.startsWith('/fact ')) {
1548
- const argStr = trimmedInput.substring(6).trim();
1549
- const eqIdx = argStr.indexOf('=');
1550
- if (eqIdx < 0) {
1551
- console.log(pc.red('[WARN] Usage: /fact <key> = <value>'));
1552
- }
1553
- else {
1554
- const key = argStr.slice(0, eqIdx).trim();
1555
- const value = argStr.slice(eqIdx + 1).trim();
1556
- sessionManager.addFact(key, value, 'user');
1557
- console.log(pc.green(`[OK] Saved fact: ${key} = ${value}`));
1558
- }
1559
- continue;
1560
- }
1561
- // Command: /convention <key> = <value>
1562
- if (lowerInput.startsWith('/convention ')) {
1563
- const argStr = trimmedInput.substring(12).trim();
1564
- const eqIdx = argStr.indexOf('=');
1565
- if (eqIdx < 0) {
1566
- console.log(pc.red('[WARN] Usage: /convention <key> = <value>'));
1567
- }
1568
- else {
1569
- const key = argStr.slice(0, eqIdx).trim();
1570
- const value = argStr.slice(eqIdx + 1).trim();
1571
- sessionManager.setConvention(key, value);
1572
- console.log(pc.green(`[OK] Saved convention: ${key} = ${value}`));
1573
- }
1574
- continue;
1575
- }
1576
- // Command: /extract — manually trigger fact extraction
1577
- if (lowerInput === '/extract') {
1578
- console.log(pc.dim(' [EXTRACT] Extracting facts from conversation...'));
1579
- await extractAndSave(router, sessionManager, messages);
1580
- continue;
1581
- }
1582
- // Command: /profile
1583
- if (lowerInput.startsWith('/profile')) {
1584
- const rest = trimmedInput.substring(8).trim();
1585
- if (!rest || rest === 'view') {
1586
- console.log(pc.bold('\n--- Your Profile ---'));
1587
- console.log(` ${pc.cyan('Name')}: ${userProfile.name || '(not set)'}`);
1588
- console.log(` ${pc.cyan('Bio')}: ${userProfile.bio || '(not set)'}`);
1589
- if (userProfile.updatedAt) {
1590
- console.log(pc.gray(` Last updated: ${new Date(userProfile.updatedAt).toLocaleString()}`));
1591
- }
1592
- console.log(pc.dim(' Set name: /profile name = Your Name'));
1593
- console.log(pc.dim(' Set bio: /profile bio = Tell me about yourself'));
1594
- continue;
1595
- }
1596
- if (rest.startsWith('name ')) {
1597
- userProfile.name = rest.substring(5).trim();
1598
- saveProfile(userProfile);
1599
- console.log(pc.green(`[OK] Profile name set: ${userProfile.name}`));
1600
- continue;
1601
- }
1602
- if (rest.startsWith('bio ')) {
1603
- userProfile.bio = rest.substring(4).trim();
1604
- saveProfile(userProfile);
1605
- console.log(pc.green('[OK] Profile bio set.'));
1606
- continue;
1607
- }
1608
- console.log(pc.red('[WARN] Usage: /profile view | /profile name = <name> | /profile bio = <bio>'));
1609
- continue;
1610
- }
1611
- // Command: /style
1612
- if (lowerInput.startsWith('/style')) {
1613
- const rest = trimmedInput.substring(6).trim();
1614
- if (!rest || rest === 'view') {
1615
- console.log(pc.bold('\n--- Coding Style ---'));
1616
- console.log(` ${userProfile.style || '(not set)'}`);
1617
- console.log(pc.dim(' Set: /style <your coding preferences>'));
1618
- console.log(pc.dim(' Example: /style I prefer tabs, functional style, descriptive variable names'));
1619
- continue;
1620
- }
1621
- userProfile.style = rest;
1622
- saveProfile(userProfile);
1623
- console.log(pc.green('[OK] Coding style saved. It will be injected into every session.'));
1624
- continue;
1625
- }
1626
- // Command: /clear
1627
- if (lowerInput === '/clear') {
1628
- messages.length = 0;
1629
- messages.push({ role: 'system', content: getSystemPromptWithMemory() });
1630
- console.log(pc.green('[OK] Conversation history cleared!'));
1631
- continue;
1632
- }
1633
- // Command: /tools
1634
- if (lowerInput === '/tools') {
1635
- console.log(pc.bold('\n--- Available Tools ---'));
1636
- BUILTIN_TOOLS.forEach(t => {
1637
- console.log(` • ${pc.cyan(t.function.name)}: ${t.function.description}`);
1638
- });
1639
- const mcpTools = mcpRegistry.getToolDefinitions();
1640
- if (mcpTools.length > 0) {
1641
- console.log(pc.bold('\n--- MCP Tools ---'));
1642
- mcpTools.forEach(t => {
1643
- console.log(` • ${pc.cyan(t.function.name)}: ${t.function.description}`);
1644
- });
1645
- }
1646
- console.log(pc.bold('-----------------------'));
1647
- continue;
1648
- }
1649
- // Command: /spawn <role> <task>
1650
- if (lowerInput.startsWith('/spawn ')) {
1651
- const parts = trimmedInput.substring(7).trim().split(' ');
1652
- if (parts.length < 2) {
1653
- console.log(pc.red('[WARN] Usage: /spawn <role> <task>'));
1654
- console.log(pc.gray(' Roles: coder, reviewer, debugger, researcher, planner'));
1655
- }
1656
- else {
1657
- const role = parts[0].toLowerCase();
1658
- const task = parts.slice(1).join(' ');
1659
- await handleSpawn(role, task);
1660
- }
1661
- continue;
1662
- }
1663
- // Command: /delegate <task> to <role>
1664
- if (lowerInput.startsWith('/delegate ')) {
1665
- const match = trimmedInput.substring(10).match(/^(.+)\s+to\s+(\w+)$/i);
1666
- if (!match) {
1667
- console.log(pc.red('[WARN] Usage: /delegate <task> to <role>'));
1668
- }
1669
- else {
1670
- const task = match[1].trim();
1671
- const role = match[2].toLowerCase();
1672
- await handleSpawn(role, task);
1673
- }
1674
- continue;
1675
- }
1676
- // Command: /orchestrate <goal>
1677
- if (lowerInput.startsWith('/orchestrate ')) {
1678
- const goal = trimmedInput.substring(13).trim();
1679
- if (!goal) {
1680
- console.log(pc.red('[WARN] Usage: /orchestrate <goal>'));
1681
- }
1682
- else {
1683
- await handleOrchestrate(goal);
1684
- }
1685
- continue;
1686
- }
1687
- // Command: /models
1688
- if (lowerInput === '/models') {
1689
- await handleModels();
1690
- continue;
1691
- }
1692
- // Command: /config
1693
- if (lowerInput === '/config') {
1694
- handleConfig();
1695
- continue;
1696
- }
1697
- // Command: /doctor
1698
- if (lowerInput === '/doctor') {
1699
- await handleDoctor();
1700
- continue;
1701
- }
1702
- // Command: /onboard
1703
- if (lowerInput === '/onboard') {
1704
- console.log(pc.cyan('\n[RESTART] Re-running onboarding wizard...\n'));
1705
- await runOnboarding(true);
1706
- continue;
1707
- }
1708
- // Command: /undo — revert the last patch
1709
- if (lowerInput === '/undo') {
1710
- const history = toolContext.patchHistory;
1711
- if (!history || history.length === 0) {
1712
- console.log(pc.yellow('[WARN] No patches to undo.'));
1713
- }
1714
- else {
1715
- const last = history[history.length - 1];
1716
- try {
1717
- const currentContent = fs.readFileSync(last.filePath, 'utf8');
1718
- if (currentContent === last.newContent) {
1719
- fs.writeFileSync(last.filePath, last.oldContent, 'utf8');
1720
- console.log(pc.green(`[OK] Undid patch to ${last.filePath} (${last.description})`));
1721
- }
1722
- else {
1723
- console.log(pc.yellow(`[WARN] File ${last.filePath} has been modified since last patch. Cannot auto-undo.`));
1724
- }
1725
- history.pop();
1726
- }
1727
- catch (err) {
1728
- console.log(pc.red(`[WARN] Failed to undo: ${err.message}`));
1729
- }
1730
- }
1731
- continue;
1732
- }
1733
- // Command: /branch — view or switch branch
1734
- if (lowerInput === '/branch' || lowerInput.startsWith('/branch ')) {
1735
- try {
1736
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
1737
- const arg = trimmedInput.substring(7).trim();
1738
- if (!arg) {
1739
- const currentBranchResult = await termExec({ command: 'git branch --show-current', timeout: 5, workdir: process.cwd() }, toolContext);
1740
- const current = currentBranchResult.content?.trim();
1741
- if (current) {
1742
- console.log(`\n ${pc.cyan('Current Git branch:')} ${pc.bold(current)}`);
1743
- }
1744
- else {
1745
- console.log(pc.red('\n Not in a Git repository or no branch found.'));
1746
- }
1747
- }
1748
- else {
1749
- console.log(`\n Creating and switching to branch ${pc.cyan(arg)}...`);
1750
- const checkoutResult = await termExec({ command: `git checkout -b ${arg}`, timeout: 10, workdir: process.cwd() }, toolContext);
1751
- if (checkoutResult.success) {
1752
- console.log(pc.green(` [OK] Switched to a new branch '${arg}'`));
1753
- }
1754
- else {
1755
- console.log(pc.yellow(` Branch might already exist, attempting to switch...`));
1756
- const switchResult = await termExec({ command: `git checkout ${arg}`, timeout: 10, workdir: process.cwd() }, toolContext);
1757
- if (switchResult.success) {
1758
- console.log(pc.green(` [OK] Switched to branch '${arg}'`));
1759
- }
1760
- else {
1761
- console.log(pc.red(` Switch failed: ${switchResult.error || switchResult.content}`));
1762
- }
1763
- }
1764
- }
1765
- }
1766
- catch (err) {
1767
- console.log(pc.red(`[WARN] Branch command error: ${err.message}`));
1768
- }
1769
- continue;
1770
- }
1771
- // Command: /pr — generate PR description
1772
- if (lowerInput === '/pr' || lowerInput.startsWith('/pr ')) {
1773
- const arg = trimmedInput.substring(3).trim();
1774
- try {
1775
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
1776
- const gitCheck = await termExec({ command: 'git rev-parse --is-inside-work-tree', timeout: 5, workdir: process.cwd() }, toolContext);
1777
- if (!gitCheck.success) {
1778
- console.log(pc.red(' Error: Not inside a Git repository.'));
1779
- continue;
1780
- }
1781
- let baseBranch = arg || 'main';
1782
- if (!arg) {
1783
- const mainCheck = await termExec({ command: 'git show-ref --verify refs/heads/main', timeout: 5, workdir: process.cwd() }, toolContext);
1784
- if (!mainCheck.success) {
1785
- const masterCheck = await termExec({ command: 'git show-ref --verify refs/heads/master', timeout: 5, workdir: process.cwd() }, toolContext);
1786
- if (masterCheck.success) {
1787
- baseBranch = 'master';
1788
- }
1789
- }
1790
- }
1791
- const currentBranchResult = await termExec({ command: 'git branch --show-current', timeout: 5, workdir: process.cwd() }, toolContext);
1792
- const currentBranch = currentBranchResult.content?.trim();
1793
- console.log(`\n Comparing ${pc.cyan(currentBranch || 'HEAD')} with base branch ${pc.cyan(baseBranch)}...`);
1794
- const commitsResult = await termExec({ command: `git log ${baseBranch}..HEAD --oneline`, timeout: 10, workdir: process.cwd() }, toolContext);
1795
- const commitList = commitsResult.content?.trim() || '';
1796
- const diffResult = await termExec({ command: `git diff ${baseBranch}...HEAD`, timeout: 15, workdir: process.cwd() }, toolContext);
1797
- const diffContent = diffResult.content?.slice(0, 15000) || '';
1798
- if (!commitList && !diffContent) {
1799
- console.log(pc.yellow(` No commits or diff found between ${currentBranch} and ${baseBranch}.`));
1800
- continue;
1801
- }
1802
- console.log(pc.dim(' Analyzing changes and generating PR description...'));
1803
- const aiResponse = await router.chat.completions.create({
1804
- model: 'auto',
1805
- messages: [
1806
- {
1807
- role: 'system',
1808
- content: 'You write clean, comprehensive, professional Pull Request descriptions in Markdown format. Output ONLY the markdown content — no extra chat, wrapper, or quotes.'
1809
- },
1810
- {
1811
- role: 'user',
1812
- content: `Generate a Pull Request description for the current branch compared to ${baseBranch}.\n\nCommits:\n${commitList}\n\nDiff:\n${diffContent}`
1813
- }
1814
- ],
1815
- temperature: 0.3,
1816
- });
1817
- const prDesc = aiResponse.choices[0]?.message?.content?.trim() || '';
1818
- if (!prDesc) {
1819
- console.log(pc.red(' Failed to generate PR description.'));
1820
- continue;
1821
- }
1822
- console.log(pc.bold('\n--- Generated PR Description ---'));
1823
- console.log(prDesc);
1824
- console.log(pc.bold('--------------------------------'));
1825
- const outPath = path.join(process.cwd(), 'pr-desc.md');
1826
- fs.writeFileSync(outPath, prDesc, 'utf8');
1827
- console.log(pc.green(`\n[OK] PR description saved to ${pc.cyan('pr-desc.md')}`));
1828
- }
1829
- catch (err) {
1830
- console.log(pc.red(`[WARN] PR command error: ${err.message}`));
1831
- }
1832
- continue;
1833
- }
1834
- // Command: /debug <command> — run command and autonomously fix errors
1835
- if (lowerInput.startsWith('/debug ')) {
1836
- const debugCmd = trimmedInput.substring(7).trim();
1837
- if (!debugCmd) {
1838
- console.log(pc.red(' Error: Please specify a command to run. Example: /debug npm test'));
1839
- continue;
1840
- }
1841
- console.log(`\n ${pc.cyan('Starting autonomous debugging loop for:')} ${pc.bold(debugCmd)}`);
1842
- const MAX_RETRIES = 5;
1843
- let attempt = 1;
1844
- let success = false;
1845
- while (attempt <= MAX_RETRIES) {
1846
- console.log(`\n ${pc.yellow(`[Attempt ${attempt}/${MAX_RETRIES}]`)} Running: ${pc.bold(debugCmd)}...`);
1847
- try {
1848
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
1849
- const execResult = await termExec({ command: debugCmd, timeout: 60, workdir: process.cwd() }, toolContext);
1850
- if (execResult.success) {
1851
- console.log(pc.green(`\n ${pc.green('✔')} ${pc.bold(`Success on attempt ${attempt}!`)} Command passed with exit code 0.`));
1852
- success = true;
1853
- break;
1854
- }
1855
- console.log(pc.red(`\n ${pc.red('✗')} ${pc.bold(`Command failed on attempt ${attempt}.`)}`));
1856
- const stdout = execResult.content || '';
1857
- const errorMsg = execResult.error || '';
1858
- const logs = `${stdout}\n${errorMsg}`.trim();
1859
- console.log(pc.bold('\n--- Failure Logs ---'));
1860
- const logLines = logs.split('\n');
1861
- const preview = logLines.length > 20 ? logLines.slice(-20).join('\n') : logs;
1862
- console.log(preview);
1863
- if (logLines.length > 20) {
1864
- console.log(pc.dim(`\n (... truncated ${logLines.length - 20} lines of logs ...)`));
1865
- }
1866
- console.log(pc.bold('--------------------'));
1867
- if (attempt === MAX_RETRIES) {
1868
- console.log(pc.red(`\n Reached maximum attempt limit of ${MAX_RETRIES}. Debugging loop failed.`));
1869
- break;
1870
- }
1871
- console.log(pc.dim('\n Calling Daedalus to analyze failure and apply a fix...'));
1872
- const debugPrompt = `The command "${debugCmd}" failed on attempt ${attempt}.
1873
- Here are the execution logs (showing the failure details):
1874
-
1875
- ${logs.slice(-6000)}
1876
-
1877
- Please analyze the error, identify which files need correction, and apply surgical edits using 'patch' or write tools to fix the issue.
1878
- Once you have finished making changes, I will automatically re-run the command to verify if it passes.`;
1879
- await callModelWithTools(debugPrompt);
1880
- }
1881
- catch (err) {
1882
- console.log(pc.red(`\n Error in debugging loop: ${err.message}`));
1883
- break;
1884
- }
1885
- attempt++;
1886
- }
1887
- if (!success) {
1888
- console.log(pc.red(`\n Autonomous debugging did not succeed after ${MAX_RETRIES} attempts.`));
1889
- }
1890
- turnSeparator();
1891
- continue;
1892
- }
1893
- // Command: /ensemble <goal> — run multi-model ensemble drafting pipeline
1894
- if (lowerInput.startsWith('/ensemble ') || lowerInput === '/ensemble') {
1895
- const ensembleGoal = trimmedInput.substring(9).trim();
1896
- if (!ensembleGoal) {
1897
- console.log(pc.red(' Error: Please specify a goal for the ensemble draft. Example: /ensemble Implement feature X'));
1898
- continue;
1899
- }
1900
- try {
1901
- const { runEnsembleWorkflow } = await import('./agents/ensemble.js');
1902
- await runEnsembleWorkflow(ensembleGoal, toolContext, config, router);
1903
- }
1904
- catch (err) {
1905
- console.log(pc.red(`\n Error in ensemble drafting: ${err.message}`));
1906
- }
1907
- turnSeparator();
1908
- continue;
1909
- }
1910
- // Command: /commit — stage and commit changes
1911
- if (lowerInput === '/commit' || lowerInput.startsWith('/commit ')) {
1912
- const forcedMsg = trimmedInput.startsWith('/commit ') ? trimmedInput.substring(8).trim() : '';
1913
- try {
1914
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
1915
- const statusResult = await termExec({ command: 'git status --short', timeout: 10, workdir: process.cwd() }, toolContext);
1916
- console.log(pc.bold('\n--- Git Status ---'));
1917
- console.log(statusResult.content || pc.gray('(clean)'));
1918
- if (!statusResult.content?.trim()) {
1919
- console.log(pc.yellow('Nothing to commit.'));
1920
- continue;
1921
- }
1922
- const addResult = await termExec({ command: 'git add -A', timeout: 10, workdir: process.cwd() }, toolContext);
1923
- if (!addResult.success) {
1924
- console.log(pc.red(`Stage failed: ${addResult.error}`));
1925
- continue;
1926
- }
1927
- let commitMsg = forcedMsg;
1928
- if (!commitMsg) {
1929
- const diffResult = await termExec({ command: 'git diff --cached --stat', timeout: 10, workdir: process.cwd() }, toolContext);
1930
- const diffFull = await termExec({ command: 'git diff --cached', timeout: 10, workdir: process.cwd() }, toolContext);
1931
- const diffContent = diffFull.content?.slice(0, 6000) || '';
1932
- if (diffResult.content)
1933
- console.log(pc.gray(diffResult.content));
1934
- if (diffContent) {
1935
- console.log(pc.dim(' Generating commit message...'));
1936
- try {
1937
- const aiResponse = await router.chat.completions.create({
1938
- model: 'auto',
1939
- messages: [
1940
- { 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.' },
1941
- { role: 'user', content: `Write a commit message for this diff:\n\n${diffContent}` }
1942
- ],
1943
- temperature: 0.2,
1944
- max_tokens: 80,
1945
- });
1946
- const suggested = (aiResponse.choices[0]?.message?.content || '').trim().split('\n')[0].trim();
1947
- if (suggested) {
1948
- console.log(`\n ${pc.dim('Suggested:')} ${pc.cyan(suggested)}`);
1949
- const choice = await askLine(pc.dim(' [Enter] accept [e] edit [n] cancel: '));
1950
- if (choice.trim().toLowerCase() === 'n') {
1951
- console.log(pc.yellow('Commit cancelled.'));
1952
- await termExec({ command: 'git restore --staged .', timeout: 10, workdir: process.cwd() }, toolContext);
1953
- continue;
1954
- }
1955
- else if (choice.trim().toLowerCase() === 'e') {
1956
- commitMsg = await askLine(pc.cyan(' Commit message: '));
1957
- }
1958
- else {
1959
- commitMsg = suggested;
1960
- }
1961
- }
1962
- }
1963
- catch {
1964
- // Model unavailable — fall back to manual
1965
- }
1966
- }
1967
- if (!commitMsg) {
1968
- commitMsg = await askLine(pc.cyan(' Commit message: '));
1969
- }
1970
- if (!commitMsg.trim()) {
1971
- console.log(pc.yellow('Commit cancelled — empty message.'));
1972
- await termExec({ command: 'git restore --staged .', timeout: 10, workdir: process.cwd() }, toolContext);
1973
- continue;
1974
- }
1975
- }
1976
- const commitResult = await termExec({ command: `git commit -m ${JSON.stringify(commitMsg)}`, timeout: 10, workdir: process.cwd() }, toolContext);
1977
- if (commitResult.success) {
1978
- console.log(pc.green(`\n[OK] Commit: ${commitMsg.slice(0, 60)}`));
1979
- }
1980
- else {
1981
- console.log(pc.red(`Commit failed: ${commitResult.error}`));
1982
- }
1983
- }
1984
- catch (err) {
1985
- console.log(pc.red(`[WARN] Commit error: ${err.message}`));
1986
- }
1987
- continue;
1988
- }
1989
- // Command: /project — show/edit project-level config
1990
- if (lowerInput === '/project') {
1991
- const { loadProjectConfig } = await import('./tools/builtin/project-config.js');
1992
- const cfg = loadProjectConfig(process.cwd());
1993
- console.log(pc.bold('\n--- Project Config (.daedalus) ---'));
1994
- console.log(JSON.stringify(cfg, null, 2));
1995
- console.log(pc.bold('----------------------------------'));
1996
- console.log(pc.gray('Use /project set <key> <value> to update'));
1997
- continue;
1998
- }
1999
- if (lowerInput.startsWith('/project set ')) {
2000
- const rest = trimmedInput.substring(13).trim();
2001
- const eqIdx = rest.indexOf('=');
2002
- let key, value;
2003
- if (eqIdx >= 0) {
2004
- key = rest.slice(0, eqIdx).trim();
2005
- value = rest.slice(eqIdx + 1).trim();
2006
- }
2007
- else {
2008
- const parts = rest.split(/\s+/);
2009
- key = parts[0];
2010
- value = parts.slice(1).join(' ');
2011
- }
2012
- if (!key || !value) {
2013
- console.log(pc.red('[WARN] Usage: /project set <key> = <value>'));
2014
- }
2015
- else {
2016
- const { loadProjectConfig, saveProjectConfig } = await import('./tools/builtin/project-config.js');
2017
- const cfg = loadProjectConfig(process.cwd());
2018
- cfg[key] = value;
2019
- saveProjectConfig(cfg);
2020
- console.log(pc.green(`[OK] Set ${key} = ${value}`));
2021
- }
2022
- continue;
2023
- }
2024
- // Command: /test — run tests and auto-fix failures (TDD loop)
2025
- if (lowerInput === '/test' || lowerInput.startsWith('/test ')) {
2026
- const maxLoops = lowerInput.startsWith('/test ') ? parseInt(trimmedInput.substring(6).trim(), 10) || 3 : 3;
2027
- const { loadProjectConfig } = await import('./tools/builtin/project-config.js');
2028
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
2029
- const cfg = loadProjectConfig(process.cwd());
2030
- const testCmd = cfg.testCommand || 'npm test';
2031
- console.log(pc.bold(`\nTest-Run-Fix Loop (max ${maxLoops} iterations)`));
2032
- console.log(pc.gray(`Test command: ${testCmd}\n`));
2033
- for (let i = 0; i < maxLoops; i++) {
2034
- console.log(pc.cyan(`\n--- Run ${i + 1}/${maxLoops} ---`));
2035
- const result = await termExec({ command: testCmd, timeout: 120, workdir: process.cwd() }, toolContext);
2036
- console.log(result.content?.slice(0, 2000) || pc.gray('(no output)'));
2037
- if (result.success) {
2038
- console.log(pc.green('\n[OK] All tests passed!'));
2039
- break;
2040
- }
2041
- if (i === maxLoops - 1) {
2042
- console.log(pc.yellow(`\n[WARN] Max loops (${maxLoops}) reached. Tests still failing.`));
2043
- break;
2044
- }
2045
- 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.`;
2046
- await callModelWithTools(`User Prompt: ${failureCtx}`);
2047
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
2048
- }
2049
- continue;
2050
- }
2051
- // Command: /index [options]
2052
- if (lowerInput.startsWith('/index')) {
2053
- const args = trimmedInput.substring(6).trim().split(/\s+/);
2054
- const opts = {};
2055
- for (const arg of args) {
2056
- if (arg.startsWith('--exclude=')) {
2057
- opts.exclude = arg.split('=')[1].split(',');
2058
- }
2059
- else if (arg.startsWith('--ext=')) {
2060
- opts.extensions = arg.split('=')[1].split(',');
2061
- }
2062
- }
2063
- await handleIndex(opts);
2064
- continue;
2065
- }
2066
- // Command: /find <query> [limit]
2067
- if (lowerInput.startsWith('/find ')) {
2068
- const parts = trimmedInput.substring(6).trim().split(/\s+/);
2069
- if (parts.length === 0) {
2070
- console.log(pc.red('[WARN] Usage: /find <query> [limit]'));
2071
- }
2072
- else {
2073
- const query = parts[0];
2074
- const limit = parts[1] ? parseInt(parts[1], 10) : 30;
2075
- if (isNaN(limit)) {
2076
- console.log(pc.red('[WARN] Invalid limit'));
2077
- }
2078
- else {
2079
- await handleFindSymbol(query, limit);
2080
- }
2081
- }
2082
- continue;
2083
- }
2084
- // Command: /refs <symbol>
2085
- if (lowerInput.startsWith('/refs ')) {
2086
- const symbol = trimmedInput.substring(6).trim();
2087
- if (!symbol) {
2088
- console.log(pc.red('[WARN] Usage: /refs <symbol>'));
2089
- }
2090
- else {
2091
- await handleGetReferences(symbol);
2092
- }
2093
- continue;
2094
- }
2095
- // Command: /def <symbol>
2096
- if (lowerInput.startsWith('/def ')) {
2097
- const symbol = trimmedInput.substring(5).trim();
2098
- if (!symbol) {
2099
- console.log(pc.red('[WARN] Usage: /def <symbol>'));
2100
- }
2101
- else {
2102
- await handleGetDefinition(symbol);
2103
- }
2104
- continue;
2105
- }
2106
- // User Message Processing
2107
- try {
2108
- const filesContext = buildFileContext();
2109
- const indexCtx = await buildIndexContext(trimmedInput);
2110
- const userContent = `${indexCtx}${filesContext}User Prompt: ${trimmedInput}`;
2111
- printUserTurn(trimmedInput);
2112
- await callModelWithTools(userContent);
2113
- // Persist session incrementally after every turn
2114
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
2115
- // Fact extraction — learns from tool results and reasoning
2116
- await extractAndSave(router, sessionManager, messages);
2117
- }
2118
- catch (error) {
2119
- try {
2120
- const filesContext = buildFileContext();
2121
- const userContent = `${filesContext}User Prompt: ${trimmedInput}`;
2122
- console.log(pc.yellow('\n [RETRY] Trying fallback mode...'));
2123
- const fallbackResult = await callModelWithFallback(userContent);
2124
- if (fallbackResult) {
2125
- // Persist session after fallback too
2126
- sessionManager.saveSessionState(messages, activeFiles, getSessionTodos(sessionId));
2127
- await extractAndSave(router, sessionManager, messages);
2128
- }
2129
- }
2130
- catch (fallbackErr) {
2131
- const firstLine = (fallbackErr.message || '').split('\n')[0];
2132
- console.log(pc.red(`\n ${pc.bold('[ERROR]')} Fallback also failed: ${firstLine}`));
2133
- console.log(pc.dim(' Check that at least one local server is running or run /doctor to debug.'));
2134
- }
2135
- }
2136
- turnSeparator();
2137
- }
203
+ function getIndexDbPath() {
204
+ return path.join(os.homedir(), '.daedalus', 'indexing', `${projectHash}.sqlite`);
2138
205
  }
206
+ const { callModelWithTools, callModelWithFallback } = createModelFunctions({
207
+ messages,
208
+ config,
209
+ router,
210
+ toolContext,
211
+ buildFileContext,
212
+ askLine,
213
+ });
214
+ // Create REPL loop
215
+ const chatLoop = createRepl({
216
+ config,
217
+ configDir,
218
+ cliTempDir,
219
+ router,
220
+ sessionManager,
221
+ userProfile,
222
+ projectHash,
223
+ messages,
224
+ activeFiles,
225
+ toolContext,
226
+ getSystemPromptWithMemory,
227
+ callModelWithTools,
228
+ callModelWithFallback,
229
+ getIndexDbPath,
230
+ });
2139
231
  // Start health checks and REPL
2140
232
  async function main() {
2141
233
  process.on('SIGINT', () => {
234
+ if (indexWatcher) {
235
+ indexWatcher.close();
236
+ }
2142
237
  if (currentAbortController) {
2143
238
  currentAbortController.abort();
2144
239
  return;
@@ -2151,12 +246,16 @@ async function main() {
2151
246
  });
2152
247
  // Clean up health checks on normal exit too
2153
248
  process.on('exit', () => {
249
+ if (indexWatcher) {
250
+ indexWatcher.close();
251
+ }
2154
252
  router.stopHealthChecks?.();
2155
253
  import('./tools/builtin/process-watcher.js').then(m => m.killAllWatchedProcesses()).catch(() => { });
2156
254
  });
2157
255
  // Print the awesome banner first!
2158
- printBanner();
2159
- printConfigInfo();
256
+ printBanner(APP_VERSION);
257
+ const enabledCount = config.router.chain.filter(m => m.enabled).length;
258
+ printConfigInfo(enabledCount, config.router.strategy, configDir + '/config.json');
2160
259
  try {
2161
260
  await router.startHealthChecks();
2162
261
  console.log(pc.green('\n[OK] Router started. Health checks running every 30s.'));
@@ -2179,7 +278,8 @@ async function main() {
2179
278
  }
2180
279
  // Check for updates — non-blocking
2181
280
  if (config.updateCheck !== false) {
2182
- setTimeout(() => checkForUpdates(), 2000);
281
+ const updateCachePath = path.join(cliTempDir, 'version-check.json');
282
+ setTimeout(() => checkForUpdates(APP_VERSION, updateCachePath), 2000);
2183
283
  }
2184
284
  if (config.indexing.enabled) {
2185
285
  setTimeout(() => {
@@ -2202,6 +302,12 @@ async function main() {
2202
302
  console.log(pc.yellow(` [WARN] ${result.errors.length} file(s) had index errors`));
2203
303
  }
2204
304
  toolContext.indexDb = db;
305
+ if (config.indexing.watch) {
306
+ const { watchCodebase } = await import('./indexing/watcher.js');
307
+ indexWatcher = watchCodebase(db, process.cwd(), projectHash, {
308
+ exclude: config.indexing.exclude,
309
+ });
310
+ }
2205
311
  }
2206
312
  catch (err) {
2207
313
  console.error(pc.yellow(` [WARN] Auto-index failed: ${err.message}`));