daedalus-cli 0.4.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 (144) hide show
  1. package/Daedalus.bat +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +254 -0
  4. package/dist/agents/agent.d.ts +10 -0
  5. package/dist/agents/agent.d.ts.map +1 -0
  6. package/dist/agents/agent.js +3 -0
  7. package/dist/agents/agent.js.map +1 -0
  8. package/dist/agents/orchestrator.d.ts +18 -0
  9. package/dist/agents/orchestrator.d.ts.map +1 -0
  10. package/dist/agents/orchestrator.js +171 -0
  11. package/dist/agents/orchestrator.js.map +1 -0
  12. package/dist/agents/roles.d.ts +14 -0
  13. package/dist/agents/roles.d.ts.map +1 -0
  14. package/dist/agents/roles.js +126 -0
  15. package/dist/agents/roles.js.map +1 -0
  16. package/dist/config/index.d.ts +485 -0
  17. package/dist/config/index.d.ts.map +1 -0
  18. package/dist/config/index.js +237 -0
  19. package/dist/config/index.js.map +1 -0
  20. package/dist/highlight.d.ts +4 -0
  21. package/dist/highlight.d.ts.map +1 -0
  22. package/dist/highlight.js +42 -0
  23. package/dist/highlight.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +1386 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/indexing/fts.d.ts +40 -0
  29. package/dist/indexing/fts.d.ts.map +1 -0
  30. package/dist/indexing/fts.js +121 -0
  31. package/dist/indexing/fts.js.map +1 -0
  32. package/dist/indexing/indexer.d.ts +22 -0
  33. package/dist/indexing/indexer.d.ts.map +1 -0
  34. package/dist/indexing/indexer.js +518 -0
  35. package/dist/indexing/indexer.js.map +1 -0
  36. package/dist/onboarding/wizard.d.ts +2 -0
  37. package/dist/onboarding/wizard.d.ts.map +1 -0
  38. package/dist/onboarding/wizard.js +231 -0
  39. package/dist/onboarding/wizard.js.map +1 -0
  40. package/dist/router/health.d.ts +6 -0
  41. package/dist/router/health.d.ts.map +1 -0
  42. package/dist/router/health.js +74 -0
  43. package/dist/router/health.js.map +1 -0
  44. package/dist/router/index.d.ts +33 -0
  45. package/dist/router/index.d.ts.map +1 -0
  46. package/dist/router/index.js +214 -0
  47. package/dist/router/index.js.map +1 -0
  48. package/dist/router/rate-limiter.d.ts +7 -0
  49. package/dist/router/rate-limiter.d.ts.map +1 -0
  50. package/dist/router/rate-limiter.js +36 -0
  51. package/dist/router/rate-limiter.js.map +1 -0
  52. package/dist/router/types.d.ts +81 -0
  53. package/dist/router/types.d.ts.map +1 -0
  54. package/dist/router/types.js +3 -0
  55. package/dist/router/types.js.map +1 -0
  56. package/dist/session/jsonl.d.ts +20 -0
  57. package/dist/session/jsonl.d.ts.map +1 -0
  58. package/dist/session/jsonl.js +61 -0
  59. package/dist/session/jsonl.js.map +1 -0
  60. package/dist/session/manager.d.ts +42 -0
  61. package/dist/session/manager.d.ts.map +1 -0
  62. package/dist/session/manager.js +184 -0
  63. package/dist/session/manager.js.map +1 -0
  64. package/dist/session/memory.d.ts +23 -0
  65. package/dist/session/memory.d.ts.map +1 -0
  66. package/dist/session/memory.js +88 -0
  67. package/dist/session/memory.js.map +1 -0
  68. package/dist/session/sqlite.d.ts +59 -0
  69. package/dist/session/sqlite.d.ts.map +1 -0
  70. package/dist/session/sqlite.js +174 -0
  71. package/dist/session/sqlite.js.map +1 -0
  72. package/dist/tools/builtin/delegation.d.ts +17 -0
  73. package/dist/tools/builtin/delegation.d.ts.map +1 -0
  74. package/dist/tools/builtin/delegation.js +85 -0
  75. package/dist/tools/builtin/delegation.js.map +1 -0
  76. package/dist/tools/builtin/diff-ui.d.ts +21 -0
  77. package/dist/tools/builtin/diff-ui.d.ts.map +1 -0
  78. package/dist/tools/builtin/diff-ui.js +211 -0
  79. package/dist/tools/builtin/diff-ui.js.map +1 -0
  80. package/dist/tools/builtin/files.d.ts +29 -0
  81. package/dist/tools/builtin/files.d.ts.map +1 -0
  82. package/dist/tools/builtin/files.js +286 -0
  83. package/dist/tools/builtin/files.js.map +1 -0
  84. package/dist/tools/builtin/git.d.ts +7 -0
  85. package/dist/tools/builtin/git.d.ts.map +1 -0
  86. package/dist/tools/builtin/git.js +11 -0
  87. package/dist/tools/builtin/git.js.map +1 -0
  88. package/dist/tools/builtin/indexing.d.ts +22 -0
  89. package/dist/tools/builtin/indexing.d.ts.map +1 -0
  90. package/dist/tools/builtin/indexing.js +159 -0
  91. package/dist/tools/builtin/indexing.js.map +1 -0
  92. package/dist/tools/builtin/project-config.d.ts +17 -0
  93. package/dist/tools/builtin/project-config.d.ts.map +1 -0
  94. package/dist/tools/builtin/project-config.js +66 -0
  95. package/dist/tools/builtin/project-config.js.map +1 -0
  96. package/dist/tools/builtin/terminal.d.ts +7 -0
  97. package/dist/tools/builtin/terminal.d.ts.map +1 -0
  98. package/dist/tools/builtin/terminal.js +99 -0
  99. package/dist/tools/builtin/terminal.js.map +1 -0
  100. package/dist/tools/builtin/todo.d.ts +20 -0
  101. package/dist/tools/builtin/todo.d.ts.map +1 -0
  102. package/dist/tools/builtin/todo.js +36 -0
  103. package/dist/tools/builtin/todo.js.map +1 -0
  104. package/dist/tools/builtin/web.d.ts +10 -0
  105. package/dist/tools/builtin/web.d.ts.map +1 -0
  106. package/dist/tools/builtin/web.js +67 -0
  107. package/dist/tools/builtin/web.js.map +1 -0
  108. package/dist/tools/daedalus-spinner.d.ts +29 -0
  109. package/dist/tools/daedalus-spinner.d.ts.map +1 -0
  110. package/dist/tools/daedalus-spinner.js +77 -0
  111. package/dist/tools/daedalus-spinner.js.map +1 -0
  112. package/dist/tools/definitions.d.ts +5 -0
  113. package/dist/tools/definitions.d.ts.map +1 -0
  114. package/dist/tools/definitions.js +296 -0
  115. package/dist/tools/definitions.js.map +1 -0
  116. package/dist/tools/executor.d.ts +4 -0
  117. package/dist/tools/executor.d.ts.map +1 -0
  118. package/dist/tools/executor.js +86 -0
  119. package/dist/tools/executor.js.map +1 -0
  120. package/dist/tools/mcp/http.d.ts +23 -0
  121. package/dist/tools/mcp/http.d.ts.map +1 -0
  122. package/dist/tools/mcp/http.js +200 -0
  123. package/dist/tools/mcp/http.js.map +1 -0
  124. package/dist/tools/mcp/registry.d.ts +16 -0
  125. package/dist/tools/mcp/registry.d.ts.map +1 -0
  126. package/dist/tools/mcp/registry.js +92 -0
  127. package/dist/tools/mcp/registry.js.map +1 -0
  128. package/dist/tools/mcp/stdio.d.ts +26 -0
  129. package/dist/tools/mcp/stdio.d.ts.map +1 -0
  130. package/dist/tools/mcp/stdio.js +157 -0
  131. package/dist/tools/mcp/stdio.js.map +1 -0
  132. package/dist/tools/mcp/tool-executor.d.ts +3 -0
  133. package/dist/tools/mcp/tool-executor.d.ts.map +1 -0
  134. package/dist/tools/mcp/tool-executor.js +23 -0
  135. package/dist/tools/mcp/tool-executor.js.map +1 -0
  136. package/dist/tools/mcp/types.d.ts +26 -0
  137. package/dist/tools/mcp/types.d.ts.map +1 -0
  138. package/dist/tools/mcp/types.js +3 -0
  139. package/dist/tools/mcp/types.js.map +1 -0
  140. package/dist/types.d.ts +58 -0
  141. package/dist/types.d.ts.map +1 -0
  142. package/dist/types.js +3 -0
  143. package/dist/types.js.map +1 -0
  144. package/package.json +63 -0
