daedalus-cli 1.83.7 → 1.83.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/AGENTS.md +31 -10
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +3 -3
  4. package/dist/agents/ensemble.d.ts.map +1 -1
  5. package/dist/agents/ensemble.js +2 -2
  6. package/dist/agents/ensemble.js.map +1 -1
  7. package/dist/agents/orchestrator-types.d.ts +20 -0
  8. package/dist/agents/orchestrator-types.d.ts.map +1 -0
  9. package/dist/agents/orchestrator-types.js +3 -0
  10. package/dist/agents/orchestrator-types.js.map +1 -0
  11. package/dist/agents/orchestrator-validation.d.ts +19 -0
  12. package/dist/agents/orchestrator-validation.d.ts.map +1 -0
  13. package/dist/agents/orchestrator-validation.js +227 -0
  14. package/dist/agents/orchestrator-validation.js.map +1 -0
  15. package/dist/agents/orchestrator-verification.d.ts +28 -0
  16. package/dist/agents/orchestrator-verification.d.ts.map +1 -0
  17. package/dist/agents/orchestrator-verification.js +355 -0
  18. package/dist/agents/orchestrator-verification.js.map +1 -0
  19. package/dist/agents/orchestrator.d.ts +1 -49
  20. package/dist/agents/orchestrator.d.ts.map +1 -1
  21. package/dist/agents/orchestrator.js +40 -678
  22. package/dist/agents/orchestrator.js.map +1 -1
  23. package/dist/agents/orchestrator.test.js +33 -81
  24. package/dist/agents/orchestrator.test.js.map +1 -1
  25. package/dist/commands/agents.d.ts +3 -0
  26. package/dist/commands/agents.d.ts.map +1 -0
  27. package/dist/commands/agents.js +886 -0
  28. package/dist/commands/agents.js.map +1 -0
  29. package/dist/commands/context.d.ts +3 -0
  30. package/dist/commands/context.d.ts.map +1 -0
  31. package/dist/commands/context.js +875 -0
  32. package/dist/commands/context.js.map +1 -0
  33. package/dist/commands/dev.d.ts +3 -0
  34. package/dist/commands/dev.d.ts.map +1 -0
  35. package/dist/commands/dev.js +820 -0
  36. package/dist/commands/dev.js.map +1 -0
  37. package/dist/commands/index.d.ts +5 -0
  38. package/dist/commands/index.d.ts.map +1 -0
  39. package/dist/commands/index.js +88 -0
  40. package/dist/commands/index.js.map +1 -0
  41. package/dist/commands/types.d.ts +45 -0
  42. package/dist/commands/types.d.ts.map +1 -0
  43. package/dist/commands/types.js +2 -0
  44. package/dist/commands/types.js.map +1 -0
  45. package/dist/commands.d.ts +2 -46
  46. package/dist/commands.d.ts.map +1 -1
  47. package/dist/commands.js +1 -2639
  48. package/dist/commands.js.map +1 -1
  49. package/dist/config/index.d.ts +78 -78
  50. package/dist/model.d.ts.map +1 -1
  51. package/dist/model.js +11 -6
  52. package/dist/model.js.map +1 -1
  53. package/package.json +1 -1
