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.
- package/Daedalus.bat +18 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/agents/agent.d.ts +10 -0
- package/dist/agents/agent.d.ts.map +1 -0
- package/dist/agents/agent.js +3 -0
- package/dist/agents/agent.js.map +1 -0
- package/dist/agents/orchestrator.d.ts +18 -0
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/agents/orchestrator.js +171 -0
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/roles.d.ts +14 -0
- package/dist/agents/roles.d.ts.map +1 -0
- package/dist/agents/roles.js +126 -0
- package/dist/agents/roles.js.map +1 -0
- package/dist/config/index.d.ts +485 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +237 -0
- package/dist/config/index.js.map +1 -0
- package/dist/highlight.d.ts +4 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +42 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1386 -0
- package/dist/index.js.map +1 -0
- package/dist/indexing/fts.d.ts +40 -0
- package/dist/indexing/fts.d.ts.map +1 -0
- package/dist/indexing/fts.js +121 -0
- package/dist/indexing/fts.js.map +1 -0
- package/dist/indexing/indexer.d.ts +22 -0
- package/dist/indexing/indexer.d.ts.map +1 -0
- package/dist/indexing/indexer.js +518 -0
- package/dist/indexing/indexer.js.map +1 -0
- package/dist/onboarding/wizard.d.ts +2 -0
- package/dist/onboarding/wizard.d.ts.map +1 -0
- package/dist/onboarding/wizard.js +231 -0
- package/dist/onboarding/wizard.js.map +1 -0
- package/dist/router/health.d.ts +6 -0
- package/dist/router/health.d.ts.map +1 -0
- package/dist/router/health.js +74 -0
- package/dist/router/health.js.map +1 -0
- package/dist/router/index.d.ts +33 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +214 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/rate-limiter.d.ts +7 -0
- package/dist/router/rate-limiter.d.ts.map +1 -0
- package/dist/router/rate-limiter.js +36 -0
- package/dist/router/rate-limiter.js.map +1 -0
- package/dist/router/types.d.ts +81 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +3 -0
- package/dist/router/types.js.map +1 -0
- package/dist/session/jsonl.d.ts +20 -0
- package/dist/session/jsonl.d.ts.map +1 -0
- package/dist/session/jsonl.js +61 -0
- package/dist/session/jsonl.js.map +1 -0
- package/dist/session/manager.d.ts +42 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +184 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/session/memory.d.ts +23 -0
- package/dist/session/memory.d.ts.map +1 -0
- package/dist/session/memory.js +88 -0
- package/dist/session/memory.js.map +1 -0
- package/dist/session/sqlite.d.ts +59 -0
- package/dist/session/sqlite.d.ts.map +1 -0
- package/dist/session/sqlite.js +174 -0
- package/dist/session/sqlite.js.map +1 -0
- package/dist/tools/builtin/delegation.d.ts +17 -0
- package/dist/tools/builtin/delegation.d.ts.map +1 -0
- package/dist/tools/builtin/delegation.js +85 -0
- package/dist/tools/builtin/delegation.js.map +1 -0
- package/dist/tools/builtin/diff-ui.d.ts +21 -0
- package/dist/tools/builtin/diff-ui.d.ts.map +1 -0
- package/dist/tools/builtin/diff-ui.js +211 -0
- package/dist/tools/builtin/diff-ui.js.map +1 -0
- package/dist/tools/builtin/files.d.ts +29 -0
- package/dist/tools/builtin/files.d.ts.map +1 -0
- package/dist/tools/builtin/files.js +286 -0
- package/dist/tools/builtin/files.js.map +1 -0
- package/dist/tools/builtin/git.d.ts +7 -0
- package/dist/tools/builtin/git.d.ts.map +1 -0
- package/dist/tools/builtin/git.js +11 -0
- package/dist/tools/builtin/git.js.map +1 -0
- package/dist/tools/builtin/indexing.d.ts +22 -0
- package/dist/tools/builtin/indexing.d.ts.map +1 -0
- package/dist/tools/builtin/indexing.js +159 -0
- package/dist/tools/builtin/indexing.js.map +1 -0
- package/dist/tools/builtin/project-config.d.ts +17 -0
- package/dist/tools/builtin/project-config.d.ts.map +1 -0
- package/dist/tools/builtin/project-config.js +66 -0
- package/dist/tools/builtin/project-config.js.map +1 -0
- package/dist/tools/builtin/terminal.d.ts +7 -0
- package/dist/tools/builtin/terminal.d.ts.map +1 -0
- package/dist/tools/builtin/terminal.js +99 -0
- package/dist/tools/builtin/terminal.js.map +1 -0
- package/dist/tools/builtin/todo.d.ts +20 -0
- package/dist/tools/builtin/todo.d.ts.map +1 -0
- package/dist/tools/builtin/todo.js +36 -0
- package/dist/tools/builtin/todo.js.map +1 -0
- package/dist/tools/builtin/web.d.ts +10 -0
- package/dist/tools/builtin/web.d.ts.map +1 -0
- package/dist/tools/builtin/web.js +67 -0
- package/dist/tools/builtin/web.js.map +1 -0
- package/dist/tools/daedalus-spinner.d.ts +29 -0
- package/dist/tools/daedalus-spinner.d.ts.map +1 -0
- package/dist/tools/daedalus-spinner.js +77 -0
- package/dist/tools/daedalus-spinner.js.map +1 -0
- package/dist/tools/definitions.d.ts +5 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +296 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/executor.d.ts +4 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +86 -0
- package/dist/tools/executor.js.map +1 -0
- package/dist/tools/mcp/http.d.ts +23 -0
- package/dist/tools/mcp/http.d.ts.map +1 -0
- package/dist/tools/mcp/http.js +200 -0
- package/dist/tools/mcp/http.js.map +1 -0
- package/dist/tools/mcp/registry.d.ts +16 -0
- package/dist/tools/mcp/registry.d.ts.map +1 -0
- package/dist/tools/mcp/registry.js +92 -0
- package/dist/tools/mcp/registry.js.map +1 -0
- package/dist/tools/mcp/stdio.d.ts +26 -0
- package/dist/tools/mcp/stdio.d.ts.map +1 -0
- package/dist/tools/mcp/stdio.js +157 -0
- package/dist/tools/mcp/stdio.js.map +1 -0
- package/dist/tools/mcp/tool-executor.d.ts +3 -0
- package/dist/tools/mcp/tool-executor.d.ts.map +1 -0
- package/dist/tools/mcp/tool-executor.js +23 -0
- package/dist/tools/mcp/tool-executor.js.map +1 -0
- package/dist/tools/mcp/types.d.ts +26 -0
- package/dist/tools/mcp/types.d.ts.map +1 -0
- package/dist/tools/mcp/types.js +3 -0
- package/dist/tools/mcp/types.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- 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
|