package/dist/index.js ADDED
@@ -0,0 +1,1386 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import readline from 'readline';
6
+ import os from 'os';
7
+ import pc from 'picocolors';
8
+ import { BUILTIN_TOOLS } from './tools/definitions.js';
9
+ import { executeToolCalls } from './tools/executor.js';
10
+ import { setRouterClient } from './tools/builtin/delegation.js';
11
+ import { mcpRegistry } from './tools/mcp/registry.js';
12
+ import { createRouter } from './router/index.js';
13
+ import { loadConfig, getConfigDirPath, discoverLocalServers } from './config/index.js';
14
+ import crypto from 'crypto';
15
+ import { getSessionTodos, setSessionTodos } from './tools/builtin/todo.js';
16
+ import { searchSymbols as ftsSearch } from './indexing/fts.js';
17
+ import { SessionManager } from './session/manager.js';
18
+ import { runOnboarding } from './onboarding/wizard.js';
19
+ import { DaedalusSpinner } from './tools/daedalus-spinner.js';
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+ // Load configuration
23
+ const config = loadConfig();
24
+ const configDir = getConfigDirPath();
25
+ // Session state
26
+ const activeFiles = new Map(); // Absolute path -> filename key
27
+ const messages = [];
28
+ // Token tracking for current turn
29
+ let currentTurnInputTokens = 0;
30
+ let currentTurnOutputTokens = 0;
31
+ let turnStartTime = 0;
32
+ let currentAbortController = null;
33
+ // Compute stable projectHash once
34
+ const projectHash = crypto.createHash('sha256').update(path.resolve(process.cwd())).digest('hex').slice(0, 12);
35
+ // Initialize session manager
36
+ const sessionManager = new SessionManager();
37
+ sessionManager.init();
38
+ const initialSession = sessionManager.startSession();
39
+ let sessionId = initialSession.sessionId;
40
+ // If there are turns from the loaded session, restore them (skip system prompt ones)
41
+ if (initialSession.turns.length > 0) {
42
+ const nonSystemTurns = initialSession.turns.filter(t => t.role !== 'system');
43
+ messages.push(...nonSystemTurns);
44
+ }
45
+ // Restore active files from session
46
+ if (initialSession.activeFiles.size > 0) {
47
+ for (const [k, v] of initialSession.activeFiles.entries()) {
48
+ activeFiles.set(k, v);
49
+ }
50
+ }
51
+ // Restore todos from session
52
+ if (initialSession.todos.length > 0) {
53
+ setSessionTodos(sessionId, initialSession.todos);
54
+ }
55
+ // Ensure CLI temp directory exists
56
+ const cliTempDir = path.join(os.homedir(), '.daedalus', 'temp');
57
+ if (!fs.existsSync(cliTempDir)) {
58
+ fs.mkdirSync(cliTempDir, { recursive: true });
59
+ }
60
+ // Initialize local router
61
+ const router = createRouter(config.router);
62
+ // Tool context for executions
63
+ const toolContext = {
64
+ sessionId,
65
+ projectRoot: process.cwd(),
66
+ projectHash,
67
+ activeFiles,
68
+ agentRole: config.agents.default,
69
+ abortSignal: new AbortController().signal,
70
+ autoApplyEdits: 'prompt',
71
+ patchHistory: [],
72
+ pauseSpinner: () => { },
73
+ resumeSpinner: () => { },
74
+ };
75
+ // Enable delegation tool
76
+ setRouterClient(router);
77
+ // Default system prompt
78
+ const systemPrompt = `You are Daedalus, an expert software developer and coding assistant.
79
+ You run locally on the user's machine with an embedded model router.
80
+ You have access to local LLM servers (LM Studio, Ollama, llama.cpp, vLLM) and a full toolset.
81
+
82
+ Your goal: help the user modify their codebase efficiently. Speed and precision matter.
83
+
84
+ ## CODEBASE INDEX (FTS5) — always available
85
+ A FTS5 symbol index is maintained automatically. The following tools let you search it:
86
+ - \`find_symbol(query, limit)\` — fuzzy search functions, classes, types across the project
87
+ - \`get_definition(name)\` — exact lookup returning file path, line range, and signature
88
+ - \`get_references(name)\` — show every call-site referencing a symbol (call graph)
89
+ - \`index_codebase(exclude, extensions)\` — manually trigger a re-index (usually automatic)
90
+
91
+ The index context is automatically injected before each user turn. When working on a task, check it first for relevant symbols before reading files.
92
+
93
+ ## CRITICAL TOOL RULES
94
+
95
+ ### Editing existing files — ALWAYS use patch, NEVER write_file
96
+ - ALWAYS use \`patch\` to modify existing files. NEVER use \`write_file\` on a file that already exists.
97
+ - \`write_file\` is ONLY for creating brand-new files that do not yet exist on disk.
98
+ - Rewriting an entire file with \`write_file\` when only a few lines need changing is a serious mistake.
99
+
100
+ ### patch best practices
101
+ - Your \`old_string\` must be the EXACT text from the file — same indentation, same spacing.
102
+ - Use read_file first if you are not 100% certain of the exact text. Do not guess.
103
+ - Make \`old_string\` as short as possible while still being unique (3-10 lines is ideal).
104
+ - If patch fails with "not found", immediately use read_file to verify the exact text, then retry.
105
+ - CRLF note: files on Windows may use CRLF line endings. The patch tool handles this automatically — always write your strings with plain \\n and the tool will match correctly.
106
+
107
+ ### Before any edit
108
+ 1. If you have not read the file yet this turn, use \`read_file\` to verify the current content.
109
+ 2. Identify the smallest possible change (the fewest lines to replace).
110
+ 3. Use \`patch\` with that minimal old_string → new_string.
111
+
112
+ ### Tool selection guide
113
+ | Goal | Use |
114
+ |------|-----|
115
+ | Read part of a file | \`read_file\` with offset+limit |
116
+ | Make a surgical edit | \`patch\` |
117
+ | Create a new file | \`write_file\` |
118
+ | Find where something is | \`search_files\` |
119
+ | Search code symbols | \`find_symbol\` (FTS5 fuzzy search) |
120
+ | Look up a definition | \`get_definition\` (exact name) |
121
+ | Find callers | \`get_references\` (call-graph) |
122
+ | Index the codebase | \`index_codebase\` (automatic on startup) |
123
+ | Run a build/test/script | \`terminal\` |
124
+ | Track multi-step work | \`todo\` |
125
+
126
+ ## CODEBASE INDEX
127
+ A FTS5 symbol index is built automatically on startup. Use \`find_symbol\` to search classes, functions, interfaces, types across the project. Use \`get_definition\` to pinpoint a symbol's file and line. Use \`get_references\` to see the call graph. The index is incremental (SHA-based) so re-indexing is fast.
128
+
129
+ ## EFFICIENCY RULES
130
+ - Batch related patches: if you need to change 3 functions in the same file, do them in 3 sequential patch calls — not 3 reads.
131
+ - Do NOT re-read a file you just read unless the content changed.
132
+ - If a task has more than 3 steps, create a todo list first so you can track progress without losing context.
133
+ - Be concise in responses — the user can see the tool check-ins. Skip narrating each step.
134
+
135
+ ## PATCH OUTCOMES — what to do in each case
136
+
137
+ | Result | Meaning | What YOU must do |
138
+ |--------|---------|-----------------|
139
+ | \`Patched <file>\` | ✅ Success — change written to disk | Continue to next step |
140
+ | error contains \`PATCH_DECLINED\` | 🚫 User reviewed the diff and said No or Skip | STOP retrying. Tell the user what you tried to change and ask how they'd like to proceed |
141
+ | error contains \`not found\` | ❌ old_string didn't match the file | Immediately call \`read_file\` on that file, find the exact text, then retry \`patch\` with the corrected old_string |
142
+ | error contains \`multiple locations\` | ❌ old_string is too generic | Add more surrounding lines to old_string to make it unique, then retry |
143
+ | error contains \`File not found\` | ❌ Wrong path | Use \`search_files\` or \`list_files\` to find the correct path |
144
+
145
+ **Never freeze or loop silently.** If a patch fails, take one corrective action and tell the user what happened.`;
146
+ // Build system prompt with project memory
147
+ function getSystemPromptWithMemory() {
148
+ let prompt = systemPrompt;
149
+ const memPrompt = sessionManager.getMemoryPrompt();
150
+ if (memPrompt) {
151
+ prompt += '\n' + memPrompt;
152
+ }
153
+ return prompt;
154
+ }
155
+ messages.push({ role: 'system', content: getSystemPromptWithMemory() });
156
+ // Setup Readline Interface with tab completion
157
+ const COMMANDS = [
158
+ '/add', '/remove', '/context', '/clear',
159
+ '/spawn', '/delegate', '/orchestrate',
160
+ '/memory', '/fact', '/convention',
161
+ '/index', '/find', '/refs', '/def',
162
+ '/commit', '/undo', '/test',
163
+ '/models', '/tools', '/config', '/project', '/doctor', '/session', '/onboard',
164
+ '/help', 'exit', 'quit', '?',
165
+ ];
166
+ const rl = readline.createInterface({
167
+ input: process.stdin,
168
+ output: process.stdout,
169
+ completer: (line) => {
170
+ const prefix = line.toLowerCase();
171
+ const hits = prefix.startsWith('/') || prefix.startsWith('?') || prefix.startsWith('exit') || prefix.startsWith('quit')
172
+ ? COMMANDS.filter(c => c.startsWith(prefix))
173
+ : [];
174
+ return [hits.length ? hits : COMMANDS, prefix];
175
+ },
176
+ });
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ // B A N N E R
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+ // Each row of the block-letter art, rendered as segments so we can colour
181
+ // the left half cyan and the right half white for a pseudo-gradient effect.
182
+ const LOGO_ROWS = [
183
+ ' ██████╗ █████╗ ███████╗ ██████╗ █████╗ ██╗ ██╗ ██╗ ███████╗ ',
184
+ ' ██╔══██╗ ██╔══██╗ ██╔════╝ ██╔══██╗ ██╔══██╗ ██║ ██║ ██║ ██╔════╝ ',
185
+ ' ██║ ██║ ███████║ █████╗ ██║ ██║ ███████║ ██║ ██║ ██║ ███████╗ ',
186
+ ' ██║ ██║ ██╔══██║ ██╔══╝ ██║ ██║ ██╔══██║ ██║ ██║ ██║ ╚════██║ ',
187
+ ' ██████╔╝ ██║ ██║ ███████╗ ██████╔╝ ██║ ██║ ███████╗╚██████╔╝ ███████║ ',
188
+ ' ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝ ',
189
+ ];
190
+ const W = 76; // inner width (between the border pipes)
191
+ function box(inner, borderColor) {
192
+ return borderColor('║') + inner + borderColor('║');
193
+ }
194
+ function hRule(l, r, fill, len, color) {
195
+ return color(l + fill.repeat(len) + r);
196
+ }
197
+ function centred(text, width, color) {
198
+ const pad = Math.max(0, width - text.length);
199
+ const left = Math.floor(pad / 2);
200
+ const right = pad - left;
201
+ return ' '.repeat(left) + color(text) + ' '.repeat(right);
202
+ }
203
+ function printBanner() {
204
+ const cyan = pc.cyan.bind(pc);
205
+ const white = pc.white.bind(pc);
206
+ const bold = pc.bold.bind(pc);
207
+ const dim = pc.dim.bind(pc);
208
+ const mag = pc.magenta.bind(pc);
209
+ // Top border
210
+ console.log(hRule('╔', '╗', '═', W, cyan));
211
+ console.log(box(' '.repeat(W), cyan));
212
+ // Logo rows — left half cyan, right half white, fading across
213
+ LOGO_ROWS.forEach((row, i) => {
214
+ const mid = Math.floor(row.length * (0.4 + i * 0.04)); // gradient cut shifts right each row
215
+ const left = cyan(row.slice(0, mid));
216
+ const right = white(row.slice(mid));
217
+ const inner = left + right;
218
+ // Pad to exactly W chars
219
+ const visLen = row.length;
220
+ const padded = inner + ' '.repeat(Math.max(0, W - visLen));
221
+ console.log(box(padded, cyan));
222
+ });
223
+ console.log(box(' '.repeat(W), cyan));
224
+ // Tagline strip
225
+ const tagline = '⬡ local-first · embedded router · multi-agent · mcp-ready ⬡';
226
+ console.log(box(centred(tagline, W, dim), cyan));
227
+ // Bottom border
228
+ console.log(hRule('╚', '╝', '═', W, cyan));
229
+ // Version / author badge — pill style
230
+ const badge = ` v0.2.0-beta`;
231
+ const author = `bgill55_dev `;
232
+ const divider = ` · `;
233
+ console.log('');
234
+ console.log(' ' +
235
+ pc.bgCyan(pc.black(bold(` DAEDALUS `))) +
236
+ pc.bgBlack(pc.cyan(bold(badge))) +
237
+ pc.bgBlack(dim(divider)) +
238
+ pc.bgBlack(pc.white(author)));
239
+ console.log('');
240
+ }
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+ // S T A R T U P I N F O
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+ function printConfigInfo() {
245
+ const enabledCount = config.router.chain.filter(m => m.enabled).length;
246
+ const strategy = config.router.strategy;
247
+ const configPath = configDir + '/config.json';
248
+ const contentLine = `strategy ${strategy} models ${enabledCount} config ${configPath}`;
249
+ const boxInnerWidth = contentLine.length + 2; // padding inside
250
+ const top = pc.dim(' ┌─ ') + pc.cyan('router') + pc.dim(' ' + '─'.repeat(Math.max(0, boxInnerWidth - 6)) + '┐');
251
+ const mid = pc.dim(' │ ') + pc.white(contentLine) + pc.dim(' │');
252
+ const bot = pc.dim(' └' + '─'.repeat(boxInnerWidth + 2) + '┘');
253
+ console.log(top);
254
+ console.log(mid);
255
+ console.log(bot);
256
+ console.log('');
257
+ // ── Quick tip ────────────────────────────────────────────────────────────
258
+ console.log(` ${pc.dim('Type')} ${pc.cyan('?')} ${pc.dim('for commands · Tab completes')}`);
259
+ console.log('');
260
+ }
261
+ // Parse initial arguments (e.g. if started as `daedalus src/index.ts`)
262
+ const initialArgs = process.argv.slice(2);
263
+ if (initialArgs.length > 0) {
264
+ initialArgs.forEach(fileArg => {
265
+ const absPath = path.resolve(fileArg);
266
+ activeFiles.set(absPath, fileArg);
267
+ toolContext.activeFiles = new Map(activeFiles);
268
+ console.log(pc.green(`✔ Added file on startup: ${pc.bold(fileArg)}`));
269
+ });
270
+ }
271
+ // Build file context for LLM
272
+ function buildFileContext() {
273
+ if (activeFiles.size === 0)
274
+ return '';
275
+ let ctx = '--- ACTIVE FILES CONTEXT ---\n';
276
+ for (const [absPath, filename] of activeFiles) {
277
+ let content = '';
278
+ if (fs.existsSync(absPath)) {
279
+ content = fs.readFileSync(absPath, 'utf8');
280
+ }
281
+ else {
282
+ content = '[New file - does not exist yet]';
283
+ }
284
+ ctx += `[File: ${filename}]\n\`\`\`\n${content}\n\`\`\`\n\n`;
285
+ }
286
+ ctx += '----------------------------\n\n';
287
+ return ctx;
288
+ }
289
+ // Build index context — auto-inject relevant symbols from FTS5 index
290
+ async function buildIndexContext(userMessage) {
291
+ if (!config.indexing.enabled || !toolContext.indexDb)
292
+ return '';
293
+ const indexDb = toolContext.indexDb;
294
+ // Extract likely symbol names from the user message (camelCase, snake_case, class names)
295
+ const symbolCandidates = userMessage.match(/\b[A-Z][a-zA-Z0-9_]*\b/g) || [];
296
+ const words = userMessage.split(/\s+/).filter(w => w.length > 2);
297
+ const allTerms = [...symbolCandidates, ...words];
298
+ if (allTerms.length === 0)
299
+ return '';
300
+ let ctx = '\n--- RELEVANT CODE SYMBOLS (from FTS5 index) ---\n';
301
+ let count = 0;
302
+ const seen = new Set();
303
+ try {
304
+ for (const term of allTerms) {
305
+ if (count >= 8)
306
+ break;
307
+ const results = ftsSearch(indexDb, term, projectHash, 3);
308
+ for (const s of results) {
309
+ const key = `${s.name}:${s.file_path}`;
310
+ if (seen.has(key))
311
+ continue;
312
+ seen.add(key);
313
+ ctx += ` [${s.kind}] ${s.name} → ${s.file_path}:${s.line_start}`;
314
+ if (s.signature)
315
+ ctx += ` (${s.signature.slice(0, 80)})`;
316
+ ctx += '\n';
317
+ count++;
318
+ }
319
+ }
320
+ }
321
+ catch {
322
+ // Index not available — skip
323
+ }
324
+ if (count === 0)
325
+ return '';
326
+ ctx += '------------------------------------------\n\n';
327
+ return ctx;
328
+ }
329
+ // Initialize session state from a loaded session snapshot
330
+ function initializeSessionState(loaded) {
331
+ sessionId = loaded.sessionId;
332
+ toolContext.sessionId = loaded.sessionId;
333
+ activeFiles.clear();
334
+ for (const [k, v] of loaded.activeFiles.entries()) {
335
+ activeFiles.set(k, v);
336
+ }
337
+ toolContext.activeFiles = new Map(activeFiles);
338
+ messages.length = 0;
339
+ const sysPrompt = getSystemPromptWithMemory();
340
+ messages.push({ role: 'system', content: sysPrompt });
341
+ if (loaded.turns.length > 0) {
342
+ const userOrAssistantTurns = loaded.turns.filter(t => t.role !== 'system');
343
+ messages.push(...userOrAssistantTurns);
344
+ }
345
+ setSessionTodos(loaded.sessionId, loaded.todos);
346
+ console.log(pc.gray(`Active files in context: ${activeFiles.size}`));
347
+ console.log(pc.gray(`Loaded ${loaded.turns.length} message turn(s)`));
348
+ }
349
+ // Handle /spawn and /delegate commands
350
+ async function handleSpawn(role, task) {
351
+ const validRoles = ['coder', 'reviewer', 'debugger', 'researcher', 'planner'];
352
+ if (!validRoles.includes(role)) {
353
+ console.log(pc.red(`⚠ Unknown role: ${role}. Valid: ${validRoles.join(', ')}`));
354
+ return;
355
+ }
356
+ console.log(pc.cyan(`\n🤝 Spawning ${role} agent for: ${task.slice(0, 80)}...`));
357
+ const context = `Active files: ${Array.from(activeFiles.values()).join(', ') || 'none'}`;
358
+ const fakeToolCall = {
359
+ id: `call_${Date.now()}`,
360
+ type: 'function',
361
+ function: {
362
+ name: 'delegate_task',
363
+ arguments: JSON.stringify({ goal: task, context, role }),
364
+ },
365
+ };
366
+ const results = await executeToolCalls([fakeToolCall], toolContext);
367
+ for (const result of results) {
368
+ const status = result.success ? pc.green('✔') : pc.red('✗');
369
+ console.log(`\n${status} ${role} agent completed`);
370
+ console.log(pc.white(result.content));
371
+ if (!result.success && result.error) {
372
+ console.log(pc.red(`Error: ${result.error}`));
373
+ }
374
+ }
375
+ }
376
+ // Handle /orchestrate command
377
+ async function handleOrchestrate(goal) {
378
+ console.log(pc.cyan(`\n🎭 Starting orchestration for: ${goal}`));
379
+ const { Orchestrator } = await import('./agents/orchestrator.js');
380
+ const orchestrator = new Orchestrator(router, messages, toolContext);
381
+ const result = await orchestrator.run(goal);
382
+ console.log(pc.white(`\n${result}`));
383
+ }
384
+ // Handle /models command
385
+ async function handleModels() {
386
+ console.log(pc.bold('\n--- Available Models ---'));
387
+ const models = await router.listModels();
388
+ if (models.length === 0) {
389
+ console.log(pc.yellow(' No models found. Check your local servers (LM Studio, Ollama, etc.)'));
390
+ }
391
+ else {
392
+ for (const model of models) {
393
+ console.log(` • ${pc.cyan(model)}`);
394
+ }
395
+ }
396
+ const { checkModelHealth } = await import('./router/health.js');
397
+ const healthyModels = router.getHealthyModels();
398
+ console.log(pc.bold('\n--- Healthy Models ---'));
399
+ for (const model of healthyModels) {
400
+ const health = await checkModelHealth(model, 5000);
401
+ const status = health?.healthy ? pc.green('●') : pc.red('●');
402
+ console.log(` ${status} ${pc.cyan(model.name)} (${model.endpoint}) - ${model.model}`);
403
+ }
404
+ console.log(pc.bold('----------------------\n'));
405
+ }
406
+ // Handle /config command
407
+ function handleConfig() {
408
+ // Max chars of tool output stored in message history (prevents context-window overflow)
409
+ const TOOL_RESULT_MAX_CHARS = 32_000;
410
+ console.log(pc.bold('\n--- Current Configuration ---'));
411
+ console.log(JSON.stringify(config, null, 2));
412
+ console.log(pc.bold('-----------------------------'));
413
+ console.log(pc.gray(`\nEdit ${configDir}/config.json to modify settings.`));
414
+ console.log(pc.gray('Run `daedalus /doctor` to auto-discover local servers.'));
415
+ }
416
+ // Handle /doctor command
417
+ async function handleDoctor() {
418
+ console.log(pc.bold('\n--- Daedalus Doctor ---'));
419
+ console.log(pc.gray('Checking local server connections...\n'));
420
+ const discovered = await discoverLocalServers();
421
+ if (discovered.length === 0) {
422
+ console.log(pc.yellow(' No local servers detected.'));
423
+ console.log(pc.gray(' Start one of:'));
424
+ console.log(pc.gray(' • LM Studio (http://localhost:1234)'));
425
+ console.log(pc.gray(' • Ollama (http://localhost:11434)'));
426
+ console.log(pc.gray(' • llama.cpp server (--server, default :8080)'));
427
+ console.log(pc.gray(' • vLLM (http://localhost:8000)'));
428
+ }
429
+ else {
430
+ console.log(pc.green(` Found ${discovered.length} running server(s):\n`));
431
+ for (const server of discovered) {
432
+ console.log(` ${pc.green('●')} ${server.name} at ${server.endpoint}`);
433
+ for (const model of server.models.slice(0, 5)) {
434
+ console.log(` - ${model}`);
435
+ }
436
+ if (server.models.length > 5) {
437
+ console.log(pc.gray(` ... and ${server.models.length - 5} more`));
438
+ }
439
+ }
440
+ }
441
+ console.log(pc.bold('\n--- Router Health ---'));
442
+ const healthyModels = router.getHealthyModels();
443
+ for (const model of healthyModels) {
444
+ const { checkModelHealth } = await import('./router/health.js');
445
+ const health = await checkModelHealth(model, 5000);
446
+ const status = health.healthy ? pc.green('●') : pc.red('●');
447
+ const latency = health.latencyMs ? ` (${health.latencyMs}ms)` : '';
448
+ console.log(` ${status} ${model.name}: ${model.model}${latency}`);
449
+ }
450
+ console.log(pc.bold('----------------------\n'));
451
+ }
452
+ // Handle /index command
453
+ async function handleIndex(opts) {
454
+ console.log(pc.bold('\n--- Indexing Codebase ---'));
455
+ console.log(pc.gray(`Project: ${process.cwd()}`));
456
+ const indexDbPath = path.join(os.homedir(), '.daedalus', 'sessions', projectHash, 'index.sqlite');
457
+ if (!fs.existsSync(path.dirname(indexDbPath))) {
458
+ fs.mkdirSync(path.dirname(indexDbPath), { recursive: true });
459
+ }
460
+ const { initIndexDb } = await import('./indexing/fts.js');
461
+ const { indexCodebase } = await import('./indexing/indexer.js');
462
+ const db = initIndexDb(indexDbPath);
463
+ console.log(pc.gray('\nScanning files...'));
464
+ const start = Date.now();
465
+ try {
466
+ const barWidth = 20;
467
+ let lastPct = -1;
468
+ const onProgress = ({ current, total, file }) => {
469
+ const pct = Math.round((current / total) * 100);
470
+ if (pct === lastPct)
471
+ return;
472
+ lastPct = pct;
473
+ const filled = Math.round((current / total) * barWidth);
474
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
475
+ process.stdout.write(`\r ${pc.cyan(bar)} ${pc.white(`${current}/${total}`)} ${pc.gray(file.slice(-40))}`);
476
+ };
477
+ const result = indexCodebase(db, process.cwd(), projectHash, { ...opts, onProgress });
478
+ process.stdout.write('\n');
479
+ const elapsed = Date.now() - start;
480
+ console.log(pc.green(`\n✔ Indexing complete in ${elapsed}ms`));
481
+ console.log(pc.white(` Total files: ${result.totalFiles}`));
482
+ console.log(pc.white(` Indexed files: ${result.indexedFiles}`));
483
+ console.log(pc.white(` Skipped (unchanged): ${result.skippedFiles}`));
484
+ if (result.errors.length > 0) {
485
+ console.log(pc.yellow(`\nErrors (${result.errors.length}):`));
486
+ result.errors.slice(0, 10).forEach(e => console.log(pc.red(` - ${e}`)));
487
+ if (result.errors.length > 10) {
488
+ console.log(pc.gray(` ... and ${result.errors.length - 10} more`));
489
+ }
490
+ }
491
+ }
492
+ catch (err) {
493
+ console.error(pc.red(`\n❌ Indexing failed: ${err.message}`));
494
+ }
495
+ }
496
+ // Handle /find <query> [limit]
497
+ async function handleFindSymbol(query, limit) {
498
+ const indexDbPath = path.join(os.homedir(), '.daedalus', 'sessions', projectHash, 'index.sqlite');
499
+ if (!fs.existsSync(indexDbPath)) {
500
+ console.log(pc.yellow('⚠ No index found. Run /index first.'));
501
+ return;
502
+ }
503
+ const { initIndexDb, searchSymbols } = await import('./indexing/fts.js');
504
+ const db = initIndexDb(indexDbPath);
505
+ console.log(pc.bold(`\n--- Symbol Search: "${query}" ---`));
506
+ const symbols = searchSymbols(db, query, projectHash, limit);
507
+ if (symbols.length === 0) {
508
+ console.log(pc.gray(' No symbols found.'));
509
+ return;
510
+ }
511
+ console.log(pc.white(`\nFound ${symbols.length} symbol(s):`));
512
+ for (const s of symbols) {
513
+ const kindColor = s.kind === 'function' ? pc.cyan : s.kind === 'class' ? pc.green : s.kind === 'interface' ? pc.blue : pc.white;
514
+ const loc = `${s.file_path}:${s.line_start}${s.line_end !== s.line_start ? '-' + s.line_end : ''}`;
515
+ console.log(` ${kindColor(`[${s.kind}]`)} ${pc.bold(s.name)} ${pc.dim(`(${loc})`)}`);
516
+ if (s.signature) {
517
+ console.log(pc.dim(` ${s.signature.slice(0, 100)}${s.signature.length > 100 ? '...' : ''}`));
518
+ }
519
+ }
520
+ }
521
+ // Handle /refs <symbol>
522
+ async function handleGetReferences(symbol) {
523
+ const indexDbPath = path.join(os.homedir(), '.daedalus', 'sessions', projectHash, 'index.sqlite');
524
+ if (!fs.existsSync(indexDbPath)) {
525
+ console.log(pc.yellow('⚠ No index found. Run /index first.'));
526
+ return;
527
+ }
528
+ const { initIndexDb, findReferences } = await import('./indexing/fts.js');
529
+ const db = initIndexDb(indexDbPath);
530
+ console.log(pc.bold(`\n--- References to: ${symbol} ---`));
531
+ const refs = findReferences(db, symbol, projectHash);
532
+ if (refs.length === 0) {
533
+ console.log(pc.gray(' No references found.'));
534
+ return;
535
+ }
536
+ // Group by caller
537
+ const byCaller = new Map();
538
+ for (const r of refs) {
539
+ const key = `${r.caller_name} (${r.caller_file}:${r.caller_line})`;
540
+ if (!byCaller.has(key))
541
+ byCaller.set(key, []);
542
+ byCaller.get(key).push(r);
543
+ }
544
+ console.log(pc.white(`\nFound ${refs.length} reference(s) from ${byCaller.size} caller(s):`));
545
+ for (const [caller, refs] of byCaller) {
546
+ console.log(pc.cyan(`\n ${caller}:`));
547
+ for (const r of refs.slice(0, 5)) {
548
+ console.log(pc.dim(` ${r.callee_name} at ${r.callee_file}:${r.callee_line}`));
549
+ }
550
+ if (refs.length > 5) {
551
+ console.log(pc.dim(` ... and ${refs.length - 5} more`));
552
+ }
553
+ }
554
+ }
555
+ // Handle /def <symbol>
556
+ async function handleGetDefinition(symbol) {
557
+ const indexDbPath = path.join(os.homedir(), '.daedalus', 'sessions', projectHash, 'index.sqlite');
558
+ if (!fs.existsSync(indexDbPath)) {
559
+ console.log(pc.yellow('⚠ No index found. Run /index first.'));
560
+ return;
561
+ }
562
+ const { initIndexDb, findDefinitions } = await import('./indexing/fts.js');
563
+ const db = initIndexDb(indexDbPath);
564
+ console.log(pc.bold(`\n--- Definition: ${symbol} ---`));
565
+ const defs = findDefinitions(db, symbol, projectHash);
566
+ if (defs.length === 0) {
567
+ console.log(pc.gray(' No definitions found.'));
568
+ return;
569
+ }
570
+ console.log(pc.white(`\nFound ${defs.length} definition(s):`));
571
+ for (const d of defs) {
572
+ const kindColor = d.kind === 'function' ? pc.cyan : d.kind === 'class' ? pc.green : d.kind === 'interface' ? pc.blue : pc.white;
573
+ const loc = `${d.file_path}:${d.line_start}${d.line_end !== d.line_start ? '-' + d.line_end : ''}`;
574
+ console.log(` ${kindColor(`[${d.kind}]`)} ${pc.bold(d.name)} ${pc.dim(`(${loc})`)}`);
575
+ if (d.signature) {
576
+ console.log(pc.dim(` ${d.signature.slice(0, 120)}${d.signature.length > 120 ? '...' : ''}`));
577
+ }
578
+ }
579
+ }
580
+ // Max chars of tool output stored in message history (prevents context-window overflow)
581
+ const TOOL_RESULT_MAX_CHARS = 32_000;
582
+ function truncateToolResult(content) {
583
+ if (content.length <= TOOL_RESULT_MAX_CHARS)
584
+ return content;
585
+ const kept = content.slice(0, TOOL_RESULT_MAX_CHARS);
586
+ const dropped = content.length - TOOL_RESULT_MAX_CHARS;
587
+ return `${kept}\n... [truncated ${dropped} chars — use read_file with offset/limit to see more]`;
588
+ }
589
+ // Streaming response handler with tool call support — iterative, not recursive
590
+ const MAX_TOOL_TURNS = 40;
591
+ async function callModelWithTools(userContent) {
592
+ // Auto-inject index context on first user turn (not on tool-result rounds)
593
+ if (userContent) {
594
+ const indexCtx = await buildIndexContext(userContent);
595
+ const augmentedContent = indexCtx ? indexCtx + userContent : userContent;
596
+ messages.push({ role: 'user', content: augmentedContent });
597
+ }
598
+ // Combine built-in tools with MCP tools (stable across turns)
599
+ const allTools = [...BUILTIN_TOOLS, ...mcpRegistry.getToolDefinitions()];
600
+ turnStartTime = Date.now();
601
+ let lastContent = '';
602
+ let turn = 0;
603
+ while (turn < MAX_TOOL_TURNS) {
604
+ const spinner = new DaedalusSpinner({ text: 'Daedalus thinking', color: (s) => pc.cyan(s) });
605
+ spinner.start();
606
+ let fullContent = '';
607
+ const toolCallMap = new Map();
608
+ let blockOpened = false;
609
+ const turnStart = Date.now();
610
+ const openBlock = () => {
611
+ if (!blockOpened) {
612
+ blockOpened = true;
613
+ spinner.stop();
614
+ openAssistantBlock();
615
+ }
616
+ };
617
+ currentAbortController = new AbortController();
618
+ const signal = currentAbortController.signal;
619
+ try {
620
+ const stream = await router.chatStream({
621
+ model: 'auto',
622
+ messages: messages,
623
+ temperature: 0.1,
624
+ tools: allTools,
625
+ tool_choice: 'auto',
626
+ stream: true,
627
+ signal,
628
+ });
629
+ for await (const chunk of stream) {
630
+ if (signal.aborted)
631
+ break;
632
+ const choice = chunk.choices[0];
633
+ if (!choice)
634
+ continue;
635
+ const delta = choice.delta;
636
+ if (delta.content) {
637
+ openBlock();
638
+ fullContent += delta.content;
639
+ writeAssistantChunk(delta.content);
640
+ }
641
+ if (delta.tool_calls) {
642
+ openBlock();
643
+ for (const tc of delta.tool_calls) {
644
+ const index = tc.index ?? 0;
645
+ if (!toolCallMap.has(index)) {
646
+ toolCallMap.set(index, {
647
+ id: tc.id ?? `call_${Date.now()}_${index}`,
648
+ type: 'function',
649
+ function: { name: '', arguments: '' },
650
+ });
651
+ }
652
+ const call = toolCallMap.get(index);
653
+ if (tc.function?.name)
654
+ call.function.name = tc.function.name;
655
+ if (tc.function?.arguments)
656
+ call.function.arguments += tc.function.arguments;
657
+ }
658
+ }
659
+ if (choice.finish_reason === 'tool_calls' || choice.finish_reason === 'stop' || choice.finish_reason === 'length') {
660
+ break;
661
+ }
662
+ }
663
+ // Ensure spinner is stopped even if no output was produced
664
+ if (!blockOpened)
665
+ spinner.stop();
666
+ // Check for user cancellation
667
+ if (signal.aborted) {
668
+ if (blockOpened)
669
+ closeAssistantBlock(fullContent.length, Date.now() - turnStart);
670
+ console.log(pc.dim('\n ⏹ Stopped'));
671
+ currentAbortController = null;
672
+ return { content: fullContent, toolCalls: [] };
673
+ }
674
+ }
675
+ catch (error) {
676
+ if (signal.aborted) {
677
+ spinner.stop();
678
+ console.log(pc.dim('\n ⏹ Stopped'));
679
+ currentAbortController = null;
680
+ return { content: '', toolCalls: [] };
681
+ }
682
+ spinner.stop();
683
+ console.error(pc.red(`\n❌ Error calling model: ${error.message}`));
684
+ throw error;
685
+ }
686
+ currentAbortController = null;
687
+ const toolCallArray = Array.from(toolCallMap.values()).filter(tc => tc.function.name);
688
+ lastContent = fullContent;
689
+ // Close assistant block after streaming, before tool results
690
+ if (blockOpened) {
691
+ closeAssistantBlock(fullContent.length, Date.now() - turnStart, toolCallArray.length);
692
+ }
693
+ if (toolCallArray.length === 0) {
694
+ // No tool calls — model is done
695
+ messages.push({ role: 'assistant', content: fullContent });
696
+ return { content: fullContent, toolCalls: [] };
697
+ }
698
+ // Push assistant turn with tool calls
699
+ messages.push({
700
+ role: 'assistant',
701
+ content: fullContent || '',
702
+ tool_calls: toolCallArray,
703
+ });
704
+ console.log(`\n ${pc.dim('🔧')} ${pc.dim(`Executing ${toolCallArray.length} tool call(s)...`)}`);
705
+ const results = await executeToolCalls(toolCallArray, toolContext);
706
+ for (const result of results) {
707
+ messages.push({
708
+ role: 'tool',
709
+ content: truncateToolResult(result.content),
710
+ tool_call_id: result.toolCallId,
711
+ });
712
+ const status = result.success ? pc.green('✔') : pc.red('✗');
713
+ console.log(` ${status} ${result.name}`);
714
+ if (!result.success && result.error) {
715
+ console.log(pc.red(` Error: ${result.error}`));
716
+ }
717
+ // Show inline preview of successful tool results
718
+ if (result.success && result.content) {
719
+ const contentStr = result.content;
720
+ const lines = contentStr.split('\n');
721
+ const previewLines = lines.slice(0, 3);
722
+ for (const line of previewLines) {
723
+ const truncated = line.length > 120 ? line.slice(0, 120) + '…' : line;
724
+ if (truncated.trim())
725
+ console.log(` ${pc.dim('┃')} ${pc.gray(truncated)}`);
726
+ }
727
+ if (lines.length > 3) {
728
+ console.log(` ${pc.dim('┃')} ${pc.dim(`… ${lines.length - 3} more line${lines.length - 3 > 1 ? 's' : ''}`)}`);
729
+ }
730
+ }
731
+ }
732
+ turn++;
733
+ }
734
+ // Reached max turns without a clean stop
735
+ console.log(`\n ${pc.yellow('⚠')} ${pc.yellow(`Reached max tool turns (${MAX_TOOL_TURNS}). Stopping.`)}`);
736
+ messages.push({ role: 'assistant', content: lastContent });
737
+ return { content: lastContent, toolCalls: [] };
738
+ }
739
+ // Fallback: Structured output for models without tool support
740
+ async function callModelWithFallback(userContent) {
741
+ messages.push({ role: 'user', content: userContent });
742
+ console.log(pc.gray('🤖 Thinking (fallback mode)...'));
743
+ try {
744
+ const response = await router.chat.completions.create({
745
+ model: 'auto',
746
+ messages: messages,
747
+ temperature: 0.1,
748
+ });
749
+ const reply = response.choices[0].message?.content || '';
750
+ messages.push({ role: 'assistant', content: reply });
751
+ openAssistantBlock();
752
+ writeAssistantChunk(reply);
753
+ const usage = response.usage;
754
+ const elapsed = Date.now() - turnStartTime;
755
+ closeAssistantBlock(reply.length, elapsed);
756
+ return reply;
757
+ }
758
+ catch (error) {
759
+ console.error(pc.red(`\n❌ Fallback error: ${error.message}`));
760
+ throw error;
761
+ }
762
+ }
763
+ // Promisify a single readline question
764
+ function askLine(prompt) {
765
+ return new Promise((resolve) => rl.question(prompt, resolve));
766
+ }
767
+ // ── Visual formatting helpers ──────────────────────────────────────────────────
768
+ function printUserTurn(userMessage) {
769
+ const bdr = (s) => pc.dim(pc.yellow(s));
770
+ const lines = userMessage.split('\n');
771
+ const w = 52;
772
+ console.log(`\n ${bdr('╭─')} ${pc.yellow(pc.bold('⬡ You'))} ${bdr('─'.repeat(Math.max(0, w - 8)))}${bdr('╮')}`);
773
+ for (const line of lines) {
774
+ const t = line.length > 70 ? line.slice(0, 70) + '…' : line;
775
+ console.log(` ${bdr('│')} ${pc.white(t)}${' '.repeat(Math.max(1, w - t.length))}${bdr('│')}`);
776
+ }
777
+ console.log(` ${bdr('╰')}${bdr('─'.repeat(w))}${bdr('╯')}`);
778
+ console.log();
779
+ }
780
+ let _assistantLineBuf = '';
781
+ function openAssistantBlock() {
782
+ const bdr = (s) => pc.dim(pc.cyan(s));
783
+ const w = 52;
784
+ console.log(` ${bdr('╭─')} ${pc.dim('◇')} ${pc.dim('Daedalus')} ${bdr('─'.repeat(Math.max(0, w - 11)))}${bdr('╮')}`);
785
+ }
786
+ function writeAssistantChunk(chunk) {
787
+ _assistantLineBuf += chunk;
788
+ const lines = _assistantLineBuf.split('\n');
789
+ _assistantLineBuf = lines.pop() || '';
790
+ const bdr = (s) => pc.dim(pc.cyan(s));
791
+ const w = 52;
792
+ for (const line of lines) {
793
+ const t = line.length > 70 ? line.slice(0, 70) + '…' : line;
794
+ console.log(` ${bdr('│')} ${pc.white(t)}${' '.repeat(Math.max(1, w - t.length))}${bdr('│')}`);
795
+ }
796
+ }
797
+ function closeAssistantBlock(tokens, elapsedMs, toolCount) {
798
+ const bdr = (s) => pc.dim(pc.cyan(s));
799
+ const w = 52;
800
+ if (_assistantLineBuf) {
801
+ const t = _assistantLineBuf.length > 70 ? _assistantLineBuf.slice(0, 70) + '…' : _assistantLineBuf;
802
+ console.log(` ${bdr('│')} ${pc.white(t)}${' '.repeat(Math.max(1, w - t.length))}${bdr('│')}`);
803
+ _assistantLineBuf = '';
804
+ }
805
+ const meta = toolCount !== undefined
806
+ ? `${toolCount} tool(s) · ~${Math.round(tokens / 4)}t out · ${elapsedMs}ms`
807
+ : `~${Math.round(tokens / 4)}t out · ${elapsedMs}ms`;
808
+ console.log(` ${bdr('╰')}${bdr('─'.repeat(w))}${bdr('╯')} ${pc.dim(meta)}`);
809
+ }
810
+ function turnSeparator() {
811
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
812
+ console.log(` ${pc.dim('─'.repeat(58))} ${pc.dim(ts)}`);
813
+ }
814
+ // Main chat loop — iterative, no recursive Promise chains
815
+ async function chatLoop() {
816
+ while (true) {
817
+ const prompt = activeFiles.size > 0
818
+ ? `\n${pc.cyan(' ⬡')} ${pc.dim(`[${activeFiles.size} file${activeFiles.size > 1 ? 's' : ''}]`)} ${pc.bold(pc.white('›'))} `
819
+ : `\n${pc.cyan(' ⬡')} ${pc.bold(pc.white('›'))} `;
820
+ const input = await askLine(prompt);
821
+ const trimmedInput = input.trim();
822
+ if (!trimmedInput)
823
+ continue;
824
+ const lowerInput = trimmedInput.toLowerCase();
825
+ // Handle Exit
826
+ if (lowerInput === 'exit' || lowerInput === 'quit') {
827
+ const todos = getSessionTodos(sessionId);
828
+ sessionManager.saveSessionState(messages, activeFiles, todos);
829
+ console.log(pc.gray(`Session saved: ${sessionManager.sessionId}`));
830
+ console.log(pc.yellow('\nEnding session. Goodbye! 👋\n'));
831
+ rl.close();
832
+ process.exit(0);
833
+ }
834
+ // Command quickref
835
+ if (lowerInput === '?' || lowerInput === 'help') {
836
+ console.log(`\n ${pc.bold('Commands')}`);
837
+ console.log(` ${pc.dim('────────────────────────────────────')}`);
838
+ console.log(` ${pc.cyan('/add')} Add file to context`);
839
+ console.log(` ${pc.cyan('/remove')} Remove file from context`);
840
+ console.log(` ${pc.cyan('/context')} Show active file context`);
841
+ console.log(` ${pc.cyan('/commit')} Stage and commit changes`);
842
+ console.log(` ${pc.cyan('/undo')} Undo last file patch`);
843
+ console.log(` ${pc.cyan('/test')} Run tests and auto-fix failures`);
844
+ console.log(` ${pc.cyan('/index')} Index codebase for symbol search`);
845
+ console.log(` ${pc.cyan('/find')} Search indexed symbols`);
846
+ console.log(` ${pc.cyan('/refs')} Find symbol references`);
847
+ console.log(` ${pc.cyan('/def')} Get symbol definition`);
848
+ console.log(` ${pc.cyan('/project')} View or set project config`);
849
+ console.log(` ${pc.cyan('/session')} Manage chat sessions`);
850
+ console.log(` ${pc.cyan('?')} Show this help menu`);
851
+ console.log(` ${pc.cyan('exit')} Save and quit\n`);
852
+ continue;
853
+ }
854
+ // Command: /add <file>
855
+ if (trimmedInput.startsWith('/add ')) {
856
+ const fileArg = trimmedInput.substring(5).trim();
857
+ if (!fileArg) {
858
+ console.log(pc.red('⚠ Please specify a file path. Example: /add src/App.tsx'));
859
+ }
860
+ else {
861
+ const absPath = path.resolve(fileArg);
862
+ activeFiles.set(absPath, fileArg);
863
+ toolContext.activeFiles = new Map(activeFiles);
864
+ console.log(pc.green(`✔ Added file to context: ${pc.bold(fileArg)}`));
865
+ }
866
+ continue;
867
+ }
868
+ // Command: /remove <file>
869
+ if (trimmedInput.startsWith('/remove ')) {
870
+ const fileArg = trimmedInput.substring(8).trim();
871
+ if (!fileArg) {
872
+ console.log(pc.red('⚠ Please specify a file path. Example: /remove src/App.tsx'));
873
+ }
874
+ else {
875
+ const absPath = path.resolve(fileArg);
876
+ if (activeFiles.delete(absPath)) {
877
+ toolContext.activeFiles = new Map(activeFiles);
878
+ console.log(pc.green(`✔ Removed file from context: ${pc.bold(fileArg)}`));
879
+ }
880
+ else {
881
+ console.log(pc.yellow(`⚠ File was not in context: ${fileArg}`));
882
+ }
883
+ }
884
+ continue;
885
+ }
886
+ // Command: /context
887
+ if (lowerInput === '/context') {
888
+ console.log(pc.bold('\n--- Monitored Files in Context ---'));
889
+ if (activeFiles.size === 0) {
890
+ console.log(pc.gray(' (No active files. Use "/add <filepath>" to add files)'));
891
+ }
892
+ else {
893
+ activeFiles.forEach((filename) => {
894
+ console.log(` • ${pc.cyan(filename)}`);
895
+ });
896
+ }
897
+ console.log(pc.bold('----------------------------------'));
898
+ continue;
899
+ }
900
+ // Command: /session list|load|new|delete
901
+ if (lowerInput.startsWith('/session')) {
902
+ const parts = trimmedInput.substring(9).trim().split(/\s+/);
903
+ const subCmd = parts[0]?.toLowerCase();
904
+ if (subCmd === 'list') {
905
+ const list = sessionManager.getSessionsForProject();
906
+ console.log(pc.bold('\n--- Sessions for this Project ---'));
907
+ if (list.length === 0) {
908
+ console.log(pc.gray(' No saved sessions.'));
909
+ }
910
+ else {
911
+ list.forEach((s) => {
912
+ const dateStr = new Date(s.updated_at).toLocaleString();
913
+ const activeMarker = s.id === sessionManager.sessionId ? pc.green(' * ') : ' ';
914
+ console.log(`${activeMarker}${s.title} (ID: ${pc.cyan(s.id)}) - Last updated: ${dateStr}`);
915
+ });
916
+ }
917
+ console.log(pc.bold('---------------------------------'));
918
+ }
919
+ else if (subCmd === 'load') {
920
+ const targetId = parts[1]?.trim();
921
+ if (!targetId) {
922
+ console.log(pc.red('⚠ Usage: /session load <id>'));
923
+ }
924
+ else {
925
+ try {
926
+ const todos = getSessionTodos(sessionId);
927
+ sessionManager.saveSessionState(messages, activeFiles, todos);
928
+ const loaded = sessionManager.startSession(targetId);
929
+ initializeSessionState(loaded);
930
+ console.log(pc.green(`✔ Loaded session: ${sessionManager.sessionTitle} (${sessionManager.sessionId})`));
931
+ }
932
+ catch (err) {
933
+ console.log(pc.red(`⚠ Failed to load session: ${err.message}`));
934
+ }
935
+ }
936
+ }
937
+ else if (subCmd === 'new') {
938
+ const title = parts.slice(1).join(' ').trim();
939
+ const todos = getSessionTodos(sessionId);
940
+ sessionManager.saveSessionState(messages, activeFiles, todos);
941
+ const loaded = sessionManager.startSession(undefined, title || undefined);
942
+ initializeSessionState(loaded);
943
+ console.log(pc.green(`✔ Started new session: ${sessionManager.sessionTitle} (${sessionManager.sessionId})`));
944
+ }
945
+ else if (subCmd === 'delete') {
946
+ const targetId = parts[1]?.trim();
947
+ if (!targetId) {
948
+ console.log(pc.red('⚠ Usage: /session delete <id>'));
949
+ }
950
+ else {
951
+ sessionManager.deleteSession(targetId);
952
+ console.log(pc.green(`✔ Deleted session: ${targetId}`));
953
+ }
954
+ }
955
+ else {
956
+ console.log(pc.red('⚠ Usage: /session list | load <id> | new [title] | delete <id>'));
957
+ }
958
+ continue;
959
+ }
960
+ // Command: /memory
961
+ if (lowerInput === '/memory') {
962
+ const mem = sessionManager.loadMemory();
963
+ console.log(pc.bold('\n--- Project Facts & Conventions (Memory) ---'));
964
+ console.log(pc.bold('Conventions:'));
965
+ if (Object.keys(mem.conventions).length === 0) {
966
+ console.log(pc.gray(' No conventions saved.'));
967
+ }
968
+ else {
969
+ for (const [k, v] of Object.entries(mem.conventions)) {
970
+ console.log(` • ${pc.cyan(k)}: ${v}`);
971
+ }
972
+ }
973
+ console.log(pc.bold('\nFacts:'));
974
+ if (mem.facts.length === 0) {
975
+ console.log(pc.gray(' No facts saved.'));
976
+ }
977
+ else {
978
+ mem.facts.forEach(f => {
979
+ console.log(` • ${pc.cyan(f.key)}: ${f.value} (source: ${f.source})`);
980
+ });
981
+ }
982
+ console.log(pc.bold('------------------------------------------'));
983
+ continue;
984
+ }
985
+ // Command: /fact <key> = <value>
986
+ if (lowerInput.startsWith('/fact ')) {
987
+ const argStr = trimmedInput.substring(6).trim();
988
+ const eqIdx = argStr.indexOf('=');
989
+ if (eqIdx < 0) {
990
+ console.log(pc.red('⚠ Usage: /fact <key> = <value>'));
991
+ }
992
+ else {
993
+ const key = argStr.slice(0, eqIdx).trim();
994
+ const value = argStr.slice(eqIdx + 1).trim();
995
+ sessionManager.addFact(key, value, 'user');
996
+ console.log(pc.green(`✔ Saved fact: ${key} = ${value}`));
997
+ }
998
+ continue;
999
+ }
1000
+ // Command: /convention <key> = <value>
1001
+ if (lowerInput.startsWith('/convention ')) {
1002
+ const argStr = trimmedInput.substring(12).trim();
1003
+ const eqIdx = argStr.indexOf('=');
1004
+ if (eqIdx < 0) {
1005
+ console.log(pc.red('⚠ Usage: /convention <key> = <value>'));
1006
+ }
1007
+ else {
1008
+ const key = argStr.slice(0, eqIdx).trim();
1009
+ const value = argStr.slice(eqIdx + 1).trim();
1010
+ sessionManager.setConvention(key, value);
1011
+ console.log(pc.green(`✔ Saved convention: ${key} = ${value}`));
1012
+ }
1013
+ continue;
1014
+ }
1015
+ // Command: /clear
1016
+ if (lowerInput === '/clear') {
1017
+ messages.length = 0;
1018
+ messages.push({ role: 'system', content: getSystemPromptWithMemory() });
1019
+ console.log(pc.green('✔ Conversation history cleared!'));
1020
+ continue;
1021
+ }
1022
+ // Command: /tools
1023
+ if (lowerInput === '/tools') {
1024
+ console.log(pc.bold('\n--- Available Tools ---'));
1025
+ BUILTIN_TOOLS.forEach(t => {
1026
+ console.log(` • ${pc.cyan(t.function.name)}: ${t.function.description}`);
1027
+ });
1028
+ const mcpTools = mcpRegistry.getToolDefinitions();
1029
+ if (mcpTools.length > 0) {
1030
+ console.log(pc.bold('\n--- MCP Tools ---'));
1031
+ mcpTools.forEach(t => {
1032
+ console.log(` • ${pc.cyan(t.function.name)}: ${t.function.description}`);
1033
+ });
1034
+ }
1035
+ console.log(pc.bold('-----------------------'));
1036
+ continue;
1037
+ }
1038
+ // Command: /spawn <role> <task>
1039
+ if (lowerInput.startsWith('/spawn ')) {
1040
+ const parts = trimmedInput.substring(7).trim().split(' ');
1041
+ if (parts.length < 2) {
1042
+ console.log(pc.red('⚠ Usage: /spawn <role> <task>'));
1043
+ console.log(pc.gray(' Roles: coder, reviewer, debugger, researcher, planner'));
1044
+ }
1045
+ else {
1046
+ const role = parts[0].toLowerCase();
1047
+ const task = parts.slice(1).join(' ');
1048
+ await handleSpawn(role, task);
1049
+ }
1050
+ continue;
1051
+ }
1052
+ // Command: /delegate <task> to <role>
1053
+ if (lowerInput.startsWith('/delegate ')) {
1054
+ const match = trimmedInput.substring(10).match(/^(.+)\s+to\s+(\w+)$/i);
1055
+ if (!match) {
1056
+ console.log(pc.red('⚠ Usage: /delegate <task> to <role>'));
1057
+ }
1058
+ else {
1059
+ const task = match[1].trim();
1060
+ const role = match[2].toLowerCase();
1061
+ await handleSpawn(role, task);
1062
+ }
1063
+ continue;
1064
+ }
1065
+ // Command: /orchestrate <goal>
1066
+ if (lowerInput.startsWith('/orchestrate ')) {
1067
+ const goal = trimmedInput.substring(13).trim();
1068
+ if (!goal) {
1069
+ console.log(pc.red('⚠ Usage: /orchestrate <goal>'));
1070
+ }
1071
+ else {
1072
+ await handleOrchestrate(goal);
1073
+ }
1074
+ continue;
1075
+ }
1076
+ // Command: /models
1077
+ if (lowerInput === '/models') {
1078
+ await handleModels();
1079
+ continue;
1080
+ }
1081
+ // Command: /config
1082
+ if (lowerInput === '/config') {
1083
+ handleConfig();
1084
+ continue;
1085
+ }
1086
+ // Command: /doctor
1087
+ if (lowerInput === '/doctor') {
1088
+ await handleDoctor();
1089
+ continue;
1090
+ }
1091
+ // Command: /onboard
1092
+ if (lowerInput === '/onboard') {
1093
+ console.log(pc.cyan('\n🔄 Re-running onboarding wizard...\n'));
1094
+ await runOnboarding(true);
1095
+ continue;
1096
+ }
1097
+ // Command: /undo — revert the last patch
1098
+ if (lowerInput === '/undo') {
1099
+ const history = toolContext.patchHistory;
1100
+ if (!history || history.length === 0) {
1101
+ console.log(pc.yellow('⚠ No patches to undo.'));
1102
+ }
1103
+ else {
1104
+ const last = history[history.length - 1];
1105
+ try {
1106
+ const currentContent = fs.readFileSync(last.filePath, 'utf8');
1107
+ if (currentContent === last.newContent) {
1108
+ fs.writeFileSync(last.filePath, last.oldContent, 'utf8');
1109
+ console.log(pc.green(`✔ Undid patch to ${last.filePath} (${last.description})`));
1110
+ }
1111
+ else {
1112
+ console.log(pc.yellow(`⚠ File ${last.filePath} has been modified since last patch. Cannot auto-undo.`));
1113
+ }
1114
+ history.pop();
1115
+ }
1116
+ catch (err) {
1117
+ console.log(pc.red(`⚠ Failed to undo: ${err.message}`));
1118
+ }
1119
+ }
1120
+ continue;
1121
+ }
1122
+ // Command: /commit — stage and commit changes
1123
+ if (lowerInput === '/commit' || lowerInput.startsWith('/commit ')) {
1124
+ const msg = trimmedInput.startsWith('/commit ') ? trimmedInput.substring(8).trim() : '';
1125
+ try {
1126
+ const { execute: termExec } = await import('./tools/builtin/terminal.js');
1127
+ const statusResult = await termExec({ command: 'git status --short', timeout: 10, workdir: process.cwd() }, toolContext);
1128
+ console.log(pc.bold('\n--- Git Status ---'));
1129
+ console.log(statusResult.content || pc.gray('(clean)'));
1130
+ if (!statusResult.content?.trim()) {
1131
+ console.log(pc.yellow('Nothing to commit.'));
1132
+ continue;
1133
+ }
1134
+ const addResult = await termExec({ command: 'git add -A', timeout: 10, workdir: process.cwd() }, toolContext);
1135
+ if (!addResult.success) {
1136
+ console.log(pc.red(`Stage failed: ${addResult.error}`));
1137
+ continue;
1138
+ }
1139
+ let commitMsg = msg;
1140
+ if (!commitMsg) {
1141
+ const diffResult = await termExec({ command: 'git diff --cached --stat', timeout: 10, workdir: process.cwd() }, toolContext);
1142
+ if (diffResult.content)
1143
+ console.log(pc.gray(diffResult.content));
1144
+ commitMsg = await askLine(pc.cyan(' Commit message: '));
1145
+ if (!commitMsg.trim()) {
1146
+ console.log(pc.yellow('Commit cancelled — empty message.'));
1147
+ await termExec({ command: 'git reset', timeout: 10, workdir: process.cwd() }, toolContext);
1148
+ continue;
1149
+ }
1150
+ }
1151
+ const commitResult = await termExec({ command: `git commit -m ${JSON.stringify(commitMsg)}`, timeout: 10, workdir: process.cwd() }, toolContext);
1152
+ if (commitResult.success) {
1153
+ console.log(pc.green(`\n✔ Commit: ${commitMsg.slice(0, 60)}`));
1154
+ }
1155
+ else {
1156
+ console.log(pc.red(`Commit failed: ${commitResult.error}`));
1157
+ }
1158
+ }
1159
+ catch (err) {
1160
+ console.log(pc.red(`⚠ Commit error: ${err.message}`));
1161
+ }
1162
+ continue;
1163
+ }
1164
+ // Command: /project — show/edit project-level config
1165
+ if (lowerInput === '/project') {
1166
+ const { loadProjectConfig } = await import('./tools/builtin/project-config.js');
1167
+ const cfg = loadProjectConfig(process.cwd());
1168
+ console.log(pc.bold('\n--- Project Config (.daedalus) ---'));
1169
+ console.log(JSON.stringify(cfg, null, 2));
1170
+ console.log(pc.bold('----------------------------------'));
1171
+ console.log(pc.gray('Use /project set <key> <value> to update'));
1172
+ continue;
1173
+ }
1174
+ if (lowerInput.startsWith('/project set ')) {
1175
+ const rest = trimmedInput.substring(13).trim();
1176
+ const eqIdx = rest.indexOf('=');
1177
+ let key, value;
1178
+ if (eqIdx >= 0) {
1179
+ key = rest.slice(0, eqIdx).trim();
1180
+ value = rest.slice(eqIdx + 1).trim();
1181
+ }
1182
+ else {
1183
+ const parts = rest.split(/\s+/);
1184
+ key = parts[0];
1185
+ value = parts.slice(1).join(' ');
1186
+ }
1187
+ if (!key || !value) {
1188
+ console.log(pc.red('⚠ Usage: /project set <key> = <value>'));
1189
+ }
1190
+ else {
1191
+ const { loadProjectConfig, saveProjectConfig } = await import('./tools/builtin/project-config.js');
1192
+ const cfg = loadProjectConfig(process.cwd());
1193
+ cfg[key] = value;
1194
+ saveProjectConfig(cfg);
1195
+ console.log(pc.green(`✔ Set ${key} = ${value}`));
1196
+ }
1197
+ continue;
1198
+ }
1199
+ // Command: /test — run tests and auto-fix failures (TDD loop)
1200
+ if (lowerInput === '/test' || lowerInput.startsWith('/test ')) {
1201
+ const maxLoops = lowerInput.startsWith('/test ') ? parseInt(trimmedInput.substring(6).trim(), 10) || 3 : 3;
1202
+ const { loadProjectConfig } = await import('./tools/builtin/project-config.js');
1203
+ const { execute: termExec } = await import('./tools/builtin/terminal.js');
1204
+ const cfg = loadProjectConfig(process.cwd());
1205
+ const testCmd = cfg.testCommand || 'npm test';
1206
+ console.log(pc.bold(`\n🧪 Test-Run-Fix Loop (max ${maxLoops} iterations)`));
1207
+ console.log(pc.gray(`Test command: ${testCmd}\n`));
1208
+ for (let i = 0; i < maxLoops; i++) {
1209
+ console.log(pc.cyan(`\n─── Run ${i + 1}/${maxLoops} ───`));
1210
+ const result = await termExec({ command: testCmd, timeout: 120, workdir: process.cwd() }, toolContext);
1211
+ console.log(result.content?.slice(0, 2000) || pc.gray('(no output)'));
1212
+ if (result.success) {
1213
+ console.log(pc.green('\n✔ All tests passed!'));
1214
+ break;
1215
+ }
1216
+ if (i === maxLoops - 1) {
1217
+ console.log(pc.yellow(`\n⚠ Max loops (${maxLoops}) reached. Tests still failing.`));
1218
+ break;
1219
+ }
1220
+ const failureCtx = `Tests failed (run ${i + 1}/${maxLoops}). Here's the output:\n\n${result.content?.slice(0, 8000) || 'Unknown failure'}\n\nAnalyze the failures and fix the code.`;
1221
+ const filesContext = buildFileContext();
1222
+ const userContent = `${filesContext}User Prompt: ${failureCtx}`;
1223
+ await callModelWithTools(userContent);
1224
+ }
1225
+ continue;
1226
+ }
1227
+ // Command: /index [options]
1228
+ if (lowerInput.startsWith('/index')) {
1229
+ const args = trimmedInput.substring(6).trim().split(/\s+/);
1230
+ const opts = {};
1231
+ for (const arg of args) {
1232
+ if (arg.startsWith('--exclude=')) {
1233
+ opts.exclude = arg.split('=')[1].split(',');
1234
+ }
1235
+ else if (arg.startsWith('--ext=')) {
1236
+ opts.extensions = arg.split('=')[1].split(',');
1237
+ }
1238
+ }
1239
+ await handleIndex(opts);
1240
+ continue;
1241
+ }
1242
+ // Command: /find <query> [limit]
1243
+ if (lowerInput.startsWith('/find ')) {
1244
+ const parts = trimmedInput.substring(6).trim().split(/\s+/);
1245
+ if (parts.length === 0) {
1246
+ console.log(pc.red('⚠ Usage: /find <query> [limit]'));
1247
+ }
1248
+ else {
1249
+ const query = parts[0];
1250
+ const limit = parts[1] ? parseInt(parts[1], 10) : 30;
1251
+ if (isNaN(limit)) {
1252
+ console.log(pc.red('⚠ Invalid limit'));
1253
+ }
1254
+ else {
1255
+ await handleFindSymbol(query, limit);
1256
+ }
1257
+ }
1258
+ continue;
1259
+ }
1260
+ // Command: /refs <symbol>
1261
+ if (lowerInput.startsWith('/refs ')) {
1262
+ const symbol = trimmedInput.substring(6).trim();
1263
+ if (!symbol) {
1264
+ console.log(pc.red('⚠ Usage: /refs <symbol>'));
1265
+ }
1266
+ else {
1267
+ await handleGetReferences(symbol);
1268
+ }
1269
+ continue;
1270
+ }
1271
+ // Command: /def <symbol>
1272
+ if (lowerInput.startsWith('/def ')) {
1273
+ const symbol = trimmedInput.substring(5).trim();
1274
+ if (!symbol) {
1275
+ console.log(pc.red('⚠ Usage: /def <symbol>'));
1276
+ }
1277
+ else {
1278
+ await handleGetDefinition(symbol);
1279
+ }
1280
+ continue;
1281
+ }
1282
+ // User Message Processing
1283
+ try {
1284
+ const filesContext = buildFileContext();
1285
+ const indexCtx = await buildIndexContext(trimmedInput);
1286
+ const userContent = `${indexCtx}${filesContext}User Prompt: ${trimmedInput}`;
1287
+ printUserTurn(trimmedInput);
1288
+ await callModelWithTools(userContent);
1289
+ }
1290
+ catch (error) {
1291
+ console.error(pc.red(`\n❌ Error: ${error.message}`));
1292
+ try {
1293
+ const filesContext = buildFileContext();
1294
+ const userContent = `${filesContext}User Prompt: ${trimmedInput}`;
1295
+ console.log(pc.yellow('\n🔄 Trying fallback mode...'));
1296
+ await callModelWithFallback(userContent);
1297
+ }
1298
+ catch (fallbackErr) {
1299
+ console.error(pc.red(`\n❌ Fallback also failed: ${fallbackErr.message}`));
1300
+ console.error(pc.gray('Check that at least one local server is running.'));
1301
+ }
1302
+ }
1303
+ turnSeparator();
1304
+ }
1305
+ }
1306
+ // Start health checks and REPL
1307
+ async function main() {
1308
+ // SIGINT handler — cancel generation if streaming, otherwise exit
1309
+ process.on('SIGINT', () => {
1310
+ if (currentAbortController) {
1311
+ currentAbortController.abort();
1312
+ return;
1313
+ }
1314
+ router.stopHealthChecks?.();
1315
+ if (process.stdin.isTTY)
1316
+ process.stdin.setRawMode?.(false);
1317
+ process.stdout.write('\n');
1318
+ process.exit(0);
1319
+ });
1320
+ // Clean up health checks on normal exit too
1321
+ process.on('exit', () => {
1322
+ router.stopHealthChecks?.();
1323
+ });
1324
+ // Print the awesome banner first!
1325
+ printBanner();
1326
+ printConfigInfo();
1327
+ try {
1328
+ await router.startHealthChecks();
1329
+ console.log(pc.green('\n✔ Router started. Health checks running every 30s.'));
1330
+ }
1331
+ catch (err) {
1332
+ console.error(pc.yellow(`\n⚠ Router health checks failed: ${err.message}`));
1333
+ }
1334
+ // Initialize MCP registry
1335
+ try {
1336
+ await mcpRegistry.connectAll();
1337
+ const servers = mcpRegistry.getConnectedServers();
1338
+ if (servers.length > 0) {
1339
+ console.log(pc.green(`\n✔ MCP connected: ${servers.join(', ')}`));
1340
+ }
1341
+ }
1342
+ catch (err) {
1343
+ console.error(pc.yellow(`\n⚠ MCP initialization failed: ${err.message}`));
1344
+ }
1345
+ // Auto-index at startup (non-blocking, background)
1346
+ if (config.indexing.enabled) {
1347
+ (async () => {
1348
+ try {
1349
+ const indexDbPath = path.join(os.homedir(), '.daedalus', 'sessions', projectHash, 'index.sqlite');
1350
+ if (!fs.existsSync(indexDbPath)) {
1351
+ console.log(pc.cyan('\n ⚡ Auto-indexing codebase (background)...'));
1352
+ const { initIndexDb } = await import('./indexing/fts.js');
1353
+ const { indexCodebase } = await import('./indexing/indexer.js');
1354
+ const db = initIndexDb(indexDbPath);
1355
+ const result = indexCodebase(db, process.cwd(), projectHash, {
1356
+ exclude: config.indexing.exclude,
1357
+ });
1358
+ console.log(pc.green(` ✔ Indexed ${result.indexedFiles} files (${result.skippedFiles} unchanged)`));
1359
+ if (result.errors.length > 0) {
1360
+ console.log(pc.yellow(` ⚠ ${result.errors.length} file(s) had errors`));
1361
+ }
1362
+ toolContext.indexDb = db;
1363
+ }
1364
+ else {
1365
+ // Re-index incrementally (fast — checks SHA hashes)
1366
+ const { initIndexDb } = await import('./indexing/fts.js');
1367
+ const { indexCodebase } = await import('./indexing/indexer.js');
1368
+ const db = initIndexDb(indexDbPath);
1369
+ const result = indexCodebase(db, process.cwd(), projectHash, {
1370
+ exclude: config.indexing.exclude,
1371
+ });
1372
+ if (result.indexedFiles > 0) {
1373
+ console.log(pc.gray(` 📚 Re-indexed ${result.indexedFiles} changed file(s)`));
1374
+ }
1375
+ toolContext.indexDb = db;
1376
+ }
1377
+ }
1378
+ catch (err) {
1379
+ console.error(pc.yellow(` ⚠ Auto-index failed: ${err.message}`));
1380
+ }
1381
+ })();
1382
+ }
1383
+ await chatLoop();
1384
+ }
1385
+ main().catch(console.error);
1386
+ //# sourceMappingURL=index.js.map