package/dist/commands.js CHANGED
@@ -1,2640 +1,2 @@
1
- // Command Registry and Router for Daedalus CLI
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { execSync } from 'child_process';
5
- import pc from 'picocolors';
6
- import { executeToolCalls } from './tools/executor.js';
7
- import { discoverLocalServers, saveConfig } from './config/index.js';
8
- import { getSessionTodos } from './tools/builtin/todo.js';
9
- import { getTurns } from './session/sqlite.js';
10
- import { saveProfile } from './profile.js';
11
- import { extractAndSave } from './extraction.js';
12
- import { printUserTurn, turnSeparator } from './formatting.js';
13
- import { getClipboardText, getClipboardImage } from './clipboard.js';
14
- import { spawnBackgroundAgent } from './agents/background.js';
15
- import { handleSpecCommand, getGitRepoInfo } from './agents/loop.js';
16
- import { createSessionBranch, checkoutSessionBranch, listSessionBranches, mergeSessionBranch, } from './session/branching.js';
17
- export const commandsList = [
18
- {
19
- name: '/add',
20
- description: 'Add file to context',
21
- usage: '/add [filepath]',
22
- helpText: 'Add a file to the active prompt context. If filepath is omitted, runs an interactive terminal file selector.',
23
- execute: async (args, ctx) => {
24
- const fileArg = args.trim();
25
- if (!fileArg) {
26
- const { runInteractiveFileSelector } = await import('./session/selector.js');
27
- ctx.rl.pause();
28
- const result = await runInteractiveFileSelector(process.cwd(), ctx.config.indexing.exclude, new Set(ctx.activeFiles.keys()));
29
- ctx.rl.resume();
30
- if (result !== null) {
31
- ctx.activeFiles.clear();
32
- for (const absPath of result) {
33
- const rel = path.relative(process.cwd(), absPath);
34
- ctx.activeFiles.set(absPath, rel);
35
- }
36
- ctx.toolContext.activeFiles = new Map(ctx.activeFiles);
37
- console.log(pc.green(`\n[OK] Active context files updated: ${ctx.activeFiles.size} file(s)`));
38
- }
39
- }
40
- else {
41
- const cleanPath = fileArg.replace(/^["']|["']$/g, '');
42
- const absPath = path.resolve(cleanPath);
43
- ctx.activeFiles.set(absPath, cleanPath);
44
- ctx.toolContext.activeFiles = new Map(ctx.activeFiles);
45
- console.log(pc.green(`[OK] Added file to context: ${pc.bold(cleanPath)}`));
46
- }
47
- }
48
- },
49
- {
50
- name: '/remove',
51
- description: 'Remove file from context',
52
- usage: '/remove <filepath>',
53
- helpText: 'Remove a file from the active prompt context.',
54
- execute: async (args, ctx) => {
55
- const fileArg = args.trim();
56
- if (!fileArg) {
57
- console.log(pc.red('[WARN] Please specify a file path. Example: /remove src/App.tsx'));
58
- }
59
- else {
60
- const cleanPath = fileArg.replace(/^["']|["']$/g, '');
61
- const absPath = path.resolve(cleanPath);
62
- if (ctx.activeFiles.delete(absPath)) {
63
- ctx.toolContext.activeFiles = new Map(ctx.activeFiles);
64
- console.log(pc.green(`[OK] Removed file from context: ${pc.bold(cleanPath)}`));
65
- }
66
- else {
67
- console.log(pc.yellow(`[WARN] File was not in context: ${cleanPath}`));
68
- }
69
- }
70
- }
71
- },
72
- {
73
- name: '/context',
74
- description: 'Show active file context',
75
- execute: async (args, ctx) => {
76
- console.log(pc.bold('\n--- Monitored Files in Context ---'));
77
- if (ctx.activeFiles.size === 0) {
78
- console.log(pc.gray(' (No active files. Use "/add <filepath>" to add files)'));
79
- }
80
- else {
81
- ctx.activeFiles.forEach((filename) => {
82
- console.log(` • ${pc.cyan(filename)}`);
83
- });
84
- }
85
- console.log(pc.bold('----------------------------------'));
86
- }
87
- },
88
- {
89
- name: '/paste',
90
- description: 'Paste clipboard text/image as message',
91
- execute: async (args, ctx) => {
92
- const extra = args.trim();
93
- if (extra && !extra.startsWith('http')) {
94
- const cleanPath = extra.replace(/^["']|["']$/g, '');
95
- const filePath = path.resolve(cleanPath);
96
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
97
- const ext = path.extname(filePath).toLowerCase();
98
- if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext)) {
99
- const imgBuffer = fs.readFileSync(filePath);
100
- const base64 = imgBuffer.toString('base64');
101
- const message = 'What do you see in this image?';
102
- printUserTurn(`${path.basename(filePath)} (image)`);
103
- try {
104
- const filesContext = ctx.buildFileContext();
105
- const indexCtx = await ctx.buildIndexContext(message);
106
- const userContent = `${indexCtx}${filesContext}User Prompt: ${message}`;
107
- await ctx.callModelWithTools(userContent, base64);
108
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
109
- }
110
- catch {
111
- try {
112
- const filesContext = ctx.buildFileContext();
113
- const userContent = `${filesContext}User Prompt: ${message}`;
114
- console.log(pc.yellow('\n [RETRY] Trying fallback mode...'));
115
- await ctx.callModelWithFallback(userContent, base64);
116
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
117
- }
118
- catch (fallbackErr) {
119
- const firstLine = (fallbackErr.message || '').split('\n')[0];
120
- console.log(pc.red(`\n ${pc.bold('[ERROR]')} Fallback also failed: ${firstLine}`));
121
- }
122
- }
123
- turnSeparator();
124
- return;
125
- }
126
- }
127
- }
128
- const imgPath = getClipboardImage(ctx.cliTempDir);
129
- if (imgPath) {
130
- const imgBuffer = fs.readFileSync(imgPath);
131
- fs.unlinkSync(imgPath);
132
- const base64 = imgBuffer.toString('base64');
133
- const message = extra || 'What do you see in this image?';
134
- printUserTurn(`${message} (image)`);
135
- try {
136
- const filesContext = ctx.buildFileContext();
137
- const indexCtx = await ctx.buildIndexContext(message);
138
- const userContent = `${indexCtx}${filesContext}User Prompt: ${message}`;
139
- await ctx.callModelWithTools(userContent, base64);
140
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
141
- }
142
- catch {
143
- try {
144
- const filesContext = ctx.buildFileContext();
145
- const userContent = `${filesContext}User Prompt: ${message}`;
146
- console.log(pc.yellow('\n [RETRY] Trying fallback mode...'));
147
- await ctx.callModelWithFallback(userContent, base64);
148
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
149
- }
150
- catch (fallbackErr) {
151
- const firstLine = (fallbackErr.message || '').split('\n')[0];
152
- console.log(pc.red(`\n ${pc.bold('[ERROR]')} Fallback also failed: ${firstLine}`));
153
- }
154
- }
155
- turnSeparator();
156
- return;
157
- }
158
- const clipboard = getClipboardText();
159
- if (!clipboard) {
160
- console.log(pc.red('[WARN] Clipboard is empty or inaccessible.'));
161
- return;
162
- }
163
- const fullMessage = extra ? `${clipboard}\n\n${extra}` : clipboard;
164
- if (fullMessage.includes('2026-07-27 022605.png')) {
165
- console.log(pc.green('[OK] Attached image: 2026-07-27 022605.png'));
166
- }
167
- else if (fullMessage.length > 0) {
168
- console.log(pc.green(`[OK] Pasted ${fullMessage.split('\n').length} lines of text`));
169
- }
170
- try {
171
- const filesContext = ctx.buildFileContext();
172
- const indexCtx = await ctx.buildIndexContext(fullMessage);
173
- const userContent = `${indexCtx}${filesContext}User Prompt: ${fullMessage}`;
174
- await ctx.callModelWithTools(userContent);
175
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
176
- }
177
- catch { /* ignored */ }
178
- turnSeparator();
179
- }
180
- },
181
- {
182
- name: '/clear',
183
- description: 'Clear conversation history',
184
- execute: async (args, ctx) => {
185
- ctx.messages.length = 0;
186
- ctx.messages.push({ role: 'system', content: ctx.getSystemPromptWithMemory() });
187
- console.log(pc.green('[OK] Conversation history cleared!'));
188
- }
189
- },
190
- {
191
- name: '/system',
192
- description: 'Print the current active system prompt (including loaded rules)',
193
- execute: async (args, ctx) => {
194
- const sysMsg = ctx.messages.find(m => m.role === 'system');
195
- if (sysMsg) {
196
- console.log(pc.bold('\n--- Current System Prompt ---'));
197
- console.log(sysMsg.content);
198
- console.log(pc.bold('-----------------------------'));
199
- }
200
- else {
201
- console.log(pc.red('[WARN] No active system prompt found in conversation.'));
202
- }
203
- }
204
- },
205
- {
206
- name: '/spawn',
207
- aliases: ['/delegate'],
208
- description: 'Spawn sub-agent: /spawn [--bg] <role> <task>',
209
- usage: '/spawn [--bg] <role> <task> OR /delegate [--bg] <task> to <role>',
210
- helpText: 'Spawns a specialized agent to execute a coding or research task.\n\nRoles:\n coder Implements, patches, and refactors code files\n reviewer Critically reviews changes and runs tests\n debugger Tackles compilation errors, runtime failures, and logs\n researcher Scans repository structure and reads doc resources\n planner Outlines architecture plans and coordinates execution\n\nOptions:\n --bg Runs the agent asynchronously in the background',
211
- execute: async (args, ctx) => {
212
- let role = '';
213
- let task = '';
214
- let isBackground = false;
215
- let cleanedArgs = args.trim();
216
- if (cleanedArgs.startsWith('--bg ')) {
217
- isBackground = true;
218
- cleanedArgs = cleanedArgs.substring(5).trim();
219
- }
220
- else if (cleanedArgs.endsWith(' --bg')) {
221
- isBackground = true;
222
- cleanedArgs = cleanedArgs.substring(0, cleanedArgs.length - 5).trim();
223
- }
224
- if (cleanedArgs.includes(' to ')) {
225
- const match = cleanedArgs.match(/^(.+)\s+to\s+(\w+)$/i);
226
- if (match) {
227
- task = match[1].trim();
228
- role = match[2].toLowerCase();
229
- }
230
- }
231
- else {
232
- const parts = cleanedArgs.split(/\s+/);
233
- if (parts.length >= 2) {
234
- role = parts[0].toLowerCase();
235
- task = cleanedArgs.substring(parts[0].length).trim();
236
- }
237
- }
238
- const validRoles = ['coder', 'reviewer', 'debugger', 'researcher', 'planner'];
239
- if (!role || !task) {
240
- console.log(pc.red('[WARN] Usage: /spawn [--bg] <role> <task> OR /delegate [--bg] <task> to <role>'));
241
- console.log(pc.gray(` Roles: ${validRoles.join(', ')}`));
242
- return;
243
- }
244
- if (!validRoles.includes(role)) {
245
- console.log(pc.red(`[WARN] Unknown role: ${role}. Valid: ${validRoles.join(', ')}`));
246
- return;
247
- }
248
- const context = `Active files: ${Array.from(ctx.activeFiles.values()).join(', ') || 'none'}`;
249
- if (isBackground) {
250
- console.log(pc.cyan(`\n[SPAWN] Spawning ${role} agent in background for: ${task.slice(0, 80)}...`));
251
- const id = spawnBackgroundAgent(role, task, context, ctx.toolContext);
252
- console.log(pc.green(`[OK] Spawned background task #${id} (${role}) successfully.`));
253
- console.log(pc.gray(` Check status via /tasks, view logs/results via /task ${id}, or cancel via /task kill ${id}`));
254
- return;
255
- }
256
- console.log(pc.cyan(`\n[SPAWN] Spawning ${role} agent for: ${task.slice(0, 80)}...`));
257
- const fakeToolCall = {
258
- id: `call_${Date.now()}`,
259
- type: 'function',
260
- function: {
261
- name: 'delegate_task',
262
- arguments: JSON.stringify({ goal: task, context, role }),
263
- },
264
- };
265
- const results = await executeToolCalls([fakeToolCall], ctx.toolContext);
266
- for (const result of results) {
267
- const status = result.success ? pc.green('✔') : pc.red('✗');
268
- console.log(`\n${status} ${role} agent completed`);
269
- console.log(pc.white(result.content));
270
- if (!result.success && result.error) {
271
- console.log(pc.red(`Error: ${result.error}`));
272
- }
273
- }
274
- }
275
- },
276
- {
277
- name: '/tasks',
278
- description: 'List background agent tasks',
279
- usage: '/tasks',
280
- helpText: 'Display a list of all active, completed, failed, or cancelled background agent tasks.',
281
- execute: async (_args, _ctx) => {
282
- const { backgroundJobs } = await import('./agents/background.js');
283
- if (backgroundJobs.size === 0) {
284
- console.log(pc.gray('No background tasks found.'));
285
- return;
286
- }
287
- console.log(pc.cyan('\n--- Background Tasks ---'));
288
- for (const job of backgroundJobs.values()) {
289
- const duration = job.finishedAt
290
- ? `${Math.round((job.finishedAt - job.startedAt) / 1000)}s`
291
- : `${Math.round((Date.now() - job.startedAt) / 1000)}s elapsed`;
292
- let statusStr;
293
- if (job.status === 'running') {
294
- statusStr = pc.blue('RUNNING');
295
- }
296
- else if (job.status === 'completed') {
297
- statusStr = pc.green('COMPLETED');
298
- }
299
- else if (job.status === 'failed') {
300
- statusStr = pc.red('FAILED');
301
- }
302
- else {
303
- statusStr = pc.yellow('CANCELLED');
304
- }
305
- console.log(`[#${job.id}] ${pc.bold(job.role)} — ${statusStr} (${duration})`);
306
- console.log(pc.gray(` Goal: ${job.goal.slice(0, 80)}`));
307
- }
308
- }
309
- },
310
- {
311
- name: '/task',
312
- description: 'Manage background task: /task <id> | /task kill <id>',
313
- usage: '/task <id> OR /task kill <id>',
314
- helpText: 'Inspect or terminate background agent tasks.\n\nArguments:\n <id> Show detail info, logs, and output/result of a background task\n kill <id> Cancel and terminate a running background task',
315
- execute: async (args, _ctx) => {
316
- const { backgroundJobs, killBackgroundAgent } = await import('./agents/background.js');
317
- const trimmed = args.trim();
318
- if (!trimmed) {
319
- console.log(pc.red('[WARN] Usage: /task <id> OR /task kill <id>'));
320
- return;
321
- }
322
- if (trimmed.startsWith('kill ')) {
323
- const idStr = trimmed.substring(5).trim();
324
- const id = parseInt(idStr, 10);
325
- if (isNaN(id)) {
326
- console.log(pc.red(`[WARN] Invalid task ID: ${idStr}`));
327
- return;
328
- }
329
- const killed = killBackgroundAgent(id);
330
- if (killed) {
331
- console.log(pc.green(`[OK] Task #${id} cancelled.`));
332
- }
333
- else {
334
- console.log(pc.red(`[WARN] Task #${id} is not running or not found.`));
335
- }
336
- return;
337
- }
338
- const id = parseInt(trimmed, 10);
339
- if (isNaN(id)) {
340
- console.log(pc.red('[WARN] Usage: /task <id> OR /task kill <id>'));
341
- return;
342
- }
343
- const job = backgroundJobs.get(id);
344
- if (!job) {
345
- console.log(pc.red(`[WARN] Task #${id} not found.`));
346
- return;
347
- }
348
- console.log(pc.cyan(`\n--- Task #${job.id} (${job.role}) ---`));
349
- console.log(`Goal: ${job.goal}`);
350
- console.log(`Status: ${job.status.toUpperCase()}`);
351
- console.log(`Started: ${new Date(job.startedAt).toLocaleTimeString()}`);
352
- if (job.finishedAt) {
353
- console.log(`Finished: ${new Date(job.finishedAt).toLocaleTimeString()}`);
354
- console.log(`Duration: ${Math.round((job.finishedAt - job.startedAt) / 1000)}s`);
355
- }
356
- if (job.status === 'completed' && job.result) {
357
- console.log(pc.white('\n--- Result ---'));
358
- console.log(job.result);
359
- }
360
- else if (job.status === 'failed' && job.error) {
361
- console.log(pc.red(`\n--- Error ---`));
362
- console.log(job.error);
363
- }
364
- else if (job.status === 'running') {
365
- console.log(pc.gray('\nThis task is still running. Check again later.'));
366
- }
367
- }
368
- },
369
- {
370
- name: '/orchestrate',
371
- aliases: ['/orc', '/run', '/o'],
372
- description: 'Orchestrate agents for a goal',
373
- usage: '/orchestrate <goal>',
374
- helpText: 'Spawns the Orchestration system to plan, execute, and verify a high-level coding goal.\nOrchestrate generates a task.md checklist, coordinates specialized sub-agents, runs verification commands, and handles self-repair loops automatically.',
375
- execute: async (args, ctx) => {
376
- const pendingPlan = ctx.sessionManager.getState('orchestrate_plan');
377
- const pendingGoal = ctx.sessionManager.getState('orchestrate_goal');
378
- if (pendingPlan && pendingGoal) {
379
- const goal = args.trim();
380
- const shouldResume = !goal || goal.toLowerCase() === pendingGoal.toLowerCase();
381
- let proceed = false;
382
- if (shouldResume && process.env.DAEDALUS_AUTO_APPROVE === 'true') {
383
- proceed = true;
384
- }
385
- else if (shouldResume) {
386
- console.log(pc.yellow(`\n[INFO] Found a pending orchestration plan for: "${pendingGoal}"`));
387
- const answer = await ctx.askLine(`Would you like to resume it? [y]es / [n]o: `);
388
- const char = answer.trim().toLowerCase().slice(0, 1);
389
- if (char === 'y' || answer.trim() === '') {
390
- proceed = true;
391
- }
392
- }
393
- if (proceed) {
394
- console.log(pc.cyan(`\n[ORCHESTRATE] Resuming orchestration for: ${pendingGoal}`));
395
- const { Orchestrator } = await import('./agents/orchestrator.js');
396
- const orchestrator = new Orchestrator(ctx.router, ctx.messages, ctx.toolContext, ctx.sessionManager);
397
- const planText = ctx.sessionManager.getState('orchestrate_plan_text') || '';
398
- const taskIndex = ctx.sessionManager.getState('orchestrate_task_index') || 0;
399
- const prevResults = ctx.sessionManager.getState('orchestrate_results') || [];
400
- const result = await orchestrator.resume(pendingGoal, planText, pendingPlan, taskIndex, prevResults);
401
- console.log(pc.white(`\n${result}`));
402
- return;
403
- }
404
- else {
405
- ctx.sessionManager.saveState('orchestrate_plan', null);
406
- ctx.sessionManager.saveState('orchestrate_goal', null);
407
- ctx.sessionManager.saveState('orchestrate_task_index', null);
408
- ctx.sessionManager.saveState('orchestrate_results', null);
409
- ctx.sessionManager.saveState('orchestrate_plan_text', null);
410
- }
411
- }
412
- const goal = args.trim();
413
- if (!goal) {
414
- console.log(pc.red('[WARN] Usage: /orchestrate <goal>'));
415
- return;
416
- }
417
- console.log(pc.cyan(`\n[ORCHESTRATE] Starting orchestration for: ${goal}`));
418
- const { Orchestrator } = await import('./agents/orchestrator.js');
419
- const orchestrator = new Orchestrator(ctx.router, ctx.messages, ctx.toolContext, ctx.sessionManager);
420
- const result = await orchestrator.run(goal);
421
- console.log(pc.white(`\n${result}`));
422
- }
423
- },
424
- {
425
- name: '/memory',
426
- description: 'View project memory (facts & conventions)',
427
- execute: async (args, ctx) => {
428
- const mem = ctx.sessionManager.loadMemory();
429
- console.log(pc.bold('\n--- Project Facts & Conventions (Memory) ---'));
430
- console.log(pc.bold('Conventions:'));
431
- if (Object.keys(mem.conventions).length === 0) {
432
- console.log(pc.gray(' No conventions saved.'));
433
- }
434
- else {
435
- for (const [k, v] of Object.entries(mem.conventions)) {
436
- console.log(` • ${pc.cyan(k)}: ${v}`);
437
- }
438
- }
439
- console.log(pc.bold('\nFacts:'));
440
- if (mem.facts.length === 0) {
441
- console.log(pc.gray(' No facts saved.'));
442
- }
443
- else {
444
- mem.facts.forEach(f => {
445
- console.log(` • ${pc.cyan(f.key)}: ${f.value} (source: ${f.source})`);
446
- });
447
- }
448
- console.log(pc.bold('------------------------------------------'));
449
- }
450
- },
451
- {
452
- name: '/fact',
453
- description: 'Add a project fact to memory',
454
- execute: async (args, ctx) => {
455
- const eqIdx = args.indexOf('=');
456
- if (eqIdx < 0) {
457
- console.log(pc.red('[WARN] Usage: /fact <key> = <value>'));
458
- }
459
- else {
460
- const key = args.slice(0, eqIdx).trim();
461
- const value = args.slice(eqIdx + 1).trim();
462
- ctx.sessionManager.addFact(key, value, 'user');
463
- console.log(pc.green(`[OK] Saved fact: ${key} = ${value}`));
464
- }
465
- }
466
- },
467
- {
468
- name: '/convention',
469
- description: 'Add a project convention to memory',
470
- execute: async (args, ctx) => {
471
- const eqIdx = args.indexOf('=');
472
- if (eqIdx < 0) {
473
- console.log(pc.red('[WARN] Usage: /convention <key> = <value>'));
474
- }
475
- else {
476
- const key = args.slice(0, eqIdx).trim();
477
- const value = args.slice(eqIdx + 1).trim();
478
- ctx.sessionManager.setConvention(key, value);
479
- console.log(pc.green(`[OK] Saved convention: ${key} = ${value}`));
480
- }
481
- }
482
- },
483
- {
484
- name: '/extract',
485
- description: 'Manually extract facts from session',
486
- execute: async (args, ctx) => {
487
- console.log(pc.dim(' [EXTRACT] Extracting facts from conversation...'));
488
- await extractAndSave(ctx.router, ctx.sessionManager, ctx.messages);
489
- }
490
- },
491
- {
492
- name: '/summarize',
493
- aliases: ['/compress'],
494
- description: 'Summarize older conversation history to save tokens and speed up turns',
495
- usage: '/summarize [keepTurns]',
496
- helpText: 'Manually compresses older conversation turns into a compact technical summary. Use this if the session grows large or model turns begin slowing down.',
497
- execute: async (args, ctx) => {
498
- const keepTurnsArg = parseInt(args.trim(), 10);
499
- const keepTurns = isNaN(keepTurnsArg) || keepTurnsArg < 1 ? 2 : keepTurnsArg;
500
- const userOrAssistantCount = ctx.messages.filter(m => m.role === 'user' || m.role === 'assistant').length;
501
- if (userOrAssistantCount <= keepTurns * 2) {
502
- console.log(pc.yellow(`[INFO] Conversation is already concise (${userOrAssistantCount} messages). At least ${keepTurns * 2 + 1} messages are needed to summarize.`));
503
- return;
504
- }
505
- console.log(pc.cyan(`[SUMMARIZE] Compressing older conversation cycles (keeping last ${keepTurns} turns intact)...`));
506
- const { summarizeMessages } = await import('./session/summarize.js');
507
- const summarizeFn = async (sysPrompt, userContent) => {
508
- try {
509
- const resp = await ctx.router.chat.completions.create({
510
- model: 'intelligence',
511
- messages: [
512
- { role: 'system', content: sysPrompt },
513
- { role: 'user', content: userContent },
514
- ],
515
- temperature: 0.3,
516
- max_tokens: 600,
517
- });
518
- return resp.choices[0]?.message?.content || '';
519
- }
520
- catch {
521
- return '';
522
- }
523
- };
524
- const result = await summarizeMessages(ctx.messages, 0, summarizeFn, keepTurns);
525
- if (result.summarizedTurns > 0) {
526
- ctx.sessionManager.saveSessionState?.(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
527
- console.log(pc.green(`\n[OK] Successfully summarized ${result.summarizedTurns} turn(s), saving ~${Math.round(result.savedTokens / 1000)}k tokens!`));
528
- }
529
- else {
530
- console.log(pc.yellow('[INFO] No older turns were large enough to summarize.'));
531
- }
532
- }
533
- },
534
- {
535
- name: '/profile',
536
- description: 'View or set user profile info',
537
- usage: '/profile [view | name = <name> | bio = <bio>]',
538
- helpText: 'Manage your persistent developer profile. Profile facts are automatically injected into the model context.\n\nSubcommands:\n view Display your current name and bio details\n name = <value> Update your profile name\n bio = <value> Update your bio/background facts',
539
- execute: async (args, ctx) => {
540
- const rest = args.trim();
541
- if (!rest || rest === 'view') {
542
- console.log(pc.bold('\n--- Your Profile ---'));
543
- console.log(` ${pc.cyan('Name')}: ${ctx.userProfile.name || '(not set)'}`);
544
- console.log(` ${pc.cyan('Bio')}: ${ctx.userProfile.bio || '(not set)'}`);
545
- if (ctx.userProfile.updatedAt) {
546
- console.log(pc.gray(` Last updated: ${new Date(ctx.userProfile.updatedAt).toLocaleString()}`));
547
- }
548
- console.log(pc.dim(' Set name: /profile name = Your Name'));
549
- console.log(pc.dim(' Set bio: /profile bio = Tell me about yourself'));
550
- return;
551
- }
552
- const eqIdx = rest.indexOf('=');
553
- if (eqIdx < 0) {
554
- if (rest.startsWith('name ')) {
555
- ctx.userProfile.name = rest.substring(5).trim();
556
- saveProfile(ctx.userProfile);
557
- console.log(pc.green(`[OK] Profile name set: ${ctx.userProfile.name}`));
558
- return;
559
- }
560
- if (rest.startsWith('bio ')) {
561
- ctx.userProfile.bio = rest.substring(4).trim();
562
- saveProfile(ctx.userProfile);
563
- console.log(pc.green('[OK] Profile bio set.'));
564
- return;
565
- }
566
- }
567
- else {
568
- const key = rest.slice(0, eqIdx).trim().toLowerCase();
569
- const val = rest.slice(eqIdx + 1).trim();
570
- if (key === 'name') {
571
- ctx.userProfile.name = val;
572
- saveProfile(ctx.userProfile);
573
- console.log(pc.green(`[OK] Profile name set: ${ctx.userProfile.name}`));
574
- return;
575
- }
576
- else if (key === 'bio') {
577
- ctx.userProfile.bio = val;
578
- saveProfile(ctx.userProfile);
579
- console.log(pc.green('[OK] Profile bio set.'));
580
- return;
581
- }
582
- }
583
- console.log(pc.red('[WARN] Usage: /profile view | /profile name = <name> | /profile bio = <bio>'));
584
- }
585
- },
586
- {
587
- name: '/style',
588
- description: 'Set your coding style preferences',
589
- usage: '/style [view | <preferences>]',
590
- helpText: 'Manage your persistent coding style preferences (e.g. tabs vs spaces, preferred library conventions, language-specific choices). Style instructions are auto-injected into all sessions.',
591
- execute: async (args, ctx) => {
592
- const rest = args.trim();
593
- if (!rest || rest === 'view') {
594
- console.log(pc.bold('\n--- Coding Style ---'));
595
- console.log(` ${ctx.userProfile.style || '(not set)'}`);
596
- console.log(pc.dim(' Set: /style <your coding preferences>'));
597
- console.log(pc.dim(' Example: /style I prefer tabs, functional style, descriptive variable names'));
598
- return;
599
- }
600
- ctx.userProfile.style = rest;
601
- saveProfile(ctx.userProfile);
602
- console.log(pc.green('[OK] Coding style saved. It will be injected into every session.'));
603
- }
604
- },
605
- {
606
- name: '/lite',
607
- description: 'Show Daedalus Lite documentation',
608
- usage: '/lite',
609
- helpText: 'Display link to Daedalus Lite documentation for building your own version of Daedalus',
610
- execute: async (_args, _ctx) => {
611
- console.log(pc.bold('\n--- Daedalus Lite Documentation ---'));
612
- console.log(pc.gray(' Build your own version of Daedalus:'));
613
- console.log(pc.cyan(' https://bgill55.github.io/daedalus-lite/'));
614
- console.log(pc.bold('----------------------------------'));
615
- }
616
- },
617
- {
618
- name: '/session',
619
- description: 'Manage chat sessions & branches: /session <list|load|new|branch|checkout|merge|export>',
620
- usage: '/session <list|load|new|delete|export|branch|checkout|merge|branches> [args]',
621
- helpText: 'Manage, snapshot, branch, load, save, and export conversation sessions.\n\nSubcommands:\n list List all saved sessions for current project\n load <id> Load a saved session by ID\n new [title] Start a new conversation session\n delete <id> Delete a saved session by ID\n export [filepath] Export the current session transcript to Markdown\n search <query> Search past sessions for keyword\n rename <title> Rename the active session\n branch <name> Create a new session branch snapshot from current state\n checkout <name> Switch active REPL session context to an existing branch\n branches Display hierarchical tree of session branches\n merge <name> Merge code patches & trajectory turns from branch into current session',
622
- execute: async (args, ctx) => {
623
- const parts = args.trim().split(/\s+/);
624
- const subcommand = parts[0]?.toLowerCase();
625
- const subcommandArg = parts.slice(1).join(' ').trim();
626
- const db = ctx.sessionManager.db;
627
- const sessionDir = path.join(ctx.configDir, 'sessions');
628
- const workspaceRoot = process.cwd();
629
- const currentSessionId = ctx.toolContext.sessionId || 'default';
630
- if (subcommand === 'branch') {
631
- if (!subcommandArg) {
632
- console.log(pc.red('[WARN] Usage: /session branch <name>'));
633
- return;
634
- }
635
- try {
636
- const branch = createSessionBranch(db, currentSessionId, subcommandArg, workspaceRoot, sessionDir);
637
- console.log(pc.green(`[OK] Created session branch '${branch.name}' (id: ${branch.id.slice(0, 8)}) at step ${branch.branch_point_step}.`));
638
- }
639
- catch (err) {
640
- const msg = err instanceof Error ? err.message : String(err);
641
- console.log(pc.red(`[ERROR] ${msg}`));
642
- }
643
- return;
644
- }
645
- if (subcommand === 'checkout') {
646
- if (!subcommandArg) {
647
- console.log(pc.red('[WARN] Usage: /session checkout <name>'));
648
- return;
649
- }
650
- try {
651
- const branch = checkoutSessionBranch(db, subcommandArg);
652
- ctx.toolContext.sessionId = branch.id;
653
- console.log(pc.green(`[OK] Switched session context to '${branch.name}' [id: ${branch.id.slice(0, 8)}].`));
654
- }
655
- catch (err) {
656
- const msg = err instanceof Error ? err.message : String(err);
657
- console.log(pc.red(`[ERROR] ${msg}`));
658
- }
659
- return;
660
- }
661
- if (subcommand === 'branches') {
662
- const treeStr = listSessionBranches(db);
663
- console.log(pc.bold('\n--- Session Branches ---'));
664
- console.log(treeStr);
665
- console.log(pc.bold('------------------------\n'));
666
- return;
667
- }
668
- if (subcommand === 'merge') {
669
- if (!subcommandArg) {
670
- console.log(pc.red('[WARN] Usage: /session merge <name>'));
671
- return;
672
- }
673
- try {
674
- const result = await mergeSessionBranch(db, subcommandArg, workspaceRoot, sessionDir);
675
- if (result.success) {
676
- console.log(pc.green(`[OK] ${result.message}`));
677
- }
678
- else {
679
- console.log(pc.red(`[ERROR] ${result.message}`));
680
- }
681
- }
682
- catch (err) {
683
- const msg = err instanceof Error ? err.message : String(err);
684
- console.log(pc.red(`[ERROR] ${msg}`));
685
- }
686
- return;
687
- }
688
- if (!subcommand || subcommand === 'list') {
689
- const sessions = ctx.sessionManager.getSessionsForProject();
690
- console.log(pc.bold('\n--- Past Sessions ---'));
691
- if (sessions.length === 0) {
692
- console.log(pc.gray(' No past sessions found.'));
693
- }
694
- else {
695
- sessions.forEach((s) => {
696
- const currentTag = s.id === ctx.sessionManager.sessionId ? pc.green(' (current)') : '';
697
- const dateStr = new Date(s.updated_at).toLocaleString();
698
- console.log(` • ${pc.cyan(s.id)}${currentTag}`);
699
- console.log(` Title: ${pc.white(s.title)}`);
700
- console.log(` Updated: ${pc.dim(dateStr)}`);
701
- });
702
- }
703
- console.log(pc.bold('---------------------\n'));
704
- console.log(pc.gray('Use `/session load <id>` to resume a past session.'));
705
- console.log(pc.gray('Use `/session new [title]` to start a new session.'));
706
- console.log(pc.gray('Use `/session branch <name>` to snapshot & branch current session.'));
707
- console.log(pc.gray('Use `/session checkout <name>` to switch to a branch.'));
708
- console.log(pc.gray('Use `/session merge <name>` to merge branch edits.'));
709
- console.log(pc.gray('Use `/session delete <id>` to delete a session.'));
710
- console.log(pc.gray('Use `/session export [path]` to export session transcript.'));
711
- return;
712
- }
713
- if (subcommand === 'load') {
714
- if (!subcommandArg) {
715
- console.log(pc.red('Usage: /session load <id>'));
716
- return;
717
- }
718
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
719
- const sessions = ctx.sessionManager.getSessionsForProject();
720
- const target = sessions.find((s) => s.id === subcommandArg || s.id.startsWith(subcommandArg));
721
- if (!target) {
722
- console.log(pc.red(`Session not found: ${subcommandArg}`));
723
- return;
724
- }
725
- const loaded = ctx.sessionManager.startSession(target.id, target.title);
726
- ctx.initializeSessionState(loaded);
727
- console.log(pc.green(`[OK] Loaded session: ${target.title} (${target.id})`));
728
- return;
729
- }
730
- if (subcommand === 'new') {
731
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
732
- const newTitle = subcommandArg || `Session ${new Date().toLocaleDateString()}`;
733
- const loaded = ctx.sessionManager.startSession(undefined, newTitle);
734
- ctx.initializeSessionState(loaded);
735
- console.log(pc.green(`[OK] Started new session: ${newTitle}`));
736
- return;
737
- }
738
- if (subcommand === 'delete') {
739
- if (!subcommandArg) {
740
- console.log(pc.red('Usage: /session delete <id>'));
741
- return;
742
- }
743
- ctx.sessionManager.deleteSession(subcommandArg);
744
- console.log(pc.green(`[OK] Deleted session: ${subcommandArg}`));
745
- return;
746
- }
747
- if (subcommand === 'export') {
748
- const defaultPath = `session-export-${Date.now()}.md`;
749
- const exportPath = subcommandArg || defaultPath;
750
- const lines = [];
751
- lines.push(`# Session Transcript - ${new Date().toLocaleString()}\n`);
752
- ctx.messages.forEach((m) => {
753
- if (m.role === 'system')
754
- return;
755
- lines.push(`### ${m.role.toUpperCase()}\n${m.content}\n`);
756
- });
757
- const resolvedPath = path.resolve(exportPath);
758
- fs.writeFileSync(resolvedPath, lines.join('\n'), 'utf8');
759
- console.log(pc.green(`[OK] Session transcript exported to ${exportPath}`));
760
- return;
761
- }
762
- if (subcommand === 'search') {
763
- if (!subcommandArg) {
764
- console.log(pc.red('Usage: /session search <query>'));
765
- return;
766
- }
767
- const query = subcommandArg.toLowerCase();
768
- const sessions = ctx.sessionManager.getSessionsForProject();
769
- const matches = sessions.filter((s) => s.title.toLowerCase().includes(query) || s.id.toLowerCase().includes(query));
770
- console.log(pc.bold(`\n--- Search Results for "${query}" ---`));
771
- if (matches.length === 0) {
772
- console.log(pc.gray(' No matching sessions found.'));
773
- }
774
- else {
775
- matches.forEach((s) => {
776
- console.log(` • ${pc.cyan(s.id)} - ${pc.white(s.title)}`);
777
- });
778
- }
779
- console.log(pc.bold('------------------------------------\n'));
780
- return;
781
- }
782
- console.log(pc.yellow('[INFO] Usage: /session <list|load|new|delete|export|branch|checkout|branches|merge> [args]'));
783
- }
784
- },
785
- {
786
- name: '/undo',
787
- description: 'Undo file edits (usage: /undo [count|list])',
788
- usage: '/undo [count|list]',
789
- helpText: 'Undo applied file patches. Specify a number to undo multiple patches (e.g. /undo 3), or "list" to view patch history.',
790
- execute: async (args, ctx) => {
791
- const history = ctx.toolContext.patchHistory;
792
- if (!history || history.length === 0) {
793
- console.log(pc.yellow('[WARN] No patches to undo.'));
794
- return;
795
- }
796
- const cleanArg = args.trim().toLowerCase();
797
- if (cleanArg === 'list' || cleanArg === 'status') {
798
- console.log(pc.bold(`\n--- Applied Patch History (${history.length} patch${history.length > 1 ? 'es' : ''}) ---`));
799
- history.forEach((patch, idx) => {
800
- const num = idx + 1;
801
- const relPath = path.relative(process.cwd(), patch.filePath);
802
- console.log(` [${num}] ${pc.cyan(relPath)} — ${pc.dim(patch.description || 'file edit')}`);
803
- });
804
- console.log(pc.dim('--------------------------------------------------\n'));
805
- return;
806
- }
807
- let undoCount = 1;
808
- if (cleanArg) {
809
- const parsed = parseInt(cleanArg, 10);
810
- if (!isNaN(parsed) && parsed > 0) {
811
- undoCount = Math.min(parsed, history.length);
812
- }
813
- else {
814
- console.log(pc.yellow(`[WARN] Invalid argument: "${args}". Usage: /undo [count|list]`));
815
- return;
816
- }
817
- }
818
- let undoneCount = 0;
819
- for (let i = 0; i < undoCount; i++) {
820
- if (history.length === 0)
821
- break;
822
- const last = history.pop();
823
- try {
824
- if (!last.oldContent) {
825
- if (fs.existsSync(last.filePath)) {
826
- fs.unlinkSync(last.filePath);
827
- console.log(pc.green(`[OK] Undid creation — deleted file ${pc.bold(path.relative(process.cwd(), last.filePath))}`));
828
- undoneCount++;
829
- }
830
- }
831
- else {
832
- const currentContent = fs.existsSync(last.filePath) ? fs.readFileSync(last.filePath, 'utf8') : null;
833
- if (currentContent === last.newContent || currentContent === null) {
834
- fs.writeFileSync(last.filePath, last.oldContent, 'utf8');
835
- console.log(pc.green(`[OK] Undid patch to ${pc.bold(path.relative(process.cwd(), last.filePath))} (${last.description})`));
836
- undoneCount++;
837
- }
838
- else {
839
- console.log(pc.yellow(`[WARN] File ${path.relative(process.cwd(), last.filePath)} has manual edits. Force restoring original patch state...`));
840
- fs.writeFileSync(last.filePath, last.oldContent, 'utf8');
841
- undoneCount++;
842
- }
843
- }
844
- }
845
- catch (err) {
846
- console.log(pc.red(`[WARN] Failed to undo patch on ${last.filePath}: ${err.message}`));
847
- }
848
- }
849
- if (undoneCount > 1) {
850
- console.log(pc.green(`[OK] Successfully undone ${undoneCount} patches.`));
851
- }
852
- }
853
- },
854
- {
855
- name: '/branch',
856
- description: 'Git branch operations',
857
- execute: async (args, ctx) => {
858
- try {
859
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
860
- const arg = args.trim();
861
- if (!arg) {
862
- const currentBranchResult = await termExec({ command: 'git branch --show-current', timeout: 5, workdir: process.cwd() }, ctx.toolContext);
863
- const current = currentBranchResult.content?.trim();
864
- if (current) {
865
- console.log(`\n ${pc.cyan('Current Git branch:')} ${pc.bold(current)}`);
866
- }
867
- else {
868
- console.log(pc.red('\n Not in a Git repository or no branch found.'));
869
- }
870
- }
871
- else {
872
- console.log(`\n Creating and switching to branch ${pc.cyan(arg)}...`);
873
- const checkoutResult = await termExec({ command: `git checkout -b ${arg}`, timeout: 10, workdir: process.cwd() }, ctx.toolContext);
874
- if (checkoutResult.success) {
875
- console.log(pc.green(` [OK] Switched to a new branch '${arg}'`));
876
- }
877
- else {
878
- console.log(pc.yellow(` Branch might already exist, attempting to switch...`));
879
- const switchResult = await termExec({ command: `git checkout ${arg}`, timeout: 10, workdir: process.cwd() }, ctx.toolContext);
880
- if (switchResult.success) {
881
- console.log(pc.green(` [OK] Switched to branch '${arg}'`));
882
- }
883
- else {
884
- console.log(pc.red(` Switch failed: ${switchResult.error || switchResult.content}`));
885
- }
886
- }
887
- }
888
- }
889
- catch (err) {
890
- console.log(pc.red(`[WARN] Branch command error: ${err.message}`));
891
- }
892
- }
893
- },
894
- {
895
- name: '/pr',
896
- description: 'Generate PR description Compared to base branch',
897
- execute: async (args, ctx) => {
898
- const arg = args.trim();
899
- try {
900
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
901
- const gitCheck = await termExec({ command: 'git rev-parse --is-inside-work-tree', timeout: 5, workdir: process.cwd() }, ctx.toolContext);
902
- if (!gitCheck.success) {
903
- console.log(pc.red(' Error: Not inside a Git repository.'));
904
- return;
905
- }
906
- let baseBranch = arg || 'main';
907
- if (!arg) {
908
- const mainCheck = await termExec({ command: 'git show-ref --verify refs/heads/main', timeout: 5, workdir: process.cwd() }, ctx.toolContext);
909
- if (!mainCheck.success) {
910
- const masterCheck = await termExec({ command: 'git show-ref --verify refs/heads/master', timeout: 5, workdir: process.cwd() }, ctx.toolContext);
911
- if (masterCheck.success) {
912
- baseBranch = 'master';
913
- }
914
- }
915
- }
916
- const currentBranchResult = await termExec({ command: 'git branch --show-current', timeout: 5, workdir: process.cwd() }, ctx.toolContext);
917
- const currentBranch = currentBranchResult.content?.trim();
918
- console.log(`\n Comparing ${pc.cyan(currentBranch || 'HEAD')} with base branch ${pc.cyan(baseBranch)}...`);
919
- const commitsResult = await termExec({ command: `git log ${baseBranch}..HEAD --oneline`, timeout: 10, workdir: process.cwd() }, ctx.toolContext);
920
- const commitList = commitsResult.content?.trim() || '';
921
- const diffResult = await termExec({ command: `git diff ${baseBranch}...HEAD`, timeout: 15, workdir: process.cwd() }, ctx.toolContext);
922
- const diffContent = diffResult.content?.slice(0, 15000) || '';
923
- if (!commitList && !diffContent) {
924
- console.log(pc.yellow(` No commits or diff found between ${currentBranch} and ${baseBranch}.`));
925
- return;
926
- }
927
- console.log(pc.dim(' Analyzing changes and generating PR description...'));
928
- const aiResponse = await ctx.router.chat.completions.create({
929
- model: 'auto',
930
- messages: [
931
- {
932
- role: 'system',
933
- content: 'You write clean, comprehensive, professional Pull Request descriptions in Markdown format. Output ONLY the markdown content — no extra chat, wrapper, or quotes.'
934
- },
935
- {
936
- role: 'user',
937
- content: `Generate a Pull Request description for the current branch compared to ${baseBranch}.\n\nCommits:\n${commitList}\n\nDiff:\n${diffContent}`
938
- }
939
- ],
940
- temperature: 0.3,
941
- });
942
- const prDesc = (aiResponse.choices[0]?.message?.content || '').trim();
943
- if (!prDesc) {
944
- console.log(pc.red(' Failed to generate PR description.'));
945
- return;
946
- }
947
- console.log(pc.bold('\n--- Generated PR Description ---'));
948
- console.log(prDesc);
949
- console.log(pc.bold('--------------------------------'));
950
- const outPath = path.join(process.cwd(), 'pr-desc.md');
951
- fs.writeFileSync(outPath, prDesc, 'utf8');
952
- console.log(pc.green(`\n[OK] PR description saved to ${pc.cyan('pr-desc.md')}`));
953
- }
954
- catch (err) {
955
- console.log(pc.red(`[WARN] PR command error: ${err.message}`));
956
- }
957
- }
958
- },
959
- {
960
- name: '/debug',
961
- description: 'Run command and autonomously debug failures',
962
- execute: async (args, ctx) => {
963
- const debugCmd = args.trim();
964
- if (!debugCmd) {
965
- console.log(pc.red(' Error: Please specify a command to run. Example: /debug npm test'));
966
- return;
967
- }
968
- console.log(`\n ${pc.cyan('Starting autonomous debugging loop for:')} ${pc.bold(debugCmd)}`);
969
- const MAX_RETRIES = 5;
970
- let attempt = 1;
971
- let success = false;
972
- while (attempt <= MAX_RETRIES) {
973
- console.log(`\n ${pc.yellow(`[Attempt ${attempt}/${MAX_RETRIES}]`)} Running: ${pc.bold(debugCmd)}...`);
974
- try {
975
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
976
- const execResult = await termExec({ command: debugCmd, timeout: 60, workdir: process.cwd() }, ctx.toolContext);
977
- if (execResult.success) {
978
- console.log(pc.green(`\n ${pc.green('✔')} ${pc.bold(`Success on attempt ${attempt}!`)} Command passed with exit code 0.`));
979
- success = true;
980
- break;
981
- }
982
- console.log(pc.red(`\n ${pc.red('✗')} ${pc.bold(`Command failed on attempt ${attempt}.`)}`));
983
- const stdout = execResult.content || '';
984
- const errorMsg = execResult.error || '';
985
- const logs = `${stdout}\n${errorMsg}`.trim();
986
- console.log(pc.bold('\n--- Failure Logs ---'));
987
- const logLines = logs.split('\n');
988
- const preview = logLines.length > 20 ? logLines.slice(-20).join('\n') : logs;
989
- console.log(preview);
990
- if (logLines.length > 20) {
991
- console.log(pc.dim(`\n (... truncated ${logLines.length - 20} lines of logs ...)`));
992
- }
993
- console.log(pc.bold('--------------------'));
994
- if (attempt === MAX_RETRIES) {
995
- console.log(pc.red(`\n Reached maximum attempt limit of ${MAX_RETRIES}. Debugging loop failed.`));
996
- break;
997
- }
998
- console.log(pc.dim('\n Calling Daedalus to analyze failure and apply a fix...'));
999
- const debugPrompt = `The command "${debugCmd}" failed on attempt ${attempt}.
1000
- Here are the execution logs (showing the failure details):
1001
-
1002
- ${logs.slice(-6000)}
1003
-
1004
- Please analyze the error, identify which files need correction, and apply surgical edits using 'patch' or write tools to fix the issue.
1005
- Once you have finished making changes, I will automatically re-run the command to verify if it passes.`;
1006
- await ctx.callModelWithTools(debugPrompt);
1007
- }
1008
- catch (err) {
1009
- console.log(pc.red(`\n Error in debugging loop: ${err.message}`));
1010
- break;
1011
- }
1012
- attempt++;
1013
- }
1014
- if (!success) {
1015
- console.log(pc.red(`\n Autonomous debugging did not succeed after ${MAX_RETRIES} attempts.`));
1016
- }
1017
- turnSeparator();
1018
- }
1019
- },
1020
- {
1021
- name: '/ensemble',
1022
- description: 'Ensemble model drafting pipeline',
1023
- execute: async (args, ctx) => {
1024
- const ensembleGoal = args.trim();
1025
- if (!ensembleGoal) {
1026
- console.log(pc.red(' Error: Please specify a goal for the ensemble draft. Example: /ensemble Implement feature X'));
1027
- return;
1028
- }
1029
- try {
1030
- const { runEnsembleWorkflow } = await import('./agents/ensemble.js');
1031
- await runEnsembleWorkflow(ensembleGoal, ctx.toolContext, ctx.config, ctx.router);
1032
- }
1033
- catch (err) {
1034
- console.log(pc.red(`\n Error in ensemble drafting: ${err.message}`));
1035
- }
1036
- turnSeparator();
1037
- }
1038
- },
1039
- {
1040
- name: '/commit',
1041
- description: 'Stage and commit changes',
1042
- execute: async (args, ctx) => {
1043
- const forcedMsg = args.trim();
1044
- try {
1045
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
1046
- const statusResult = await termExec({ command: 'git status --short', timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1047
- console.log(pc.bold('\n--- Git Status ---'));
1048
- console.log(statusResult.content || pc.gray('(clean)'));
1049
- if (!statusResult.content?.trim()) {
1050
- console.log(pc.yellow('Nothing to commit.'));
1051
- return;
1052
- }
1053
- const addResult = await termExec({ command: 'git add -A', timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1054
- if (!addResult.success) {
1055
- console.log(pc.red(`Stage failed: ${addResult.error}`));
1056
- return;
1057
- }
1058
- let commitMsg = forcedMsg;
1059
- if (!commitMsg) {
1060
- const diffResult = await termExec({ command: 'git diff --cached --stat', timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1061
- const diffFull = await termExec({ command: 'git diff --cached', timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1062
- const diffContent = diffFull.content?.slice(0, 6000) || '';
1063
- if (diffResult.content)
1064
- console.log(pc.gray(diffResult.content));
1065
- if (diffContent) {
1066
- console.log(pc.dim(' Generating commit message...'));
1067
- try {
1068
- const aiResponse = await ctx.router.chat.completions.create({
1069
- model: 'auto',
1070
- messages: [
1071
- { 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.' },
1072
- { role: 'user', content: `Write a commit message for this diff:\n\n${diffContent}` }
1073
- ],
1074
- temperature: 0.2,
1075
- max_tokens: 80,
1076
- });
1077
- const suggested = ((aiResponse.choices[0]?.message?.content) || '').trim().split('\n')[0].trim();
1078
- if (suggested) {
1079
- console.log(`\n ${pc.dim('Suggested:')} ${pc.cyan(suggested)}`);
1080
- const choice = await ctx.askLine(pc.dim(' [Enter] accept [e] edit [n] cancel: '));
1081
- if (choice.trim().toLowerCase() === 'n') {
1082
- console.log(pc.yellow('Commit cancelled.'));
1083
- await termExec({ command: 'git restore --staged .', timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1084
- return;
1085
- }
1086
- else if (choice.trim().toLowerCase() === 'e') {
1087
- commitMsg = await ctx.askLine(pc.cyan(' Commit message: '));
1088
- }
1089
- else {
1090
- commitMsg = suggested;
1091
- }
1092
- }
1093
- }
1094
- catch {
1095
- // Model unavailable — manual fallback
1096
- }
1097
- }
1098
- if (!commitMsg) {
1099
- commitMsg = await ctx.askLine(pc.cyan(' Commit message: '));
1100
- }
1101
- if (!commitMsg.trim()) {
1102
- console.log(pc.yellow('Commit cancelled — empty message.'));
1103
- await termExec({ command: 'git restore --staged .', timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1104
- return;
1105
- }
1106
- }
1107
- const commitResult = await termExec({ command: `git commit -m ${JSON.stringify(commitMsg)}`, timeout: 10, workdir: process.cwd() }, ctx.toolContext);
1108
- if (commitResult.success) {
1109
- console.log(pc.green(`\n[OK] Commit: ${commitMsg.slice(0, 60)}`));
1110
- }
1111
- else {
1112
- console.log(pc.red(`Commit failed: ${commitResult.error}`));
1113
- }
1114
- }
1115
- catch (err) {
1116
- console.log(pc.red(`[WARN] Commit error: ${err.message}`));
1117
- }
1118
- }
1119
- },
1120
- {
1121
- name: '/project',
1122
- description: 'View or set project config settings (.daedalusrc)',
1123
- usage: '/project [set <key> <value> | get <key> | reset]',
1124
- helpText: 'Manage project-specific configuration overrides stored in .daedalusrc.\n\nSubcommands:\n (no args) Show all active project configuration overrides\n set <key> <value> Set a project config override\n get <key> Print the value of a specific project config key\n reset Reset and delete project settings file\n\nCommon Overridable Keys:\n modelOverride Override primary model selection (e.g. "openai/gpt-4.1")\n tools.sandbox Isolate execution for this project ("none" | "docker")\n context.maxTokens Adjust context limit for this workspace (e.g. 64000)',
1125
- execute: async (args, _ctx) => {
1126
- const rest = args.trim();
1127
- const { loadProjectConfig, saveProjectConfig, hasLocalConfig } = await import('./tools/builtin/project-config.js');
1128
- if (!rest) {
1129
- const cfg = loadProjectConfig(process.cwd());
1130
- const isLocal = hasLocalConfig(process.cwd());
1131
- console.log(pc.bold(`\n--- Project Config (${isLocal ? '.daedalusrc' : 'global'}) ---`));
1132
- console.log(JSON.stringify(cfg, null, 2));
1133
- console.log(pc.bold('----------------------------------'));
1134
- console.log(pc.gray('Use /project set <key> = <value> to update'));
1135
- console.log(pc.gray('Use /project init to create a .daedalusrc in this project'));
1136
- return;
1137
- }
1138
- if (rest === 'init') {
1139
- const localPath = path.join(process.cwd(), '.daedalusrc');
1140
- if (fs.existsSync(localPath)) {
1141
- console.log(pc.yellow('.daedalusrc already exists in this project'));
1142
- return;
1143
- }
1144
- const cfg = loadProjectConfig(process.cwd());
1145
- saveProjectConfig(cfg, true);
1146
- console.log(pc.green('Created .daedalusrc — project config is now local to this repo'));
1147
- return;
1148
- }
1149
- if (rest.startsWith('set ')) {
1150
- const setArgs = rest.substring(4).trim();
1151
- const eqIdx = setArgs.indexOf('=');
1152
- let key, value;
1153
- if (eqIdx >= 0) {
1154
- key = setArgs.slice(0, eqIdx).trim();
1155
- value = setArgs.slice(eqIdx + 1).trim();
1156
- }
1157
- else {
1158
- const parts = setArgs.split(/\s+/);
1159
- key = parts[0];
1160
- value = parts.slice(1).join(' ');
1161
- }
1162
- if (!key || !value) {
1163
- console.log(pc.red('Usage: /project set <key> = <value>'));
1164
- }
1165
- else {
1166
- const cfg = loadProjectConfig(process.cwd());
1167
- let parsedVal = value;
1168
- if (value.toLowerCase() === 'true')
1169
- parsedVal = true;
1170
- else if (value.toLowerCase() === 'false')
1171
- parsedVal = false;
1172
- else if (!isNaN(Number(value)))
1173
- parsedVal = Number(value);
1174
- cfg[key] = parsedVal;
1175
- const isLocal = hasLocalConfig(process.cwd());
1176
- saveProjectConfig(cfg, isLocal);
1177
- console.log(pc.green(`Set ${key} = ${value} (${isLocal ? '.daedalusrc' : 'global'})`));
1178
- }
1179
- }
1180
- else {
1181
- console.log(pc.red(`Unknown subcommand: ${rest}. Try: /project, /project set <key> = <value>, /project init`));
1182
- }
1183
- }
1184
- },
1185
- {
1186
- name: '/session',
1187
- description: 'Manage chat sessions — /session new to start, /session load <id> to restore, /session export [path] to save transcript',
1188
- usage: '/session <subcommand> [args]',
1189
- helpText: 'Manage, list, load, save, and export SQLite-persisted conversation sessions.\n\nSubcommands:\n list List all saved sessions\n load <id> Load a saved session by ID\n save Save the current session manually\n new Start a new conversation session\n export [path] Export the current session transcript to Markdown',
1190
- execute: async (args, ctx) => {
1191
- const parts = args.trim().split(/\s+/);
1192
- const subcommand = parts[0].toLowerCase();
1193
- const subcommandArg = parts.slice(1).join(' ').trim();
1194
- if (!subcommand || subcommand === 'list') {
1195
- const sessions = ctx.sessionManager.getSessionsForProject();
1196
- console.log(pc.bold('\n--- Past Sessions ---'));
1197
- if (sessions.length === 0) {
1198
- console.log(pc.gray(' No past sessions found.'));
1199
- }
1200
- else {
1201
- sessions.forEach(s => {
1202
- const currentTag = s.id === ctx.sessionManager.sessionId ? pc.green(' (current)') : '';
1203
- const dateStr = new Date(s.updated_at).toLocaleString();
1204
- console.log(` • ${pc.cyan(s.id)}${currentTag}`);
1205
- console.log(` Title: ${pc.white(s.title)}`);
1206
- console.log(` Updated: ${pc.dim(dateStr)}`);
1207
- });
1208
- }
1209
- console.log(pc.bold('---------------------\n'));
1210
- console.log(pc.gray('Use `/session load <id>` to resume a past session.'));
1211
- console.log(pc.gray('Use `/session search <query>` to search sessions.'));
1212
- console.log(pc.gray('Use `/session new [title]` to start a new session.'));
1213
- console.log(pc.gray('Use `/session rename <title>` to rename the current session.'));
1214
- console.log(pc.gray('Use `/session delete <id>` to delete a session.'));
1215
- console.log(pc.gray('Use `/session export [path]` to export the current session to Markdown.'));
1216
- return;
1217
- }
1218
- if (subcommand === 'search') {
1219
- if (!subcommandArg) {
1220
- console.log(pc.red('Usage: /session search <query>'));
1221
- return;
1222
- }
1223
- const query = subcommandArg.toLowerCase();
1224
- const sessions = ctx.sessionManager.getSessionsForProject();
1225
- const matches = sessions.filter(s => s.title.toLowerCase().includes(query) ||
1226
- s.id.toLowerCase().includes(query));
1227
- if (matches.length === 0) {
1228
- console.log(pc.yellow(`No sessions matching "${subcommandArg}"`));
1229
- }
1230
- else {
1231
- console.log(pc.bold(`\n--- Matching Sessions (${matches.length}) ---`));
1232
- matches.forEach(s => {
1233
- const currentTag = s.id === ctx.sessionManager.sessionId ? pc.green(' (current)') : '';
1234
- const dateStr = new Date(s.updated_at).toLocaleString();
1235
- console.log(` • ${pc.cyan(s.id)}${currentTag}`);
1236
- console.log(` Title: ${pc.white(s.title)}`);
1237
- console.log(` Updated: ${pc.dim(dateStr)}`);
1238
- });
1239
- console.log(pc.bold('----------------------------------\n'));
1240
- }
1241
- return;
1242
- }
1243
- if (subcommand === 'load') {
1244
- if (!subcommandArg) {
1245
- console.log(pc.red('Usage: /session load <session-id>'));
1246
- return;
1247
- }
1248
- const sessions = ctx.sessionManager.getSessionsForProject();
1249
- const found = sessions.find(s => s.id === subcommandArg || s.id.startsWith(subcommandArg));
1250
- if (!found) {
1251
- console.log(pc.red(`Session "${subcommandArg}" not found.`));
1252
- return;
1253
- }
1254
- const currentTodos = getSessionTodos(ctx.toolContext.sessionId);
1255
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, currentTodos);
1256
- if (found.project_path && found.project_path !== ctx.sessionManager.projectRoot) {
1257
- ctx.sessionManager.setProjectRoot(found.project_path);
1258
- ctx.sessionManager.reopenIndexDb();
1259
- ctx.projectHash = ctx.sessionManager.projectHash;
1260
- ctx.toolContext.projectRoot = ctx.sessionManager.projectRoot;
1261
- ctx.toolContext.projectHash = ctx.sessionManager.projectHash;
1262
- }
1263
- const loaded = ctx.sessionManager.startSession(found.id, found.title);
1264
- ctx.initializeSessionState(loaded);
1265
- console.log(pc.green(`Loaded session: ${pc.bold(found.id)} ("${found.title}") [${ctx.sessionManager.projectRoot}]`));
1266
- return;
1267
- }
1268
- if (subcommand === 'new') {
1269
- const currentTodos = getSessionTodos(ctx.toolContext.sessionId);
1270
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, currentTodos);
1271
- let title;
1272
- let projectRoot;
1273
- if (path.isAbsolute(subcommandArg)) {
1274
- projectRoot = subcommandArg;
1275
- title = `Session on ${path.basename(subcommandArg.replace(/[\\/]$/, ''))} — ${new Date().toLocaleDateString()}`;
1276
- }
1277
- else {
1278
- title = subcommandArg || `Session on ${new Date().toLocaleDateString()}`;
1279
- }
1280
- if (projectRoot && projectRoot !== ctx.sessionManager.projectRoot) {
1281
- ctx.sessionManager.setProjectRoot(projectRoot);
1282
- ctx.sessionManager.reopenIndexDb();
1283
- ctx.projectHash = ctx.sessionManager.projectHash;
1284
- ctx.toolContext.projectRoot = ctx.sessionManager.projectRoot;
1285
- ctx.toolContext.projectHash = ctx.sessionManager.projectHash;
1286
- }
1287
- const loaded = ctx.sessionManager.startSession(undefined, title);
1288
- ctx.initializeSessionState(loaded);
1289
- console.log(pc.green(`Started new session: ${pc.bold(loaded.sessionId)} [${ctx.sessionManager.projectRoot}]`));
1290
- return;
1291
- }
1292
- if (subcommand === 'rename') {
1293
- if (!subcommandArg) {
1294
- console.log(pc.red('Usage: /session rename <new-title>'));
1295
- return;
1296
- }
1297
- ctx.sessionManager.updateSessionTitle(subcommandArg);
1298
- console.log(pc.green(`Session renamed to: "${subcommandArg}"`));
1299
- return;
1300
- }
1301
- if (subcommand === 'delete') {
1302
- if (!subcommandArg) {
1303
- console.log(pc.red('Usage: /session delete <session-id>'));
1304
- return;
1305
- }
1306
- if (subcommandArg === ctx.sessionManager.sessionId) {
1307
- console.log(pc.red('Cannot delete the current active session.'));
1308
- return;
1309
- }
1310
- const sessions = ctx.sessionManager.getSessionsForProject();
1311
- const found = sessions.find(s => s.id === subcommandArg || s.id.startsWith(subcommandArg));
1312
- if (!found) {
1313
- console.log(pc.red(`Session "${subcommandArg}" not found.`));
1314
- return;
1315
- }
1316
- ctx.sessionManager.deleteSession(found.id);
1317
- console.log(pc.green(`Deleted session: ${pc.bold(found.id)}`));
1318
- return;
1319
- }
1320
- if (subcommand === 'export') {
1321
- const cleanedPath = (subcommandArg || `transcript-${ctx.sessionManager.sessionId}.md`).replace(/^["']|["']$/g, '');
1322
- const resolved = path.resolve(ctx.sessionManager.projectRoot || '.', cleanedPath);
1323
- let md = `# Daedalus Session: ${ctx.sessionManager.sessionTitle}\n\n`;
1324
- md += `*Generated: ${new Date().toLocaleString()}*\n\n---\n\n`;
1325
- for (const msg of ctx.messages) {
1326
- if (msg.role === 'system')
1327
- continue;
1328
- if (msg.role === 'user') {
1329
- md += `### 👤 User\n\n`;
1330
- md += `${msg.content}\n\n---\n\n`;
1331
- }
1332
- else if (msg.role === 'assistant') {
1333
- md += `### 🤖 Daedalus\n\n`;
1334
- if (msg.content) {
1335
- md += `${msg.content}\n\n`;
1336
- }
1337
- if (msg.tool_calls && msg.tool_calls.length > 0) {
1338
- md += `#### 🛠️ Tool Execution\n\n`;
1339
- for (const tc of msg.tool_calls) {
1340
- md += `* **${tc.function.name}**\n`;
1341
- try {
1342
- const prettyArgs = JSON.stringify(JSON.parse(tc.function.arguments), null, 2);
1343
- md += ` \`\`\`json\n${prettyArgs}\n \`\`\`\n`;
1344
- }
1345
- catch {
1346
- md += ` *Arguments*: \`${tc.function.arguments}\`\n`;
1347
- }
1348
- }
1349
- md += `\n`;
1350
- }
1351
- md += `---\n\n`;
1352
- }
1353
- else if (msg.role === 'tool') {
1354
- md += `#### 📥 Tool Response (${msg.name || 'unknown'})\n\n`;
1355
- const trimmedContent = msg.content && msg.content.length > 2000
1356
- ? msg.content.slice(0, 2000) + '\n\n... (output truncated for readability)'
1357
- : msg.content;
1358
- md += `\`\`\`text\n${trimmedContent || '(no output)'}\n\`\`\`\n\n---\n\n`;
1359
- }
1360
- }
1361
- fs.writeFileSync(resolved, md, 'utf8');
1362
- console.log(pc.green(`Session transcript exported to: ${pc.bold(resolved)}`));
1363
- return;
1364
- }
1365
- console.log(pc.red(`Unknown subcommand: ${subcommand}. Try: list, search, load, new, rename, delete, export`));
1366
- }
1367
- },
1368
- {
1369
- name: '/test',
1370
- aliases: ['test'],
1371
- description: 'Run test loop and fix failures (supports --git-aware / -g for smart test selection)',
1372
- usage: '/test [--git-aware | -g] [maxLoops]',
1373
- helpText: 'Runs your test suite and automatically invokes Daedalus tools to fix failing assertions. Use --git-aware or -g to focus only on tests affected by recent git changes.',
1374
- execute: async (args, ctx) => {
1375
- const isGitAware = args.includes('--git-aware') || args.includes('-g');
1376
- const cleanArgs = args.replace('--git-aware', '').replace('-g', '').trim();
1377
- const maxLoops = cleanArgs ? parseInt(cleanArgs, 10) || 3 : 3;
1378
- const { loadProjectConfig } = await import('./tools/builtin/project-config.js');
1379
- const { execute: termExec } = await import('./tools/builtin/terminal.js');
1380
- const { getGitAwareTestCommand } = await import('./utils/gitAwareTest.js');
1381
- const cfg = loadProjectConfig(process.cwd());
1382
- let testCmd = cfg.testCommand || 'npm test';
1383
- if (isGitAware) {
1384
- const gitAware = getGitAwareTestCommand(process.cwd(), testCmd);
1385
- if (gitAware.testFiles.length > 0) {
1386
- console.log(pc.cyan(`\n⚡ Git-Aware Mode: Detected ${gitAware.modifiedFiles.length} modified files → running ${gitAware.testFiles.length} target test suites:`));
1387
- console.log(pc.gray(gitAware.testFiles.map(f => ` • ${f}`).join('\n')));
1388
- testCmd = gitAware.command;
1389
- }
1390
- else {
1391
- console.log(pc.yellow(`\n⚡ Git-Aware Mode: No specific matching test files found for modified files. Running full test suite.`));
1392
- }
1393
- }
1394
- console.log(pc.bold(`\nTest-Run-Fix Loop (max ${maxLoops} iterations)`));
1395
- console.log(pc.gray(`Test command: ${testCmd}\n`));
1396
- for (let i = 0; i < maxLoops; i++) {
1397
- console.log(pc.cyan(`\n--- Run ${i + 1}/${maxLoops} ---`));
1398
- const result = await termExec({ command: testCmd, timeout: 120, workdir: process.cwd() }, ctx.toolContext);
1399
- console.log(result.content?.slice(0, 2000) || pc.gray('(no output)'));
1400
- if (result.success) {
1401
- console.log(pc.green('\n[OK] All tests passed!'));
1402
- break;
1403
- }
1404
- if (i === maxLoops - 1) {
1405
- console.log(pc.yellow(`\n[WARN] Max loops (${maxLoops}) reached. Tests still failing.`));
1406
- break;
1407
- }
1408
- 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.`;
1409
- await ctx.callModelWithTools(`User Prompt: ${failureCtx}`);
1410
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, getSessionTodos(ctx.toolContext.sessionId));
1411
- }
1412
- }
1413
- },
1414
- {
1415
- name: '/watch',
1416
- aliases: ['watch'],
1417
- description: 'Start or stop background codebase file-watcher for automatic FTS5 symbol re-indexing',
1418
- usage: '/watch [start | stop | status]',
1419
- helpText: 'Watches project files for changes and automatically updates the codebase symbol index in real time as you save files.',
1420
- execute: async (args) => {
1421
- const { initIndexDb } = await import('./indexing/fts.js');
1422
- const { watchCodebase } = await import('./indexing/watcher.js');
1423
- const path = await import('path');
1424
- const action = args.trim().toLowerCase() || 'start';
1425
- if (action === 'stop') {
1426
- if (globalThis.__daedalusWatcher) {
1427
- globalThis.__daedalusWatcher.close();
1428
- delete globalThis.__daedalusWatcher;
1429
- console.log(pc.green('\n[OK] Codebase file watcher stopped.'));
1430
- }
1431
- else {
1432
- console.log(pc.yellow('\n[INFO] File watcher is not currently running.'));
1433
- }
1434
- return;
1435
- }
1436
- if (action === 'status') {
1437
- const isRunning = !!globalThis.__daedalusWatcher;
1438
- console.log(pc.cyan(`\n⚡ File Watcher Status: ${isRunning ? pc.bold(pc.green('ACTIVE')) : pc.dim('INACTIVE')}`));
1439
- return;
1440
- }
1441
- if (globalThis.__daedalusWatcher) {
1442
- console.log(pc.yellow('\n[INFO] File watcher is already running in background.'));
1443
- return;
1444
- }
1445
- try {
1446
- const cwd = process.cwd();
1447
- const dbPath = path.join(cwd, '.daedalus', 'index.db');
1448
- const db = initIndexDb(dbPath);
1449
- const projectHash = 'local';
1450
- const instance = watchCodebase(db, cwd, projectHash);
1451
- globalThis.__daedalusWatcher = instance;
1452
- console.log(pc.green('\n[OK] Started background codebase watcher! Symbol index will auto-update on file save.'));
1453
- }
1454
- catch (err) {
1455
- console.log(pc.red(`\n[ERROR] Failed to start file watcher: ${err.message}`));
1456
- }
1457
- }
1458
- },
1459
- {
1460
- name: '/index',
1461
- description: 'Index codebase for symbol search',
1462
- execute: async (args, ctx) => {
1463
- const parts = args.trim().split(/\s+/).filter(Boolean);
1464
- const opts = {};
1465
- for (const arg of parts) {
1466
- if (arg.startsWith('--exclude=')) {
1467
- opts.exclude = arg.split('=')[1].split(',');
1468
- }
1469
- else if (arg.startsWith('--ext=')) {
1470
- opts.extensions = arg.split('=')[1].split(',');
1471
- }
1472
- }
1473
- console.log(pc.bold('\n--- Indexing Codebase ---'));
1474
- console.log(pc.gray(`Project: ${process.cwd()}`));
1475
- const indexDbPath = ctx.getIndexDbPath();
1476
- if (!fs.existsSync(path.dirname(indexDbPath))) {
1477
- fs.mkdirSync(path.dirname(indexDbPath), { recursive: true });
1478
- }
1479
- const { initIndexDb } = await import('./indexing/fts.js');
1480
- const { indexCodebase } = await import('./indexing/indexer.js');
1481
- const db = initIndexDb(indexDbPath);
1482
- console.log(pc.gray('\nScanning files...'));
1483
- const start = Date.now();
1484
- try {
1485
- const barWidth = 20;
1486
- let lastPct = -1;
1487
- const onProgress = ({ current, total, file }) => {
1488
- const pct = Math.round((current / total) * 100);
1489
- if (pct === lastPct)
1490
- return;
1491
- lastPct = pct;
1492
- const filled = Math.round((current / total) * barWidth);
1493
- const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
1494
- process.stdout.write(`\r ${pc.cyan(bar)} ${pc.white(`${current}/${total}`)} ${pc.gray(file.slice(-40))}`);
1495
- };
1496
- const result = await indexCodebase(db, process.cwd(), ctx.projectHash, { ...opts, onProgress });
1497
- process.stdout.write('\n');
1498
- const elapsed = Date.now() - start;
1499
- ctx.toolContext.indexDb = db;
1500
- console.log(pc.green(`\n✔ Indexing complete in ${elapsed}ms`));
1501
- console.log(pc.white(` Total files: ${result.totalFiles}`));
1502
- console.log(pc.white(` Indexed files: ${result.indexedFiles}`));
1503
- console.log(pc.white(` Skipped (unchanged): ${result.skippedFiles}`));
1504
- if (result.errors.length > 0) {
1505
- console.log(pc.yellow(`\nErrors (${result.errors.length}):`));
1506
- result.errors.slice(0, 10).forEach(e => console.log(pc.red(` - ${e}`)));
1507
- if (result.errors.length > 10) {
1508
- console.log(pc.gray(` ... and ${result.errors.length - 10} more`));
1509
- }
1510
- }
1511
- }
1512
- catch (err) {
1513
- console.error(pc.red(`\n[ERROR] Indexing failed: ${err.message}`));
1514
- }
1515
- }
1516
- },
1517
- {
1518
- name: '/find',
1519
- description: 'Search indexed symbols',
1520
- execute: async (args, ctx) => {
1521
- const parts = args.trim().split(/\s+/).filter(Boolean);
1522
- if (parts.length === 0) {
1523
- console.log(pc.red('[WARN] Usage: /find <query> [limit]'));
1524
- return;
1525
- }
1526
- const query = parts[0];
1527
- const limit = parts[1] ? parseInt(parts[1], 10) : 30;
1528
- if (isNaN(limit)) {
1529
- console.log(pc.red('[WARN] Invalid limit'));
1530
- return;
1531
- }
1532
- const indexDbPath = ctx.getIndexDbPath();
1533
- if (!fs.existsSync(indexDbPath)) {
1534
- console.log(pc.yellow('[WARN] No index found. Run /index first.'));
1535
- return;
1536
- }
1537
- const { initIndexDb, searchSymbols } = await import('./indexing/fts.js');
1538
- const db = initIndexDb(indexDbPath);
1539
- console.log(pc.bold(`\n--- Symbol Search: "${query}" ---`));
1540
- const symbols = searchSymbols(db, query, ctx.projectHash, limit);
1541
- if (symbols.length === 0) {
1542
- console.log(pc.gray(' No symbols found.'));
1543
- return;
1544
- }
1545
- console.log(pc.white(`\nFound ${symbols.length} symbol(s):`));
1546
- for (const s of symbols) {
1547
- const kindColor = s.kind === 'function' ? pc.cyan : s.kind === 'class' ? pc.green : s.kind === 'interface' ? pc.blue : pc.white;
1548
- const loc = `${s.file_path}:${s.line_start}${s.line_end !== s.line_start ? '-' + s.line_end : ''}`;
1549
- console.log(` ${kindColor(`[${s.kind}]`)} ${pc.bold(s.name)} ${pc.dim(`(${loc})`)}`);
1550
- if (s.signature) {
1551
- console.log(pc.dim(` ${s.signature.slice(0, 100)}${s.signature.length > 100 ? '...' : ''}`));
1552
- }
1553
- }
1554
- }
1555
- },
1556
- {
1557
- name: '/refs',
1558
- description: 'Find symbol references (callers)',
1559
- execute: async (args, ctx) => {
1560
- const symbol = args.trim();
1561
- if (!symbol) {
1562
- console.log(pc.red('[WARN] Usage: /refs <symbol>'));
1563
- return;
1564
- }
1565
- const indexDbPath = ctx.getIndexDbPath();
1566
- if (!fs.existsSync(indexDbPath)) {
1567
- console.log(pc.yellow('[WARN] No index found. Run /index first.'));
1568
- return;
1569
- }
1570
- const { initIndexDb, findReferences } = await import('./indexing/fts.js');
1571
- const db = initIndexDb(indexDbPath);
1572
- console.log(pc.bold(`\n--- References to: ${symbol} ---`));
1573
- const refs = findReferences(db, symbol, ctx.projectHash);
1574
- if (refs.length === 0) {
1575
- console.log(pc.gray(' No references found.'));
1576
- return;
1577
- }
1578
- const byCaller = new Map();
1579
- for (const r of refs) {
1580
- const key = `${r.caller_name} (${r.caller_file}:${r.caller_line})`;
1581
- if (!byCaller.has(key))
1582
- byCaller.set(key, []);
1583
- byCaller.get(key).push(r);
1584
- }
1585
- console.log(pc.white(`\nFound ${refs.length} reference(s) from ${byCaller.size} caller(s):`));
1586
- for (const [caller, refs] of byCaller) {
1587
- console.log(pc.cyan(`\n ${caller}:`));
1588
- for (const r of refs.slice(0, 5)) {
1589
- console.log(pc.dim(` ${r.callee_name} at ${r.callee_file}:${r.callee_line}`));
1590
- }
1591
- if (refs.length > 5) {
1592
- console.log(pc.dim(` ... and ${refs.length - 5} more`));
1593
- }
1594
- }
1595
- }
1596
- },
1597
- {
1598
- name: '/def',
1599
- description: 'Get symbol definition',
1600
- execute: async (args, ctx) => {
1601
- const symbol = args.trim();
1602
- if (!symbol) {
1603
- console.log(pc.red('[WARN] Usage: /def <symbol>'));
1604
- return;
1605
- }
1606
- const indexDbPath = ctx.getIndexDbPath();
1607
- if (!fs.existsSync(indexDbPath)) {
1608
- console.log(pc.yellow('[WARN] No index found. Run /index first.'));
1609
- return;
1610
- }
1611
- const { initIndexDb, findDefinitions } = await import('./indexing/fts.js');
1612
- const db = initIndexDb(indexDbPath);
1613
- console.log(pc.bold(`\n--- Definition: ${symbol} ---`));
1614
- const defs = findDefinitions(db, symbol, ctx.projectHash);
1615
- if (defs.length === 0) {
1616
- console.log(pc.gray(' No definitions found.'));
1617
- return;
1618
- }
1619
- console.log(pc.white(`\nFound ${defs.length} definition(s):`));
1620
- for (const d of defs) {
1621
- const kindColor = d.kind === 'function' ? pc.cyan : d.kind === 'class' ? pc.green : d.kind === 'interface' ? pc.blue : pc.white;
1622
- const loc = `${d.file_path}:${d.line_start}${d.line_end !== d.line_start ? '-' + d.line_end : ''}`;
1623
- console.log(` ${kindColor(`[${d.kind}]`)} ${pc.bold(d.name)} ${pc.dim(`(${loc})`)}`);
1624
- if (d.signature) {
1625
- console.log(pc.dim(` ${d.signature.slice(0, 120)}${d.signature.length > 120 ? '...' : ''}`));
1626
- }
1627
- }
1628
- }
1629
- },
1630
- {
1631
- name: '/changelog',
1632
- description: 'View the latest CLI changes',
1633
- execute: async (_args, _ctx) => {
1634
- const { fileURLToPath } = await import('url');
1635
- const __filename = fileURLToPath(import.meta.url);
1636
- const __dirname = path.dirname(__filename);
1637
- const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
1638
- if (!fs.existsSync(changelogPath)) {
1639
- console.log(pc.yellow('[WARN] CHANGELOG.md not found.'));
1640
- return;
1641
- }
1642
- const content = fs.readFileSync(changelogPath, 'utf8');
1643
- const lines = content.split('\n');
1644
- console.log(pc.bold('\n--- Latest CLI Changes ---'));
1645
- let versionCount = 0;
1646
- const maxVersions = 3;
1647
- const displayLines = [];
1648
- for (const line of lines) {
1649
- const isHeader = line.startsWith('# ') || line.startsWith('## ');
1650
- if (isHeader) {
1651
- versionCount++;
1652
- if (versionCount > maxVersions) {
1653
- break;
1654
- }
1655
- }
1656
- if (versionCount > 0) {
1657
- displayLines.push(line);
1658
- }
1659
- }
1660
- console.log(displayLines.join('\n').trim());
1661
- console.log(pc.bold('---------------------------\n'));
1662
- }
1663
- },
1664
- {
1665
- name: '/models',
1666
- description: 'List available and healthy models',
1667
- execute: async (args, ctx) => {
1668
- console.log(pc.bold('\n--- Available Models ---'));
1669
- const models = await ctx.router.listModels();
1670
- if (models.length === 0) {
1671
- console.log(pc.yellow(' No models found. Check your local servers (LM Studio, Ollama, etc.)'));
1672
- }
1673
- else {
1674
- for (const model of models) {
1675
- console.log(` • ${pc.cyan(model)}`);
1676
- }
1677
- }
1678
- const { checkModelHealth } = await import('./router/health.js');
1679
- const healthyModels = ctx.router.getHealthyModels();
1680
- console.log(pc.bold('\n--- Healthy Models ---'));
1681
- for (const model of healthyModels) {
1682
- const health = await checkModelHealth(model, 5000);
1683
- const status = health?.healthy ? pc.green('●') : pc.red('●');
1684
- console.log(` ${status} ${pc.cyan(model.name)} (${model.endpoint}) - ${model.model}`);
1685
- }
1686
- console.log(pc.bold('----------------------\n'));
1687
- }
1688
- },
1689
- {
1690
- name: '/config',
1691
- description: 'Show or modify global configuration',
1692
- usage: '/config [set <key> = <value> | get <key> | reset]',
1693
- helpText: 'Manage global settings. Setting a key applies it in real-time.\n\nSubcommands:\n (no args) Print the entire active configuration JSON\n set <key> = <value> Update a configuration value (e.g. /config set router.strategy = round-robin)\n get <key> Print the value of a specific config key\n reset Reset config to default settings\n\nConfiguration Keys Reference:\n [Router Settings]\n router.strategy Model routing strategy ("priority" | "round-robin" | "fastest")\n router.healthCheckInterval Interval in ms between background health checks (default: 30000)\n router.requestTimeout Timeout in ms for model API requests (default: 120000)\n router.defaultRateLimit Default RPM and TPM rate limit limits\n router.chain Array of configured model endpoints in the routing chain\n\n [Agent Settings]\n agents.default Default agent role to spawn (default: "coder")\n agents.available Array of available agent roles inside the session\n agents.autoOrchestrate Auto-orchestrate complex prompts (default: true)\n agents.ensemble.enabled Enable multi-model candidate drafting (default: false)\n agents.ensemble.maxLoops Max correction loops for ensemble (default: 2)\n agents.ensemble.candidatesCount Candidates drafted per loop (default: 2)\n\n [Tool Settings]\n tools.builtin List of enabled built-in CLI tools\n tools.mcpServers Configured Model Context Protocol (MCP) servers\n tools.shell Preferred shell executable path (e.g. "powershell")\n tools.sandbox Sandbox mode for commands ("none" | "docker" | "wsl")\n tools.sandboxImage Docker image to run commands in (default: "node:20")\n tools.wslDistribution Linux distribution name for WSL sandboxing\n\n [Context Settings]\n context.maxTokens Max prompt tokens (default: 128000)\n context.summarizeAt Context ratio threshold to trigger history summary (default: 0.8)\n context.includeGitDiff Auto-inject active git diff in prompts (default: true)\n context.includeIndex Auto-inject codebase index in prompts (default: true)\n\n [Codebase Indexing Settings]\n indexing.enabled Index codebase files on CLI start (default: true)\n indexing.watch incremental index updates via watcher (default: true)\n indexing.languages Programming languages to parse/index (default: ["typescript", "python", "go", "rust"])\n indexing.exclude Folders to ignore (default: ["node_modules", "dist", ".git", "target"])\n\n [Session Settings]\n session.autoSave Auto-save session state on REPL exit (default: true)\n session.exportJsonl Export chat history to JSONL (default: true)\n session.maxHistoryTurns Max turns to retain in session state (default: 200)\n\n [UI Settings]\n ui.streaming Stream tokens in real-time (default: true)\n ui.showTokens Output token statistics (default: true)\n ui.showCost Output cost estimation stats (default: true)\n ui.diffStyle Visual diff style ("unified" | "side-by-side")\n ui.theme CLI theme colors ("dark" | "light" | "auto")\n ui.tui Launch in terminal dashboard mode by default (default: false)\n\n [Safety Settings]\n safety.protectGit Protect git workspace files (default: true)\n safety.autoApprove Skip prompt confirmations for tools (default: false)\n\n [Update Settings]\n updateCheck Check for updates on NPM on startup (default: true)',
1694
- execute: async (args, ctx) => {
1695
- const rest = args.trim();
1696
- if (!rest) {
1697
- console.log(pc.bold('\n--- Current Configuration ---'));
1698
- console.log(JSON.stringify(ctx.config, null, 2));
1699
- console.log(pc.bold('-----------------------------'));
1700
- console.log(pc.gray(`\nEdit ${ctx.configDir}/config.json to modify settings.`));
1701
- console.log(pc.gray('Or run `/config set <key> = <value>` (e.g. `/config set router.strategy = round-robin`)'));
1702
- console.log(pc.gray('Or run `/config set model.<name>.<property> = <value>` (e.g. `/config set model.lmstudio-default.tier = intelligence`)'));
1703
- return;
1704
- }
1705
- if (rest.startsWith('set ')) {
1706
- const setArgs = rest.substring(4).trim();
1707
- const eqIdx = setArgs.indexOf('=');
1708
- let key, value;
1709
- if (eqIdx >= 0) {
1710
- key = setArgs.slice(0, eqIdx).trim();
1711
- value = setArgs.slice(eqIdx + 1).trim();
1712
- }
1713
- else {
1714
- const parts = setArgs.split(/\s+/);
1715
- key = parts[0];
1716
- value = parts.slice(1).join(' ').trim();
1717
- }
1718
- if (!key || !value) {
1719
- console.log(pc.red('[WARN] Usage: /config set <key> = <value>'));
1720
- return;
1721
- }
1722
- const { saveConfig, ConfigSchema } = await import('./config/index.js');
1723
- let parsedVal = value;
1724
- if (value.toLowerCase() === 'true')
1725
- parsedVal = true;
1726
- else if (value.toLowerCase() === 'false')
1727
- parsedVal = false;
1728
- else if (!isNaN(Number(value)))
1729
- parsedVal = Number(value);
1730
- try {
1731
- if (key.startsWith('model.')) {
1732
- const parts = key.split('.');
1733
- if (parts.length < 3) {
1734
- console.log(pc.red('[WARN] Usage: /config set model.<name>.<property> = <value>'));
1735
- return;
1736
- }
1737
- const modelIdentifier = parts[1];
1738
- const property = parts.slice(2).join('.');
1739
- const chain = ctx.config.router.chain;
1740
- const modelEntry = chain.find((m) => m.name === modelIdentifier || m.model === modelIdentifier);
1741
- if (!modelEntry) {
1742
- console.log(pc.red(`[WARN] Model '${modelIdentifier}' not found in router chain.`));
1743
- return;
1744
- }
1745
- modelEntry[property] = parsedVal;
1746
- }
1747
- else {
1748
- const parts = key.split('.');
1749
- let currentObj = ctx.config;
1750
- for (let i = 0; i < parts.length - 1; i++) {
1751
- if (currentObj[parts[i]] === undefined) {
1752
- currentObj[parts[i]] = {};
1753
- }
1754
- currentObj = currentObj[parts[i]];
1755
- }
1756
- currentObj[parts[parts.length - 1]] = parsedVal;
1757
- }
1758
- const validated = ConfigSchema.parse(ctx.config);
1759
- ctx.config = validated;
1760
- saveConfig(validated);
1761
- if (ctx.router && typeof ctx.router.updateConfig === 'function') {
1762
- ctx.router.updateConfig(ctx.config.router);
1763
- }
1764
- console.log(pc.green(`[OK] Set global config: ${key} = ${value}`));
1765
- }
1766
- catch (err) {
1767
- console.log(pc.red(`[WARN] Invalid configuration value: ${err.message}`));
1768
- }
1769
- }
1770
- else {
1771
- console.log(pc.red('[WARN] Usage: /config | /config set <key> = <value>'));
1772
- }
1773
- }
1774
- },
1775
- {
1776
- name: '/doctor',
1777
- description: 'Diagnose connection and discovery',
1778
- usage: '/doctor',
1779
- helpText: 'Run diagnostics on model server connections (Ollama, LM Studio, etc.), verify model health, measure API latencies, and check location of active configurations.',
1780
- execute: async (args, ctx) => {
1781
- console.log(pc.bold('\n--- Daedalus Doctor ---'));
1782
- console.log(pc.gray('Checking local server connections...\n'));
1783
- const discovered = await discoverLocalServers();
1784
- if (discovered.length === 0) {
1785
- console.log(pc.yellow(' No local servers detected.'));
1786
- console.log(pc.gray(' Start one of:'));
1787
- console.log(pc.gray(' • LM Studio (http://localhost:1234)'));
1788
- console.log(pc.gray(' • Ollama (http://localhost:11434)'));
1789
- console.log(pc.gray(' • llama.cpp server (--server, default :8080)'));
1790
- console.log(pc.gray(' • vLLM (http://localhost:8000)'));
1791
- }
1792
- else {
1793
- console.log(pc.green(` Found ${discovered.length} running server(s):\n`));
1794
- for (const server of discovered) {
1795
- console.log(` ${pc.green('●')} ${server.name} at ${server.endpoint}`);
1796
- for (const model of server.models.slice(0, 5)) {
1797
- console.log(` - ${model}`);
1798
- }
1799
- if (server.models.length > 5) {
1800
- console.log(pc.gray(` ... and ${server.models.length - 5} more`));
1801
- }
1802
- }
1803
- }
1804
- console.log(pc.bold('\n--- Router Health ---'));
1805
- const enabledModels = ctx.router.getEnabledModels();
1806
- if (enabledModels.length === 0) {
1807
- console.log(pc.yellow(' No models configured. Run /onboard to set one up.'));
1808
- }
1809
- else {
1810
- for (const model of enabledModels) {
1811
- const { checkModelHealth } = await import('./router/health.js');
1812
- const health = await checkModelHealth(model, 5000);
1813
- const status = health.healthy ? pc.green('●') : pc.red('●');
1814
- const latency = health.latencyMs ? ` (${health.latencyMs}ms)` : '';
1815
- const err = health.error ? ` ${pc.red(health.error)}` : '';
1816
- console.log(` ${status} ${model.name}: ${model.endpoint}${latency}${err}`);
1817
- }
1818
- }
1819
- console.log(pc.bold(' Config:') + pc.gray(` ${ctx.configDir}\\config.json`));
1820
- console.log(pc.bold('----------------------\n'));
1821
- }
1822
- },
1823
- {
1824
- name: '/spec',
1825
- description: 'Flesh out a feature idea into a GitHub Issue spec (Finn Loop)',
1826
- usage: '/spec <goal>',
1827
- helpText: 'Generate detailed specifications and requirements for a coding goal.\nRuns an interactive interview query chain to clarify goals, then saves the resulting specification format.',
1828
- execute: async (args, ctx) => {
1829
- await handleSpecCommand(args, ctx);
1830
- }
1831
- },
1832
- {
1833
- name: '/stats',
1834
- aliases: ['stats'],
1835
- description: 'Display session analytics, token usage, index count, and router status',
1836
- usage: '/stats',
1837
- helpText: 'Display real-time session statistics including token counters, uptime, codebase index counts, and model router health.',
1838
- execute: async (_args, _ctx) => {
1839
- const { handleStatsCommand } = await import('./commands/stats.js');
1840
- console.log(`\n${handleStatsCommand()}\n`);
1841
- }
1842
- },
1843
- {
1844
- name: '/health',
1845
- aliases: ['health'],
1846
- description: 'Display model router provider latency, health status, and API key status',
1847
- usage: '/health [--json]',
1848
- helpText: 'Display real-time diagnostic health metrics for all configured LLM providers, including latency, availability status, and API key configuration.',
1849
- execute: async (args, _ctx) => {
1850
- const { loadConfig } = await import('./config/index.js');
1851
- const { formatHealthTable } = await import('./utils/table.js');
1852
- const { maskKey } = await import('./utils/apiKeyMask.js');
1853
- const config = loadConfig();
1854
- const providers = {};
1855
- for (const p of config.router?.chain || []) {
1856
- const isUp = p.enabled !== false;
1857
- providers[p.name || 'default'] = {
1858
- status: isUp ? 'UP' : 'DOWN',
1859
- avgLatencyMs: isUp ? 24 : null,
1860
- apiKey: p.apiKey ? maskKey(p.apiKey) : 'MISSING',
1861
- };
1862
- }
1863
- const payload = {
1864
- routerStrategy: config.router?.strategy || 'priority',
1865
- providers,
1866
- };
1867
- if (args.includes('--json') || args.includes('-j')) {
1868
- console.log(JSON.stringify(payload, null, 2));
1869
- }
1870
- else {
1871
- console.log(`\n${formatHealthTable(payload)}\n`);
1872
- }
1873
- }
1874
- },
1875
- {
1876
- name: '/help',
1877
- aliases: ['?', 'help'],
1878
- description: 'Show available commands or detailed info for a specific command',
1879
- usage: '/help [command_name]',
1880
- helpText: 'Display general help or a detailed "man page" for a given slash command.',
1881
- execute: async (args, _ctx) => {
1882
- const query = args.trim().toLowerCase();
1883
- if (query) {
1884
- const cmdName = query.startsWith('/') ? query : `/${query}`;
1885
- const cmd = commandsList.find(c => c.name.toLowerCase() === cmdName ||
1886
- c.name.toLowerCase() === query ||
1887
- c.aliases?.some(alias => alias.toLowerCase() === cmdName || alias.toLowerCase() === query));
1888
- if (!cmd) {
1889
- console.log(pc.red(`\n [WARN] Unknown command: "${query}". Type /help to see all commands.`));
1890
- return;
1891
- }
1892
- console.log(pc.bold(`\n=== COMMAND MANUAL: ${cmd.name} ===`));
1893
- console.log(` ${pc.bold('Description:')} ${cmd.description}`);
1894
- if (cmd.usage) {
1895
- console.log(` ${pc.bold('Usage:')} ${pc.cyan(cmd.usage)}`);
1896
- }
1897
- if (cmd.aliases && cmd.aliases.length > 0) {
1898
- const formattedAliases = cmd.aliases.map(a => a.startsWith('/') ? a : `/${a}`).join(', ');
1899
- console.log(` ${pc.bold('Aliases:')} ${pc.yellow(formattedAliases)}`);
1900
- }
1901
- if (cmd.helpText) {
1902
- console.log(`\n${pc.bold('Details:')}\n${cmd.helpText.split('\n').map(line => ` ${line}`).join('\n')}`);
1903
- }
1904
- console.log(pc.bold('='.repeat(20 + cmd.name.length)));
1905
- console.log();
1906
- return;
1907
- }
1908
- console.log(pc.bold('\n--- Available Commands ---'));
1909
- for (const cmd of commandsList) {
1910
- const aliasList = cmd.aliases ? cmd.aliases.map(a => a.startsWith('/') ? a : `/${a}`) : [];
1911
- const nameAndAliases = [cmd.name, ...aliasList].join(', ');
1912
- console.log(` ${pc.cyan(nameAndAliases.padEnd(30))} - ${cmd.description}`);
1913
- }
1914
- console.log(pc.bold('--------------------------'));
1915
- console.log(pc.gray(' Detailed documentation: ') + pc.underline(pc.cyan('https://bgill55.github.io/daedalus/#/')));
1916
- console.log(pc.gray(' Tip: Type ') + pc.cyan('/help <command>') + pc.gray(' for detailed usage and subcommands (e.g. /help config)'));
1917
- console.log();
1918
- }
1919
- },
1920
- {
1921
- name: '/mcp',
1922
- description: 'Manage MCP servers: explore, search, install, list, remove, info',
1923
- usage: '/mcp <subcommand> [args]',
1924
- helpText: 'Configure and interact with Model Context Protocol (MCP) servers.\n\nSubcommands:\n explore, ex Browse curated featured community MCP servers\n list, l List all installed MCP servers and their active state\n search, s <query> Search the public MCP Registry for available servers\n install, i <name> Install an MCP server from the registry\n remove, rm <name> Uninstall an MCP server\n info <name> Display metadata and information for a registry server\n enable <name> Enable a configured server\n disable <name> Disable a configured server without removing it',
1925
- execute: async (args, _ctx) => {
1926
- const parts = args.trim().split(/\s+/);
1927
- const sub = parts[0]?.toLowerCase();
1928
- const rest = parts.slice(1).join(' ').trim();
1929
- const { searchRegistry, fetchServerByName, fetchAllServers, registryEntryToConfig, addServerToConfig, removeServerFromConfig, listInstalledServers, toggleServer } = await import('./tools/mcp/manager.js');
1930
- const { mcpRegistry } = await import('./tools/mcp/registry.js');
1931
- switch (sub) {
1932
- case 'search':
1933
- case 's': {
1934
- if (!rest) {
1935
- console.log(pc.yellow(' Usage: /mcp search <query>'));
1936
- return;
1937
- }
1938
- console.log(pc.dim(` Searching registry for "${rest}"...`));
1939
- try {
1940
- const results = await searchRegistry(rest, 15);
1941
- if (results.length === 0) {
1942
- console.log(pc.yellow(' No servers found. Try a broader search.'));
1943
- return;
1944
- }
1945
- console.log(`\n ${pc.bold(`Found ${results.length} server(s):`)}`);
1946
- for (const s of results) {
1947
- const label = s.title || s.name;
1948
- const desc = s.description.length > 80 ? s.description.slice(0, 80) + '…' : s.description;
1949
- const remote = s.remotes?.[0]?.url || '';
1950
- const pkg = s.packages?.[0]?.identifier || '';
1951
- const source = remote || pkg || '(no install info)';
1952
- const installType = s.packages ? 'stdio' : s.remotes ? 'http' : '?';
1953
- console.log(` ${pc.cyan(label)}`);
1954
- console.log(` ${pc.dim(desc)}`);
1955
- console.log(` ${pc.gray('Install:')} ${pc.dim(source)} (${installType})`);
1956
- console.log();
1957
- }
1958
- }
1959
- catch (err) {
1960
- console.log(pc.red(` Search failed: ${err.message}`));
1961
- }
1962
- return;
1963
- }
1964
- case 'install':
1965
- case 'i': {
1966
- if (!rest) {
1967
- console.log(pc.yellow(' Usage: /mcp install <server-name>'));
1968
- console.log(pc.dim(' First search for a server with: /mcp search <query>'));
1969
- return;
1970
- }
1971
- console.log(pc.dim(` Fetching "${rest}" from registry...`));
1972
- try {
1973
- const entry = await fetchServerByName(rest);
1974
- if (!entry) {
1975
- console.log(pc.yellow(` Server "${rest}" not found in registry. Try /mcp search first.`));
1976
- return;
1977
- }
1978
- const config = registryEntryToConfig(entry);
1979
- if (!config) {
1980
- console.log(pc.yellow(` Cannot install "${rest}": no stdio package or remote URL found.`));
1981
- return;
1982
- }
1983
- const result = addServerToConfig(config);
1984
- if (result.success) {
1985
- console.log(pc.green(` ${result.message}`));
1986
- console.log(pc.dim(' Restart Daedalus or reconnect to load the new server.'));
1987
- }
1988
- else {
1989
- console.log(pc.yellow(` ${result.message}`));
1990
- }
1991
- }
1992
- catch (err) {
1993
- console.log(pc.red(` Install failed: ${err.message}`));
1994
- }
1995
- return;
1996
- }
1997
- case 'explore':
1998
- case 'ex': {
1999
- console.log(pc.dim(' Browsing the MCP registry...\n'));
2000
- try {
2001
- const all = await fetchAllServers(100);
2002
- const local = all.filter(s => s.packages && s.packages.length > 0);
2003
- const remote = all.filter(s => s.remotes && s.remotes.length > 0);
2004
- console.log(` ${pc.bold(`Found ${all.length} servers in registry`)}`);
2005
- const showSample = (list, label, max = 5) => {
2006
- if (list.length === 0)
2007
- return;
2008
- console.log(`\n ${pc.underline(label)} (${list.length} available)`);
2009
- for (const s of list.slice(0, max)) {
2010
- const pkg = s.packages?.[0]?.identifier || '';
2011
- const url = s.remotes?.[0]?.url || '';
2012
- const source = pkg || url;
2013
- const info = s.description.length > 55 ? s.description.slice(0, 53) + '…' : s.description;
2014
- const showName = s.name.length > 28 ? s.name.slice(0, 26) + '…' : s.name;
2015
- console.log(` ${pc.cyan(showName.padEnd(30))} ${pc.dim(info)}`);
2016
- console.log(` ${' '.repeat(30)} ${pc.gray('→')} ${pc.dim(source)}`);
2017
- }
2018
- if (list.length > max) {
2019
- console.log(` ${' '.repeat(30)} ${pc.dim(`… and ${list.length - max} more`)}`);
2020
- }
2021
- };
2022
- showSample(local, 'Local (stdio — install & run)', 6);
2023
- showSample(remote, 'Remote (HTTP — cloud API)', 6);
2024
- console.log(`\n ${pc.dim('Tip: /mcp search <query> to find specific servers')}`);
2025
- }
2026
- catch (err) {
2027
- console.log(pc.red(` Explore failed: ${err.message}`));
2028
- }
2029
- return;
2030
- }
2031
- case 'list':
2032
- case 'ls':
2033
- case 'l': {
2034
- const servers = listInstalledServers();
2035
- if (servers.length === 0) {
2036
- console.log(pc.yellow(' No MCP servers installed.'));
2037
- console.log(pc.dim(' Try /mcp explore to see what\'s available.'));
2038
- return;
2039
- }
2040
- const connected = mcpRegistry.getConnectedServers();
2041
- console.log(`\n ${pc.bold('Installed MCP Servers:')}`);
2042
- for (const s of servers) {
2043
- const status = connected.includes(s.name) ? pc.green('●') : s.enabled ? pc.yellow('○') : pc.red('○');
2044
- const state = connected.includes(s.name) ? pc.green('connected')
2045
- : s.enabled ? pc.yellow('pending')
2046
- : pc.red('disabled');
2047
- console.log(` ${status} ${pc.cyan(s.name.padEnd(20))} ${pc.dim(s.transport.padEnd(6))} ${state}`);
2048
- }
2049
- console.log();
2050
- return;
2051
- }
2052
- case 'remove':
2053
- case 'rm':
2054
- case 'r': {
2055
- if (!rest) {
2056
- console.log(pc.yellow(' Usage: /mcp remove <server-name>'));
2057
- return;
2058
- }
2059
- const result = removeServerFromConfig(rest);
2060
- if (result.success) {
2061
- console.log(pc.green(` ${result.message}`));
2062
- }
2063
- else {
2064
- console.log(pc.yellow(` ${result.message}`));
2065
- }
2066
- return;
2067
- }
2068
- case 'info': {
2069
- if (!rest) {
2070
- console.log(pc.yellow(' Usage: /mcp info <server-name>'));
2071
- return;
2072
- }
2073
- try {
2074
- console.log(pc.dim(` Fetching "${rest}" from registry...`));
2075
- const entry = await fetchServerByName(rest);
2076
- if (!entry) {
2077
- console.log(pc.yellow(` Server "${rest}" not found.`));
2078
- return;
2079
- }
2080
- console.log(`\n ${pc.bold(entry.title || entry.name)}`);
2081
- console.log(` ${pc.dim(entry.description)}`);
2082
- console.log(` ${pc.gray('Name:')} ${entry.name}`);
2083
- console.log(` ${pc.gray('Version:')} ${entry.version}`);
2084
- if (entry.websiteUrl)
2085
- console.log(` ${pc.gray('Website:')} ${entry.websiteUrl}`);
2086
- if (entry.repository?.url)
2087
- console.log(` ${pc.gray('Source:')} ${entry.repository.url}`);
2088
- if (entry.remotes && entry.remotes.length > 0) {
2089
- console.log(`\n ${pc.bold('Remote endpoints:')}`);
2090
- for (const r of entry.remotes) {
2091
- console.log(` ${pc.cyan(r.type)} ${pc.dim(r.url)}`);
2092
- if (r.headers) {
2093
- for (const h of r.headers) {
2094
- const req = h.isRequired ? pc.yellow(' (required)') : '';
2095
- const secret = h.isSecret ? pc.dim(' [secret]') : '';
2096
- console.log(` ${pc.gray('Header:')} ${h.name}${req}${secret}`);
2097
- }
2098
- }
2099
- }
2100
- }
2101
- if (entry.packages && entry.packages.length > 0) {
2102
- console.log(`\n ${pc.bold('Packages:')}`);
2103
- for (const p of entry.packages) {
2104
- const [cmd, ...args] = p.registryType === 'npm' ? ['npx', '-y', p.identifier]
2105
- : p.registryType === 'pypi' ? ['uvx', p.identifier]
2106
- : [p.identifier];
2107
- console.log(` ${pc.cyan(p.registryType)} ${pc.dim(`${cmd} ${args.join(' ')}`)}`);
2108
- if (p.environmentVariables) {
2109
- for (const env of p.environmentVariables) {
2110
- const req = env.isRequired ? pc.yellow(' (required)') : '';
2111
- const secret = env.isSecret ? pc.dim(' [secret]') : '';
2112
- console.log(` ${pc.gray('Env:')} ${env.name}${req}${secret}`);
2113
- if (env.description)
2114
- console.log(` ${pc.dim(env.description)}`);
2115
- }
2116
- }
2117
- }
2118
- }
2119
- console.log();
2120
- }
2121
- catch (err) {
2122
- console.log(pc.red(` Info fetch failed: ${err.message}`));
2123
- }
2124
- return;
2125
- }
2126
- case 'reconnect':
2127
- case 'rc': {
2128
- const { loadConfig } = await import('./config/index.js');
2129
- const config = loadConfig();
2130
- const mcpConfigs = Object.entries(config.tools.mcpServers)
2131
- .filter(([_, s]) => s.enabled)
2132
- .map(([name, s]) => ({
2133
- name,
2134
- transport: s.transport,
2135
- command: s.command,
2136
- args: s.args,
2137
- url: s.url,
2138
- headers: s.headers,
2139
- enabled: s.enabled,
2140
- }));
2141
- const already = mcpRegistry.getConnectedServers();
2142
- const newServers = mcpConfigs.filter(c => !already.includes(c.name));
2143
- if (newServers.length === 0) {
2144
- if (mcpConfigs.length === 0) {
2145
- console.log(pc.yellow(' No enabled MCP servers configured. Install one with /mcp install'));
2146
- }
2147
- else {
2148
- console.log(pc.dim(' All enabled MCP servers are already connected.'));
2149
- }
2150
- return;
2151
- }
2152
- mcpRegistry.setConfigs(mcpConfigs);
2153
- const connected = [];
2154
- const failed = [];
2155
- for (const s of newServers) {
2156
- try {
2157
- await mcpRegistry.connectServer(s);
2158
- connected.push(s.name);
2159
- }
2160
- catch (err) {
2161
- failed.push(`${s.name} (${err.message})`);
2162
- }
2163
- }
2164
- if (connected.length > 0) {
2165
- const totalTools = mcpRegistry.getToolDefinitions().length;
2166
- console.log(pc.green(` Connected: ${connected.join(', ')} (${totalTools} MCP tools total)`));
2167
- }
2168
- if (failed.length > 0) {
2169
- console.log(pc.yellow(` Failed: ${failed.join(', ')}`));
2170
- }
2171
- return;
2172
- }
2173
- case 'enable':
2174
- case 'e': {
2175
- if (!rest) {
2176
- console.log(pc.yellow(' Usage: /mcp enable <server-name>'));
2177
- return;
2178
- }
2179
- const enableResult = toggleServer(rest, true);
2180
- console.log(enableResult.success ? pc.green(` ${enableResult.message}`) : pc.yellow(` ${enableResult.message}`));
2181
- return;
2182
- }
2183
- case 'disable':
2184
- case 'd': {
2185
- if (!rest) {
2186
- console.log(pc.yellow(' Usage: /mcp disable <server-name>'));
2187
- return;
2188
- }
2189
- const disableResult = toggleServer(rest, false);
2190
- console.log(disableResult.success ? pc.green(` ${disableResult.message}`) : pc.yellow(` ${disableResult.message}`));
2191
- return;
2192
- }
2193
- default:
2194
- console.log(pc.bold('\n MCP Server Manager'));
2195
- console.log(` ${pc.cyan('/mcp explore')} ${pc.dim('Browse available servers in the registry')}`);
2196
- console.log(` ${pc.cyan('/mcp search <query>')} ${pc.dim('Search the official MCP registry')}`);
2197
- console.log(` ${pc.cyan('/mcp install <name>')} ${pc.dim('Install a server from the registry')}`);
2198
- console.log(` ${pc.cyan('/mcp list')} ${pc.dim('List installed servers')}`);
2199
- console.log(` ${pc.cyan('/mcp remove <name>')} ${pc.dim('Remove an installed server')}`);
2200
- console.log(` ${pc.cyan('/mcp info <name>')} ${pc.dim('Show server details')}`);
2201
- console.log(` ${pc.cyan('/mcp reconnect')} ${pc.dim('Reconnect all enabled servers')}`);
2202
- console.log(` ${pc.cyan('/mcp enable <name>')} ${pc.dim('Enable a disabled server')}`);
2203
- console.log(` ${pc.cyan('/mcp disable <name>')} ${pc.dim('Disable a server without removing it')}`);
2204
- console.log(`\n ${pc.bold('Zero-config starters (no API keys needed):')}`);
2205
- console.log(` ${pc.gray('→')} ${pc.cyan('io.github/modelcontextprotocol/sequential-thinking')} ${pc.dim('Step-by-step reasoning')}`);
2206
- console.log(` ${pc.gray('→')} ${pc.cyan('io.github/modelcontextprotocol/filesystem')} ${pc.dim('Read/write files in allowed dirs')}`);
2207
- console.log(` ${pc.gray('→')} ${pc.cyan('io.github/modelcontextprotocol/memory')} ${pc.dim('Persistent key-value store')}`);
2208
- console.log(` ${pc.gray('→')} ${pc.cyan('io.github/modelcontextprotocol/fetch')} ${pc.dim('Fetch URLs and extract content')}`);
2209
- console.log(` ${pc.gray('→')} ${pc.cyan('io.github/modelcontextprotocol/puppeteer')} ${pc.dim('Browser automation')}`);
2210
- console.log(` ${pc.gray('→')} ${pc.cyan('ai.ankimcp/anki-mcp-server')} ${pc.dim('Anki flashcard management')}`);
2211
- console.log(` ${pc.dim(' /mcp install <name> to install any of the above')}`);
2212
- console.log();
2213
- }
2214
- }
2215
- },
2216
- {
2217
- name: '/onboard',
2218
- description: 'First-time setup — discover local models, configure, and test',
2219
- usage: '/onboard',
2220
- helpText: 'Run the interactive setup wizard to scan your local network/environment for model servers, select a primary model tier, and test its output/diagnostics.',
2221
- execute: async (_args, ctx) => {
2222
- const config = ctx.config;
2223
- console.log(pc.bold(pc.cyan('\n╔══════════════════════════════════════╗')));
2224
- console.log(pc.bold(pc.cyan('║ Daedalus Onboarding ║')));
2225
- console.log(pc.bold(pc.cyan('╚══════════════════════════════════════╝')));
2226
- console.log();
2227
- console.log('Daedalus runs AI models locally on your machine.');
2228
- console.log('First, I need to know which model server to use.');
2229
- console.log();
2230
- // Step 1: Discover local model servers
2231
- console.log(pc.bold('🔍 Scanning for local model servers...'));
2232
- const discovered = await discoverLocalServers();
2233
- let chosenEndpoint = '';
2234
- let chosenModel = '';
2235
- if (discovered.length > 0) {
2236
- console.log(pc.green(`\n Found ${discovered.length} running server(s):\n`));
2237
- for (let i = 0; i < discovered.length; i++) {
2238
- const s = discovered[i];
2239
- console.log(` ${i + 1}. ${pc.cyan(s.name)} at ${s.endpoint}`);
2240
- for (const m of s.models.slice(0, 3)) {
2241
- console.log(` - ${m}`);
2242
- }
2243
- if (s.models.length > 3) {
2244
- console.log(pc.gray(` ... and ${s.models.length - 3} more`));
2245
- }
2246
- }
2247
- console.log();
2248
- const serverChoice = await ctx.askLine(`Select a server (1-${discovered.length}) or press Enter to add manually: `);
2249
- const idx = parseInt(serverChoice) - 1;
2250
- if (idx >= 0 && idx < discovered.length) {
2251
- const server = discovered[idx];
2252
- chosenEndpoint = server.endpoint;
2253
- if (server.models.length === 1) {
2254
- chosenModel = server.models[0];
2255
- }
2256
- else {
2257
- console.log(`\nModels on ${pc.cyan(server.name)}:`);
2258
- for (let i = 0; i < server.models.length; i++) {
2259
- console.log(` ${i + 1}. ${server.models[i]}`);
2260
- }
2261
- const modelChoice = await ctx.askLine(`Select a model (1-${server.models.length}): `);
2262
- const midx = parseInt(modelChoice) - 1;
2263
- if (midx >= 0 && midx < server.models.length) {
2264
- chosenModel = server.models[midx];
2265
- }
2266
- }
2267
- }
2268
- }
2269
- if (!chosenEndpoint) {
2270
- console.log(`\nEnter your model server details manually.`);
2271
- chosenEndpoint = await ctx.askLine('API endpoint (e.g. http://localhost:1234/v1): ');
2272
- if (!chosenEndpoint)
2273
- chosenEndpoint = 'http://localhost:1234/v1';
2274
- chosenModel = await ctx.askLine('Model name (e.g. qwen2.5-coder-7b-instruct): ');
2275
- if (!chosenModel)
2276
- chosenModel = 'auto';
2277
- }
2278
- if (!chosenModel)
2279
- chosenModel = 'auto';
2280
- // Step 2: Add to config
2281
- const entry = {
2282
- name: chosenModel,
2283
- endpoint: chosenEndpoint,
2284
- model: chosenModel,
2285
- priority: 1,
2286
- enabled: true,
2287
- };
2288
- // Replace any existing chain or add to it
2289
- config.router.chain = [entry, ...config.router.chain.filter((e) => e.endpoint !== chosenEndpoint)];
2290
- saveConfig(config);
2291
- console.log(pc.green(`\n✓ Added model "${pc.bold(chosenModel)}" at ${chosenEndpoint}`));
2292
- // Step 3: Test the model
2293
- const testPrompt = await ctx.askLine('\nRun a quick test? (Y/n): ');
2294
- if (testPrompt.toLowerCase() !== 'n') {
2295
- console.log(pc.dim('\nSending test request...'));
2296
- try {
2297
- const start = Date.now();
2298
- const testMessages = [
2299
- { role: 'system', content: 'You are a helpful assistant. Respond in 1-2 sentences.' },
2300
- { role: 'user', content: 'Say hello and confirm you are working.' },
2301
- ];
2302
- const testRouter = ctx.router;
2303
- const completion = await testRouter.chat.completions.create({
2304
- model: chosenModel,
2305
- messages: testMessages,
2306
- temperature: 0.1,
2307
- });
2308
- const elapsed = Date.now() - start;
2309
- const text = completion.choices?.[0]?.message?.content || '(no response)';
2310
- console.log(pc.green(`\n✓ Response received in ${elapsed}ms:`));
2311
- console.log(` ${pc.white(text)}`);
2312
- }
2313
- catch (err) {
2314
- console.log(pc.yellow(`\n⚠ Test failed: ${err.message}`));
2315
- console.log(' The model is configured but may need troubleshooting.');
2316
- console.log(` Check ${pc.cyan(ctx.configDir + '/config.json')} and verify the endpoint.`);
2317
- }
2318
- }
2319
- console.log(pc.green(`\n✓ Onboarding complete! Configuration saved to:`));
2320
- console.log(` ${pc.cyan(ctx.configDir + '/config.json')}`);
2321
- console.log(`\nType ${pc.cyan('?')} to see all available commands, or just start typing.`);
2322
- }
2323
- },
2324
- {
2325
- name: '/tui',
2326
- description: 'Toggle the Terminal User Interface (TUI) dashboard',
2327
- usage: '/tui',
2328
- helpText: 'Switch between standard REPL chat mode and the side-by-side Terminal dashboard mode (which includes resource charts, model settings, and context monitors).',
2329
- execute: async (args, ctx) => {
2330
- if (!ctx.rl) {
2331
- throw new Error('SWITCH_MODE_CLI');
2332
- }
2333
- else {
2334
- throw new Error('SWITCH_MODE_TUI');
2335
- }
2336
- }
2337
- },
2338
- {
2339
- name: '/image',
2340
- description: 'Generate an image using local Stable Diffusion WebUI or Pollinations AI',
2341
- usage: '/image <prompt> [--output path] [--provider auto|sd-webui|pollinations] [--width 512] [--height 512] [--steps 20]',
2342
- helpText: 'Generate an image using a local Stable Diffusion WebUI instance (http://127.0.0.1:7860) or free Pollinations AI.\n\nArguments:\n <prompt> Detailed description of the image to generate\n --provider <engine> Engine: auto (local SD with Pollinations fallback), sd-webui, or pollinations\n --output <path> Filepath to save PNG (default: ./assets/images/img_<timestamp>.png)\n --width <pixels> Image width (default: 512)\n --height <pixels> Image height (default: 512)\n --steps <count> Sampling steps for local SD (default: 20)',
2343
- execute: async (args, _ctx) => {
2344
- const promptText = args.trim();
2345
- if (!promptText) {
2346
- console.log(pc.yellow('Usage: /image <prompt> [--provider auto|sd-webui|pollinations] [--output path] [--width 512] [--height 512] [--steps 20]'));
2347
- return;
2348
- }
2349
- console.log(pc.cyan(`\n Generating image...`));
2350
- const { generateImage } = await import('./tools/builtin/image.js');
2351
- let width;
2352
- let height;
2353
- let steps;
2354
- let provider;
2355
- let output_path;
2356
- const cleanedPrompt = promptText
2357
- .replace(/--provider\s+([^\s]+)/i, (_, pr) => {
2358
- if (['auto', 'sd-webui', 'pollinations'].includes(pr.toLowerCase())) {
2359
- provider = pr.toLowerCase();
2360
- }
2361
- return '';
2362
- })
2363
- .replace(/--output\s+([^\s]+)/i, (_, p) => { output_path = p; return ''; })
2364
- .replace(/--width\s+(\d+)/i, (_, w) => { width = parseInt(w, 10); return ''; })
2365
- .replace(/--height\s+(\d+)/i, (_, h) => { height = parseInt(h, 10); return ''; })
2366
- .replace(/--steps\s+(\d+)/i, (_, s) => { steps = parseInt(s, 10); return ''; })
2367
- .trim();
2368
- const res = await generateImage({
2369
- prompt: cleanedPrompt || promptText,
2370
- width,
2371
- height,
2372
- steps,
2373
- provider,
2374
- output_path,
2375
- });
2376
- if (res.success) {
2377
- console.log(pc.green(`\n✔ ${res.content}`));
2378
- }
2379
- else {
2380
- console.log(pc.red(`\n✗ Image generation failed: ${res.error}`));
2381
- }
2382
- }
2383
- },
2384
- {
2385
- name: '/autopilot',
2386
- description: 'Autonomously implement a feature: branch, code, test, commit, and PR',
2387
- usage: '/autopilot <feature description>',
2388
- helpText: 'End-to-end autonomous feature development. Creates a branch, plans and implements the feature, runs verification, commits, pushes, and opens a pull request.\n\nFlow:\n 1. Interactive Q&A to refine the feature spec\n 2. Creates a git branch (daedalus-autopilot-<slug>)\n 3. Runs the multi-agent orchestrator to implement it\n 4. Verifies with build/lint/tests\n 5. Commits and pushes to GitHub\n 6. Opens a Pull Request against main\n\nRequires a GitHub repository with a configured remote origin.',
2389
- execute: async (args, ctx) => {
2390
- const idea = args.trim();
2391
- if (!idea) {
2392
- console.log(pc.red('[WARN] Usage: /autopilot <feature description>'));
2393
- return;
2394
- }
2395
- const repoInfo = getGitRepoInfo(ctx.toolContext.projectRoot);
2396
- if (!repoInfo) {
2397
- console.log(pc.yellow('[INFO] No GitHub remote found. Running in local-only mode (no PR will be created).'));
2398
- }
2399
- const slug = idea.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
2400
- const branchName = `daedalus-autopilot-${slug}`;
2401
- try {
2402
- execSync(`git checkout -B ${branchName}`, { cwd: ctx.toolContext.projectRoot });
2403
- console.log(pc.green(`[OK] Created branch: ${branchName}`));
2404
- }
2405
- catch (err) {
2406
- const msg = err instanceof Error ? err.message : String(err);
2407
- console.log(pc.red(`[ERROR] Failed to create branch: ${msg}`));
2408
- return;
2409
- }
2410
- const goal = `Implement the following feature: ${idea}`;
2411
- console.log(pc.cyan(`\n[AUTOPILOT] Starting autonomous implementation...`));
2412
- process.env.DAEDALUS_AUTO_APPROVE = 'true';
2413
- try {
2414
- const { Orchestrator } = await import('./agents/orchestrator.js');
2415
- const orchestrator = new Orchestrator(ctx.router, ctx.messages, ctx.toolContext, ctx.sessionManager);
2416
- const result = await orchestrator.run(goal);
2417
- console.log(pc.white(`\n${result}`));
2418
- const orchestrationFailed = result.startsWith('Orchestration failed') || result.includes('## Orchestration Hit Verification Failures');
2419
- const wasAborted = result.includes('## Orchestration Paused');
2420
- if (orchestrationFailed || wasAborted) {
2421
- throw new Error(orchestrationFailed ? 'Orchestration reported failure' : 'Orchestration was paused/aborted');
2422
- }
2423
- }
2424
- catch (err) {
2425
- const msg = err instanceof Error ? err.message : String(err);
2426
- console.log(pc.red(`\n[ERROR] Implementation failed: ${msg}`));
2427
- console.log(pc.yellow('[ROLLBACK] Rolling back to main branch...'));
2428
- try {
2429
- execSync('git reset --hard', { cwd: ctx.toolContext.projectRoot });
2430
- execSync('git checkout main', { cwd: ctx.toolContext.projectRoot });
2431
- execSync(`git branch -D ${branchName}`, { cwd: ctx.toolContext.projectRoot });
2432
- console.log(pc.green('[OK] Rolled back to main. Branch deleted.'));
2433
- }
2434
- catch (rollbackErr) {
2435
- const rbMsg = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
2436
- console.log(pc.red(`[ERROR] Rollback failed: ${rbMsg}. Manual cleanup may be needed.`));
2437
- }
2438
- return;
2439
- }
2440
- console.log(pc.cyan('\n[AUTOPILOT] Committing changes...'));
2441
- try {
2442
- execSync('git add .', { cwd: ctx.toolContext.projectRoot });
2443
- const cleanTitle = idea.replace(/[^a-zA-Z0-9 ]/g, '').trim();
2444
- execSync(`git commit -m "feat: ${cleanTitle}"`, { cwd: ctx.toolContext.projectRoot });
2445
- console.log(pc.green('[OK] Changes committed.'));
2446
- }
2447
- catch (err) {
2448
- const msg = err instanceof Error ? err.message : String(err);
2449
- if (msg.includes('nothing to commit')) {
2450
- console.log(pc.yellow('[INFO] No changes to commit.'));
2451
- }
2452
- else {
2453
- console.log(pc.red(`[ERROR] Failed to commit: ${msg}`));
2454
- return;
2455
- }
2456
- }
2457
- if (repoInfo) {
2458
- console.log(pc.cyan('\n[AUTOPILOT] Pushing branch and creating PR...'));
2459
- let token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
2460
- if (!token) {
2461
- try {
2462
- token = execSync('gh auth token', { encoding: 'utf8' }).trim();
2463
- }
2464
- catch {
2465
- console.log(pc.yellow('[INFO] No GitHub token found. Run `gh auth login` or set GITHUB_TOKEN.'));
2466
- console.log(pc.yellow(`[INFO] Branch ${branchName} is ready locally. Push manually.`));
2467
- return;
2468
- }
2469
- }
2470
- try {
2471
- execSync(`git push -u origin ${branchName} --force`, { cwd: ctx.toolContext.projectRoot });
2472
- const prResponse = await fetch(`https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/pulls`, {
2473
- method: 'POST',
2474
- headers: {
2475
- 'Authorization': `Bearer ${token}`,
2476
- 'Content-Type': 'application/json',
2477
- },
2478
- body: JSON.stringify({
2479
- title: `[Autopilot] ${idea}`,
2480
- head: branchName,
2481
- base: 'main',
2482
- body: `## Description\n\nAutonomously implemented by Daedalus Autopilot.\n\n**Feature:** ${idea}\n\n---\n_Generated by \`/autopilot\`_`,
2483
- }),
2484
- });
2485
- if (prResponse.ok) {
2486
- const pr = await prResponse.json();
2487
- console.log(pc.green(`\n[OK] Pull Request created: ${pr.html_url}`));
2488
- }
2489
- else {
2490
- const errText = await prResponse.text();
2491
- console.log(pc.red(`[ERROR] Failed to create PR: ${prResponse.status} ${errText}`));
2492
- console.log(pc.yellow(`[INFO] Branch ${branchName} is pushed. Create PR manually.`));
2493
- }
2494
- }
2495
- catch (err) {
2496
- const msg = err instanceof Error ? err.message : String(err);
2497
- console.log(pc.red(`[ERROR] Push/PR failed: ${msg}`));
2498
- console.log(pc.yellow(`[INFO] Branch ${branchName} is ready locally.`));
2499
- }
2500
- }
2501
- else {
2502
- console.log(pc.yellow('\n[INFO] No GitHub remote configured. Implementation is committed locally.'));
2503
- console.log(pc.yellow(`[INFO] Branch: ${branchName}`));
2504
- }
2505
- console.log(pc.cyan(`\n[AUTOPILOT] Done! Run 'git checkout main' to return to main branch.`));
2506
- }
2507
- },
2508
- {
2509
- name: '/preview',
2510
- description: 'Screenshot a local HTML file or URL and save the image',
2511
- usage: '/preview <filepath | url>',
2512
- helpText: 'Opens the given HTML file or URL in headless Chrome and saves a PNG screenshot.\n Examples:\n /preview preview.html\n /preview http://localhost:3000\n /preview ./src/components/output.html',
2513
- execute: async (args, ctx) => {
2514
- const target = args.trim();
2515
- if (!target) {
2516
- console.log(pc.red('[WARN] Usage: /preview <filepath or URL>'));
2517
- return;
2518
- }
2519
- let url = target;
2520
- if (!/^https?:\/\//i.test(target) && !/^file:\/\//i.test(target)) {
2521
- const absPath = path.resolve(target);
2522
- if (!fs.existsSync(absPath)) {
2523
- console.log(pc.red(`[ERROR] File not found: ${absPath}`));
2524
- return;
2525
- }
2526
- url = `file:///${absPath.replace(/\\/g, '/')}`;
2527
- }
2528
- console.log(pc.dim(`[PREVIEW] Screenshotting ${url}...`));
2529
- try {
2530
- const { screenshotPage } = await import('./tools/builtin/screenshot.js');
2531
- const result = await screenshotPage({ url }, ctx.toolContext);
2532
- if (!result.success) {
2533
- console.log(pc.red(`[ERROR] ${result.error || 'Screenshot failed'}`));
2534
- return;
2535
- }
2536
- const data = JSON.parse(result.content);
2537
- console.log(pc.green(`[OK] Screenshot saved to: ${data.savedPath}`));
2538
- console.log(pc.dim(` URL: ${data.url}`));
2539
- }
2540
- catch (err) {
2541
- const msg = err instanceof Error ? err.message : String(err);
2542
- console.log(pc.red(`[ERROR] Preview failed: ${msg}`));
2543
- }
2544
- }
2545
- },
2546
- {
2547
- name: '/history',
2548
- aliases: ['/h'],
2549
- description: 'Show recent turns with tool calls from the session log',
2550
- usage: '/history [n]',
2551
- helpText: 'Display the last N assistant/user turns from the SQLite session log, including tool calls and response previews. Default: 5.',
2552
- execute: async (args, ctx) => {
2553
- const n = parseInt((args || '5').trim(), 10);
2554
- if (isNaN(n) || n < 1) {
2555
- console.log(pc.red('[ERROR] Provide a positive number'));
2556
- return;
2557
- }
2558
- const turns = getTurns(ctx.sessionManager.db);
2559
- const recent = turns.slice(-n);
2560
- for (const t of recent) {
2561
- const roleColor = t.role === 'assistant' ? pc.cyan : t.role === 'tool' ? pc.yellow : pc.white;
2562
- const roleLabel = t.role === 'assistant' ? 'Assistant' : t.role === 'tool' ? 'Tool' : 'User';
2563
- const meta = [];
2564
- if (t.model)
2565
- meta.push(pc.dim(t.model));
2566
- if (t.tokens_output)
2567
- meta.push(pc.dim(`~${Math.round(t.tokens_output / 4)} tok out`));
2568
- if (t.latency_ms) {
2569
- const el = t.latency_ms >= 1000 ? `${(t.latency_ms / 1000).toFixed(1)}s` : `${t.latency_ms}ms`;
2570
- meta.push(pc.dim(el));
2571
- }
2572
- const metaStr = meta.length ? ` ${meta.join(' · ')}` : '';
2573
- console.log(`\n ${roleColor(pc.bold(`#${t.id ?? '?'} ${roleLabel}`))}${metaStr}`);
2574
- if (t.tool_calls) {
2575
- try {
2576
- const parsed = JSON.parse(t.tool_calls);
2577
- const names = parsed.map(c => c.function?.name ?? '?');
2578
- console.log(` ${pc.dim('Tools:')} ${names.join(', ')}`);
2579
- }
2580
- catch { /* not JSON, skip */ }
2581
- }
2582
- if (t.content) {
2583
- const preview = t.content.replace(/```[\s\S]*?```/g, '[code block]').split('\n').slice(0, 3).join('\n ').slice(0, 300);
2584
- if (preview)
2585
- console.log(` ${preview}`);
2586
- }
2587
- }
2588
- if (recent.length === 0)
2589
- console.log(pc.gray(' No turns in session yet.'));
2590
- }
2591
- },
2592
- {
2593
- name: '/exit',
2594
- aliases: ['/quit', '/bye'],
2595
- description: 'Save session and exit',
2596
- execute: async (args, ctx) => {
2597
- const todos = getSessionTodos(ctx.toolContext.sessionId);
2598
- ctx.sessionManager.saveSessionState(ctx.messages, ctx.activeFiles, todos);
2599
- console.log(pc.dim(' [EXTRACT] Extracting facts from session...'));
2600
- await extractAndSave(ctx.router, ctx.sessionManager, ctx.messages);
2601
- console.log(pc.gray(`Session saved: ${ctx.sessionManager.sessionId}`));
2602
- console.log(pc.yellow('\nEnding session. Goodbye!\n'));
2603
- ctx.rl.close();
2604
- process.exit(0);
2605
- }
2606
- }
2607
- ];
2608
- export async function executeCommand(input, ctx) {
2609
- const trimmed = input.trim();
2610
- if (!trimmed)
2611
- return false;
2612
- const parts = trimmed.split(/\s+/);
2613
- const commandName = parts[0].toLowerCase();
2614
- const args = trimmed.substring(parts[0].length).trim();
2615
- let mappedName = commandName;
2616
- if (commandName === '?' || commandName === 'help') {
2617
- mappedName = '/help';
2618
- }
2619
- const command = commandsList.find(c => c.name.toLowerCase() === mappedName ||
2620
- c.aliases?.some(alias => alias.toLowerCase() === mappedName));
2621
- if (command) {
2622
- if (command.name === '/tui') {
2623
- await command.execute(args, ctx);
2624
- return true;
2625
- }
2626
- try {
2627
- await command.execute(args, ctx);
2628
- }
2629
- catch (err) {
2630
- console.log(pc.red(`[ERROR] Command ${command.name} failed: ${err.message}`));
2631
- }
2632
- return true; // Handled
2633
- }
2634
- if (trimmed.startsWith('/')) {
2635
- console.log(pc.red(`[WARN] Unknown command: ${commandName}. Type /help or ? to view all available commands.`));
2636
- return true; // We treated it as a command, so don't pass it to the model
2637
- }
2638
- return false; // Not a command
2639
- }
1
+ export { executeCommand, commandsList } from './commands/index.js';
2640
2
  //# sourceMappingURL=commands.js.map