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