cmdr-agent 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/bin/cmdr.js +2 -1
  2. package/dist/bin/cmdr.js.map +1 -1
  3. package/dist/src/cli/args.js +1 -1
  4. package/dist/src/cli/commands.js +26 -0
  5. package/dist/src/cli/commands.js.map +1 -1
  6. package/dist/src/cli/ink/App.d.ts +40 -0
  7. package/dist/src/cli/ink/App.d.ts.map +1 -0
  8. package/dist/src/cli/ink/App.js +717 -0
  9. package/dist/src/cli/ink/App.js.map +1 -0
  10. package/dist/src/cli/repl.d.ts +4 -0
  11. package/dist/src/cli/repl.d.ts.map +1 -1
  12. package/dist/src/cli/repl.js +59 -532
  13. package/dist/src/cli/repl.js.map +1 -1
  14. package/dist/src/cli/theme.d.ts +1 -1
  15. package/dist/src/cli/theme.d.ts.map +1 -1
  16. package/dist/src/cli/theme.js +2 -2
  17. package/dist/src/cli/theme.js.map +1 -1
  18. package/dist/src/core/types.d.ts +6 -0
  19. package/dist/src/core/types.d.ts.map +1 -1
  20. package/dist/src/llm/model-registry.d.ts +5 -0
  21. package/dist/src/llm/model-registry.d.ts.map +1 -1
  22. package/dist/src/llm/model-registry.js +43 -0
  23. package/dist/src/llm/model-registry.js.map +1 -1
  24. package/dist/src/llm/ollama.d.ts.map +1 -1
  25. package/dist/src/llm/ollama.js +6 -0
  26. package/dist/src/llm/ollama.js.map +1 -1
  27. package/dist/src/session/prompt-builder.d.ts.map +1 -1
  28. package/dist/src/session/prompt-builder.js +9 -0
  29. package/dist/src/session/prompt-builder.js.map +1 -1
  30. package/dist/src/skills/injector.d.ts +19 -0
  31. package/dist/src/skills/injector.d.ts.map +1 -0
  32. package/dist/src/skills/injector.js +67 -0
  33. package/dist/src/skills/injector.js.map +1 -0
  34. package/dist/src/skills/loader.d.ts +31 -0
  35. package/dist/src/skills/loader.d.ts.map +1 -0
  36. package/dist/src/skills/loader.js +147 -0
  37. package/dist/src/skills/loader.js.map +1 -0
  38. package/package.json +6 -2
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Interactive REPL — the primary cmdr interface.
3
3
  *
4
+ * Uses Ink (React for CLI) for robust terminal management,
5
+ * matching the approach of Claude Code and Gemini CLI.
6
+ *
4
7
  * Streaming output, tool execution display, markdown rendering,
5
8
  * AMOLED black + green/purple aesthetic.
6
9
  */
7
- import * as readline from 'readline';
10
+ import React from 'react';
11
+ import { render } from 'ink';
8
12
  import { Agent } from '../core/agent.js';
9
13
  import { OllamaAdapter } from '../llm/ollama.js';
10
14
  import { ToolRegistry } from '../tools/registry.js';
@@ -14,9 +18,8 @@ import { Orchestrator } from '../core/orchestrator.js';
14
18
  import { SessionManager } from '../session/session-manager.js';
15
19
  import { discoverProject } from '../session/project-context.js';
16
20
  import { buildSystemPrompt } from '../session/prompt-builder.js';
17
- import { startThinking, startToolExec, stopSpinner, spinnerSuccess, spinnerFail, getCompletionSummary } from './spinner.js';
18
- import { renderWelcome, renderToolExec, renderError, PROMPT_SYMBOL, GREEN, PURPLE, DIM, WHITE, CYAN, renderInfo, GREEN_DIM, YELLOW, RED, SUCCESS_SYMBOL, ERROR_SYMBOL, } from './theme.js';
19
- import { isSlashCommand, parseSlashCommand, getCommand, } from './commands.js';
21
+ import { resolveContextLength } from '../llm/model-registry.js';
22
+ import { renderWelcome, renderError, GREEN, PURPLE, DIM, WHITE, renderInfo, YELLOW, RED, } from './theme.js';
20
23
  import { PermissionManager } from '../core/permissions.js';
21
24
  import { saveSession, loadSession, findRecentSession, DebouncedSaver } from '../session/session-persistence.js';
22
25
  import { PluginManager } from '../plugins/plugin-manager.js';
@@ -24,6 +27,8 @@ import { McpClient } from '../plugins/mcp-client.js';
24
27
  import { loadConfig } from '../config/config-loader.js';
25
28
  import { CostTracker } from '../session/cost-tracker.js';
26
29
  import { UndoManager } from '../session/undo-manager.js';
30
+ import { startThinking, stopSpinner, spinnerSuccess, spinnerFail, getCompletionSummary, startToolExec } from './spinner.js';
31
+ import App from './ink/App.js';
27
32
  export async function startRepl(options) {
28
33
  const cwd = process.cwd();
29
34
  const verbose = options.verbose ?? false;
@@ -42,8 +47,9 @@ export async function startRepl(options) {
42
47
  const projectInfo = projectContext.language !== 'unknown'
43
48
  ? `${projectContext.language}${projectContext.framework ? ' / ' + projectContext.framework : ''}`
44
49
  : cwd.split('/').pop() || 'unknown';
45
- // Session
46
- const session = new SessionManager(projectContext);
50
+ // Session — resolve model's actual context length
51
+ const modelContextLength = await resolveContextLength(options.model, options.ollamaUrl);
52
+ const session = new SessionManager(projectContext, modelContextLength);
47
53
  // Build system prompt with project context
48
54
  const systemPrompt = buildSystemPrompt({
49
55
  basePrompt: SOLO_CODER.systemPrompt,
@@ -86,7 +92,6 @@ export async function startRepl(options) {
86
92
  // Permission manager
87
93
  const permissionManager = new PermissionManager(options.dangerouslySkipPermissions ? 'yolo' : 'normal');
88
94
  await permissionManager.loadSettings();
89
- // CLI flag overrides persisted mode
90
95
  if (options.dangerouslySkipPermissions) {
91
96
  permissionManager.setMode('yolo');
92
97
  }
@@ -104,21 +109,20 @@ export async function startRepl(options) {
104
109
  }
105
110
  }
106
111
  // Create agent
107
- let currentModel = options.model;
112
+ const currentModel = options.model;
108
113
  const agent = new Agent({ ...SOLO_CODER, model: currentModel, systemPrompt }, adapter, toolRegistry, cwd, permissionManager);
109
- // --- Welcome ---
114
+ // --- Welcome banner (prints to normal terminal before Ink takes over) ---
110
115
  const modeLabel = permissionManager.getMode() === 'yolo'
111
116
  ? YELLOW('⚠ yolo (all tools auto-approved)')
112
117
  : permissionManager.getMode() === 'strict'
113
118
  ? RED('strict (all tools require approval)')
114
119
  : GREEN('normal (write tools require approval)');
115
- console.log(renderWelcome(currentModel, projectInfo));
120
+ console.log(renderWelcome(currentModel, projectInfo, options.version));
116
121
  console.log(` ${DIM('Permissions:')} ${modeLabel}`);
117
122
  if (activeTeamConfig) {
118
123
  const teamAgents = activeTeamConfig.agents.map(a => a.name).join(', ');
119
124
  console.log(` ${DIM('Team:')} ${PURPLE(activeTeamConfig.name)} ${DIM(`(${teamAgents})`)}`);
120
125
  }
121
- // Show CMDR.md loading status
122
126
  if (projectContext.cmdrInstructions) {
123
127
  const lineCount = projectContext.cmdrInstructions.split('\n').length;
124
128
  console.log(` ${DIM(`CMDR.md loaded (${lineCount} lines)`)}`);
@@ -155,438 +159,49 @@ export async function startRepl(options) {
155
159
  }
156
160
  };
157
161
  console.log('');
158
- // --- Handle initial prompt if provided ---
162
+ // --- Handle one-shot prompt (non-interactive) ---
159
163
  if (options.initialPrompt) {
160
- await handleUserMessage(options.initialPrompt, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager);
164
+ await handleOneShot(options.initialPrompt, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager);
161
165
  await doSave();
162
166
  return;
163
167
  }
164
- // --- Interactive REPL ---
165
- const rl = readline.createInterface({
166
- input: process.stdin,
167
- output: process.stdout,
168
- prompt: PROMPT_SYMBOL,
169
- terminal: true,
170
- });
171
- rl.prompt();
172
- // --- Paste detection ---
173
- // Buffer lines arriving within 50ms of each other and join as single input
174
- let pasteBuffer = [];
175
- let pasteTimer = null;
176
- const PASTE_THRESHOLD_MS = 50;
177
- // Async queue — ensures commands are processed sequentially
178
- let processing = false;
179
- let closed = false;
180
- async function processInput(input) {
181
- processing = true;
182
- try {
183
- await processLine(input);
184
- }
185
- catch (err) {
186
- const msg = err instanceof Error ? err.message : String(err);
187
- console.error(renderError(msg));
188
- }
189
- finally {
190
- processing = false;
191
- if (closed) {
192
- process.exit(0);
193
- }
194
- else {
195
- rl.prompt();
196
- }
197
- }
198
- }
199
- function flushPasteBuffer() {
200
- pasteTimer = null;
201
- if (pasteBuffer.length === 0)
202
- return;
203
- const lines = pasteBuffer.slice();
204
- pasteBuffer = [];
205
- if (lines.length > 1) {
206
- console.log(` ${DIM(`+${lines.length} lines pasted`)}`);
207
- }
208
- const merged = lines.join('\n').trim();
209
- if (!merged) {
210
- rl.prompt();
211
- return;
212
- }
213
- processInput(merged);
214
- }
215
- async function processLine(input) {
216
- // Slash commands
217
- if (isSlashCommand(input)) {
218
- const { name, args } = parseSlashCommand(input);
219
- const cmd = getCommand(name);
220
- if (!cmd) {
221
- console.log(renderError(`Unknown command: /${name}. Type /help for available commands.`));
222
- return;
223
- }
224
- const result = await cmd.execute(args, {
225
- session: session.getState(),
226
- switchModel: (model) => {
227
- currentModel = model;
228
- },
229
- clearHistory: () => {
230
- session.clear();
231
- agent.reset();
232
- permissionManager.resetSession();
233
- },
234
- ollamaUrl: options.ollamaUrl,
235
- adapter,
236
- model: currentModel,
237
- agentTokenUsage: agent.getState().tokenUsage,
238
- permissionManager,
239
- });
240
- if (result === '__QUIT__') {
241
- autoSaver.cancel();
242
- session.syncFromAgent(agent.getHistory());
243
- if (session.messages.length > 0) {
244
- const sid = await saveSession(session.getState(), currentModel);
245
- console.log(`\n ${DIM('Session saved:')} ${DIM(sid)}`);
246
- }
247
- console.log(`\n ${PURPLE('Goodbye.')} ${DIM('Session ended.')}\n`);
248
- closed = true;
249
- rl.close();
250
- return;
251
- }
252
- if (result === '__COMPACT__') {
253
- session.syncFromAgent(agent.getHistory());
254
- const beforeTokens = session.tokenCount;
255
- const stats = await session.compact(adapter, currentModel);
256
- agent.replaceMessages(session.messages);
257
- const afterTokens = session.tokenCount;
258
- console.log(renderInfo(`${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`));
259
- return;
260
- }
261
- if (result === '__DIFF__') {
262
- const gitTool = toolRegistry.get('git_diff');
263
- if (gitTool) {
264
- const diffResult = await gitTool.execute({ staged: false }, {
265
- agent: { name: 'cmdr', role: 'assistant', model: currentModel },
266
- cwd,
267
- });
268
- console.log(`\n${WHITE(diffResult.data)}\n`);
269
- }
270
- return;
271
- }
272
- if (result === '__SESSION_SAVE__') {
273
- session.syncFromAgent(agent.getHistory());
274
- if (session.messages.length > 0) {
275
- const sid = await saveSession(session.getState(), currentModel);
276
- console.log(renderInfo(`Session saved: ${DIM(sid)}`));
277
- }
278
- else {
279
- console.log(renderInfo('No messages to save.'));
280
- }
281
- return;
282
- }
283
- if (typeof result === 'string' && result.startsWith('__SESSION_RESUME__:')) {
284
- const sessionId = result.slice('__SESSION_RESUME__:'.length);
285
- const saved = await loadSession(sessionId);
286
- if (saved) {
287
- agent.replaceMessages(saved.messages);
288
- session.syncFromAgent(saved.messages);
289
- console.log(renderInfo(`Resumed session ${DIM(saved.id)} (${saved.messages.length} messages)`));
290
- }
291
- else {
292
- console.log(renderError(`Session not found: ${sessionId}`));
293
- }
294
- return;
295
- }
296
- if (typeof result === 'string' && result.startsWith('__TEAM_SWITCH__:')) {
297
- const preset = result.slice('__TEAM_SWITCH__:'.length);
298
- const teamCfg = getTeamPreset(preset);
299
- if (teamCfg) {
300
- activeTeamConfig = teamCfg;
301
- const teamAgents = teamCfg.agents.map(a => a.name).join(', ');
302
- console.log(renderInfo(`Switched to team: ${PURPLE(teamCfg.name)} (${teamAgents})`));
303
- }
304
- else {
305
- console.log(renderError(`Unknown team: ${preset}. Use: review, fullstack, security`));
306
- }
307
- return;
308
- }
309
- if (result === '__AGENTS_STATUS__') {
310
- if (!activeTeamConfig) {
311
- console.log(renderInfo(`Solo mode (agent: ${GREEN('cmdr')}). Use /team <preset> for multi-agent.`));
312
- }
313
- else {
314
- const status = orchestrator.getStatus();
315
- const lines = ['', ` ${PURPLE.bold(`Team: ${activeTeamConfig.name}`)}`, ''];
316
- for (const agentCfg of activeTeamConfig.agents) {
317
- const agentStatus = status?.agents.find(a => a.name === agentCfg.name);
318
- const statusLabel = agentStatus ? DIM(agentStatus.status) : DIM('idle');
319
- lines.push(` ${GREEN('•')} ${WHITE(agentCfg.name.padEnd(12))} ${statusLabel}`);
320
- }
321
- lines.push('');
322
- console.log(lines.join('\n'));
323
- }
324
- return;
325
- }
326
- if (result === '__TASKS_STATUS__') {
327
- const status = orchestrator.getStatus();
328
- if (!status) {
329
- console.log(renderInfo('No active team or tasks.'));
330
- }
331
- else {
332
- const s = status.tasks;
333
- if (s) {
334
- console.log(renderInfo(`Tasks: ${GREEN(`${s.completed} done`)} · ${YELLOW(`${s.in_progress} running`)} · ${DIM(`${s.pending} pending`)} · ${s.failed > 0 ? RED(`${s.failed} failed`) : DIM('0 failed')}`));
335
- }
336
- }
337
- return;
338
- }
339
- if (result === '__COST__') {
340
- const summary = costTracker.getSummary();
341
- const elapsed = costTracker.formatElapsed();
342
- const lines = [
343
- '',
344
- ` ${PURPLE.bold('Token usage')}`,
345
- '',
346
- ` ${DIM('Model:')} ${WHITE(summary.model)}`,
347
- ` ${DIM('Turns:')} ${WHITE(String(summary.turns))}`,
348
- ` ${DIM('Input tokens:')} ${WHITE(String(summary.totalInputTokens))}`,
349
- ` ${DIM('Output tokens:')} ${WHITE(String(summary.totalOutputTokens))}`,
350
- ` ${DIM('Total tokens:')} ${GREEN(String(summary.totalTokens))}`,
351
- ` ${DIM('Tool calls:')} ${WHITE(String(summary.totalToolCalls))}`,
352
- ` ${DIM('Avg/turn:')} ${WHITE(String(summary.avgTokensPerTurn))}`,
353
- ` ${DIM('Session time:')} ${WHITE(elapsed)}`,
354
- '',
355
- ];
356
- console.log(lines.join('\n'));
357
- return;
358
- }
359
- if (result === '__UNDO__') {
360
- if (undoManager.count === 0) {
361
- console.log(renderInfo('Nothing to undo.'));
362
- }
363
- else {
364
- const change = await undoManager.undoLast();
365
- if (change) {
366
- const action = change.originalContent === null ? 'deleted' : 'restored';
367
- const fname = change.path.split('/').pop() ?? change.path;
368
- console.log(renderInfo(`Undid ${change.type} on ${GREEN(fname)} (${action})`));
369
- }
370
- }
371
- return;
372
- }
373
- if (typeof result === 'string' && result.startsWith('__PLUGIN__:')) {
374
- const sub = result.slice('__PLUGIN__:'.length).trim();
375
- if (sub === 'list' || !sub) {
376
- const plugins = pluginManager.list();
377
- if (plugins.length === 0) {
378
- console.log(renderInfo('No plugins loaded. Add plugins to ~/.cmdr/config.toml'));
379
- }
380
- else {
381
- const lines = ['', ` ${PURPLE.bold('Loaded plugins')}`, ''];
382
- for (const p of plugins) {
383
- const hooks = p.hooks ? Object.keys(p.hooks).length : 0;
384
- const tools = p.tools?.length ?? 0;
385
- console.log(` ${GREEN('•')} ${WHITE(p.name)} v${p.version} ${DIM(`(${hooks} hooks, ${tools} tools)`)}`);
386
- }
387
- lines.push('');
388
- console.log(lines.join('\n'));
389
- }
390
- }
391
- return;
392
- }
393
- if (typeof result === 'string' && result.startsWith('__MCP__:')) {
394
- const sub = result.slice('__MCP__:'.length).trim().split(/\s+/);
395
- const action = sub[0];
396
- if (action === 'list' || !action) {
397
- const conns = mcpClient.listConnections();
398
- if (conns.length === 0) {
399
- console.log(renderInfo('No MCP servers connected. Add to ~/.cmdr/config.toml or use /mcp connect <name> <url>'));
400
- }
401
- else {
402
- const lines = ['', ` ${PURPLE.bold('MCP servers')}`, ''];
403
- for (const c of conns) {
404
- const status = c.connected ? GREEN('connected') : RED('disconnected');
405
- lines.push(` ${GREEN('•')} ${WHITE(c.name)} ${DIM(c.url)} ${status} ${DIM(`(${c.tools} tools)`)}`);
406
- }
407
- lines.push('');
408
- console.log(lines.join('\n'));
409
- }
410
- }
411
- else if (action === 'connect') {
412
- const name = sub[1];
413
- const url = sub[2];
414
- if (!name || !url) {
415
- console.log(renderInfo('Usage: /mcp connect <name> <url>'));
416
- }
417
- else {
418
- try {
419
- const tools = await mcpClient.connect({ name, url });
420
- mcpClient.registerTools(toolRegistry);
421
- console.log(renderInfo(`Connected to ${name}: ${tools.length} tools discovered`));
422
- }
423
- catch (err) {
424
- const msg = err instanceof Error ? err.message : String(err);
425
- console.log(renderError(msg));
426
- }
427
- }
428
- }
429
- else if (action === 'disconnect') {
430
- const name = sub[1];
431
- if (name && mcpClient.disconnect(name)) {
432
- console.log(renderInfo(`Disconnected from ${name}`));
433
- }
434
- else {
435
- console.log(renderError(`MCP server "${name}" not found`));
436
- }
437
- }
438
- return;
439
- }
440
- if (result)
441
- console.log(result);
442
- return;
443
- }
444
- // Regular message — team mode or solo mode
445
- if (activeTeamConfig) {
446
- await handleTeamMessage(input, orchestrator, activeTeamConfig, currentModel, verbose);
447
- }
448
- else {
449
- await handleUserMessage(input, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager, () => {
450
- autoSaver.schedule(doSave);
451
- });
452
- }
453
- }
454
- rl.on('line', (line) => {
455
- pasteBuffer.push(line);
456
- if (pasteTimer)
457
- clearTimeout(pasteTimer);
458
- pasteTimer = setTimeout(flushPasteBuffer, PASTE_THRESHOLD_MS);
168
+ // --- Interactive REPL via Ink ---
169
+ const app = render(React.createElement(App, {
170
+ agent,
171
+ session,
172
+ model: currentModel,
173
+ permissionManager,
174
+ adapter,
175
+ orchestrator,
176
+ activeTeamConfig,
177
+ costTracker,
178
+ undoManager,
179
+ pluginManager,
180
+ mcpClient,
181
+ toolRegistry,
182
+ ollamaUrl: options.ollamaUrl,
183
+ verbose,
184
+ doSave,
185
+ autoSaver,
186
+ }), {
187
+ exitOnCtrlC: false, // We handle Ctrl+C ourselves
188
+ patchConsole: false, // Ban console logs from being intercepted during banner
459
189
  });
460
- rl.on('close', async () => {
461
- autoSaver.cancel();
462
- if (!closed) {
463
- session.syncFromAgent(agent.getHistory());
464
- if (session.messages.length > 0) {
465
- try {
466
- const sid = await saveSession(session.getState(), currentModel);
467
- console.log(`\n ${DIM('Session saved:')} ${DIM(sid)}`);
468
- }
469
- catch {
470
- // best effort
471
- }
472
- }
473
- console.log(`\n ${PURPLE('Goodbye.')} ${DIM('Session ended.')}\n`);
474
- }
475
- if (processing) {
476
- closed = true;
477
- }
478
- else {
479
- process.exit(0);
480
- }
481
- });
482
- }
483
- // ---------------------------------------------------------------------------
484
- // Approval prompt — asks the user to approve a tool call
485
- // ---------------------------------------------------------------------------
486
- function promptApproval(toolName, input, riskLevel) {
487
- return new Promise((resolve) => {
488
- const riskColor = riskLevel === 'dangerous' ? RED : YELLOW;
489
- const riskLabel = riskColor(riskLevel.toUpperCase());
490
- // Show the tool call details
491
- console.log('');
492
- console.log(` ${YELLOW('⚠')} ${WHITE('Tool approval required')} ${DIM('[')}${riskLabel}${DIM(']')}`);
493
- console.log(` ${DIM('Tool:')} ${CYAN(toolName)}`);
494
- // Show a summary of key arguments
495
- for (const [key, val] of Object.entries(input)) {
496
- const display = typeof val === 'string'
497
- ? val.length > 120 ? val.slice(0, 120) + DIM('...') : val
498
- : JSON.stringify(val).slice(0, 120);
499
- console.log(` ${DIM(key + ':')} ${WHITE(display)}`);
500
- }
501
- console.log('');
502
- console.log(` ${GREEN('y')}${DIM('es')} ${DIM('/')} ${RED('n')}${DIM('o')} ${DIM('/')} ${PURPLE('a')}${DIM('lways allow this tool')}`);
503
- // Create a one-shot readline for the approval prompt
504
- const approvalRl = readline.createInterface({
505
- input: process.stdin,
506
- output: process.stdout,
507
- terminal: true,
508
- });
509
- approvalRl.question(` ${YELLOW('?')} `, (answer) => {
510
- approvalRl.close();
511
- const trimmed = answer.trim().toLowerCase();
512
- if (trimmed === 'y' || trimmed === 'yes' || trimmed === '') {
513
- resolve('allow');
514
- }
515
- else if (trimmed === 'a' || trimmed === 'always') {
516
- resolve('allow-always');
517
- }
518
- else {
519
- resolve('deny');
520
- }
521
- });
522
- });
523
- }
524
- // ---------------------------------------------------------------------------
525
- // Tool result summary — collapsed single-line display
526
- // ---------------------------------------------------------------------------
527
- function summarizeToolResult(toolName, input, content, isError) {
528
- const lineCount = content.split('\n').length;
529
- const prefix = isError ? ERROR_SYMBOL : SUCCESS_SYMBOL;
530
- let summary;
531
- switch (toolName) {
532
- case 'file_read': {
533
- const file = input.path ?? 'unknown';
534
- const fname = file.split('/').pop() ?? file;
535
- summary = `${fname} (${lineCount} lines)`;
536
- break;
537
- }
538
- case 'glob': {
539
- const pattern = input.pattern ?? '*';
540
- const matches = content === '(no matches)' ? 0 : lineCount;
541
- summary = `${pattern} (${matches} matches)`;
542
- break;
543
- }
544
- case 'bash': {
545
- const cmd = input.command ?? '';
546
- const truncCmd = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
547
- // Extract exit code from result if present
548
- const exitMatch = content.match(/\[exit code: (\d+)\]/);
549
- const exitCode = exitMatch ? exitMatch[1] : '0';
550
- summary = `\`${truncCmd}\` exit=${exitCode} (${lineCount} lines)`;
551
- break;
552
- }
553
- case 'grep': {
554
- const pattern = input.pattern ?? '';
555
- const matches = content === '(no matches)' ? 0 : lineCount;
556
- summary = `/${pattern}/ (${matches} matches)`;
557
- break;
558
- }
559
- case 'think': {
560
- const thought = input.thought ?? '';
561
- const preview = thought.length > 60 ? thought.slice(0, 57) + '...' : thought;
562
- summary = preview;
563
- break;
564
- }
565
- default: {
566
- const bytes = Buffer.byteLength(content, 'utf-8');
567
- summary = `${bytes > 1024 ? Math.round(bytes / 1024) + ' KB' : bytes + ' B'}`;
568
- break;
569
- }
570
- }
571
- return ` ${prefix} ${DIM(toolName)} ${DIM(summary)}`;
190
+ await app.waitUntilExit();
572
191
  }
573
192
  // ---------------------------------------------------------------------------
574
- // Message handler streaming output with tool execution display
193
+ // One-shot handler (non-interactive --prompt mode)
575
194
  // ---------------------------------------------------------------------------
576
- async function handleUserMessage(message, agent, session, model, permissionManager, verbose, adapter, costTracker, undoManager, onAfterResponse) {
577
- console.log(''); // spacing
195
+ async function handleOneShot(message, agent, session, model, permissionManager, verbose, adapter, costTracker, undoManager) {
196
+ console.log('');
578
197
  startThinking();
579
198
  let fullOutput = '';
580
199
  let firstText = true;
581
200
  let currentTool = '';
582
201
  let currentToolInput = {};
583
202
  let toolCallCount = 0;
584
- // Build callbacks with the approval gate
585
- const callbacks = {
586
- onToolApproval: (toolName, input, riskLevel) => promptApproval(toolName, input, riskLevel),
587
- };
588
203
  try {
589
- for await (const event of agent.stream(message, callbacks)) {
204
+ for await (const event of agent.stream(message)) {
590
205
  switch (event.type) {
591
206
  case 'text': {
592
207
  if (firstText) {
@@ -596,8 +211,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
596
211
  }
597
212
  const chunk = event.data;
598
213
  fullOutput += chunk;
599
- // Stream raw text token-by-token (no markdown on partial chunks)
600
- // Handle newlines by adding the prefix
601
214
  const formatted = chunk.replace(/\n/g, `\n ${PURPLE('│')} `);
602
215
  process.stdout.write(formatted);
603
216
  break;
@@ -605,7 +218,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
605
218
  case 'tool_use': {
606
219
  stopSpinner();
607
220
  if (!firstText) {
608
- // Terminate previous text stream line
609
221
  process.stdout.write('\n');
610
222
  firstText = true;
611
223
  }
@@ -613,46 +225,36 @@ async function handleUserMessage(message, agent, session, model, permissionManag
613
225
  currentTool = block.name;
614
226
  currentToolInput = block.input;
615
227
  toolCallCount++;
616
- // Record file state for undo before write/edit tools
617
- if (undoManager && (block.name === 'file_write' || block.name === 'file_edit')) {
228
+ if (block.name === 'file_write' || block.name === 'file_edit') {
618
229
  const filePath = (block.input.path ?? block.input.file_path);
619
- if (filePath) {
230
+ if (filePath)
620
231
  await undoManager.recordBefore(filePath, block.name === 'file_write' ? 'write' : 'edit');
621
- }
622
232
  }
623
- console.log(renderToolExec(block.name, block.input));
233
+ const toolSummary = Object.entries(block.input)
234
+ .map(([k, v]) => {
235
+ const val = typeof v === 'string' ? v.slice(0, 80) : JSON.stringify(v).slice(0, 80);
236
+ return `${DIM(k + ':')} ${WHITE(val)}`;
237
+ })
238
+ .join(' ');
239
+ console.log(` ${GREEN('⚡')} ${GREEN.bold(block.name)} ${toolSummary}`);
624
240
  startToolExec(block.name);
625
241
  break;
626
242
  }
627
243
  case 'tool_result': {
628
244
  const block = event.data;
629
- if (block.is_error) {
245
+ if (block.is_error)
630
246
  spinnerFail(currentTool);
631
- }
632
- else {
247
+ else
633
248
  spinnerSuccess(currentTool);
634
- }
635
- if (verbose) {
636
- // Full output in verbose mode
637
- const truncated = block.content.length > 2000
638
- ? block.content.slice(0, 2000) + DIM('\n... (truncated)')
639
- : block.content;
640
- const prefix = block.is_error ? ERROR_SYMBOL : SUCCESS_SYMBOL;
641
- console.log(` ${prefix} ${DIM(currentTool + ':')} ${block.is_error ? RED(truncated) : DIM(truncated)}`);
642
- }
643
- else {
644
- console.log(summarizeToolResult(currentTool, currentToolInput, block.content, block.is_error));
645
- }
646
249
  currentTool = '';
647
250
  currentToolInput = {};
648
251
  startThinking();
649
252
  firstText = true;
650
253
  break;
651
254
  }
652
- case 'done': {
255
+ case 'done':
653
256
  stopSpinner();
654
257
  break;
655
- }
656
258
  case 'error': {
657
259
  stopSpinner();
658
260
  const err = event.data;
@@ -665,22 +267,12 @@ async function handleUserMessage(message, agent, session, model, permissionManag
665
267
  catch (err) {
666
268
  stopSpinner();
667
269
  const msg = err instanceof Error ? err.message : String(err);
668
- if (msg.includes('not found') || msg.includes('404') || (msg.includes('model') && msg.includes('pull'))) {
669
- console.error(renderError(`Model '${model}' not found. Run ${GREEN('/models')} to see available models or ${GREEN('/model <name>')} to switch.`));
670
- }
671
- else {
672
- console.error(renderError(msg));
673
- }
270
+ console.error(renderError(msg));
674
271
  }
675
- // Ensure we end the text block with a newline
676
- if (!firstText) {
272
+ if (!firstText)
677
273
  process.stdout.write('\n');
678
- }
679
- // Add spacing after response
680
- if (fullOutput) {
274
+ if (fullOutput)
681
275
  console.log('');
682
- }
683
- // Show turn summary with whimsical verb
684
276
  const state = agent.getState();
685
277
  const tokens = state.tokenUsage;
686
278
  const summary = getCompletionSummary();
@@ -688,72 +280,7 @@ async function handleUserMessage(message, agent, session, model, permissionManag
688
280
  ? ` ${DIM('·')} ${DIM(`${tokens.input_tokens} in / ${tokens.output_tokens} out`)}`
689
281
  : '';
690
282
  console.log(` ${DIM(summary)}${tokenInfo}`);
691
- // Record cost data
692
- costTracker?.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
693
- // Sync agent messages into session for compaction tracking
283
+ costTracker.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
694
284
  session.syncFromAgent(agent.getHistory());
695
- // Auto-compact if context is getting full
696
- if (session.shouldCompact()) {
697
- try {
698
- const stats = await session.compact(adapter, model);
699
- agent.replaceMessages(session.messages);
700
- console.log(` ${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`);
701
- }
702
- catch {
703
- // best effort — don't break the REPL
704
- }
705
- }
706
- // Trigger debounced auto-save
707
- onAfterResponse?.();
708
- console.log(GREEN_DIM('─'.repeat(60)));
709
- console.log('');
710
- }
711
- // ---------------------------------------------------------------------------
712
- // Team message handler — runs goal through the orchestrator
713
- // ---------------------------------------------------------------------------
714
- async function handleTeamMessage(goal, orchestrator, teamConfig, model, verbose) {
715
- console.log('');
716
- console.log(` ${PURPLE('◈')} Running team ${PURPLE.bold(teamConfig.name)} with ${teamConfig.agents.length} agents...`);
717
- console.log('');
718
- startThinking();
719
- try {
720
- const result = await orchestrator.runTeam(teamConfig, goal);
721
- stopSpinner();
722
- // Display results from each agent
723
- for (const [agentName, agentResult] of result.agentResults) {
724
- const status = agentResult.success ? GREEN('✓') : RED('✗');
725
- console.log(` ${status} ${CYAN(agentName)}`);
726
- if (agentResult.output) {
727
- const lines = agentResult.output.split('\n');
728
- const displayLines = verbose ? lines : lines.slice(0, 20);
729
- for (const line of displayLines) {
730
- console.log(` ${PURPLE('│')} ${line}`);
731
- }
732
- if (!verbose && lines.length > 20) {
733
- console.log(` ${PURPLE('│')} ${DIM(`... ${lines.length - 20} more lines (use --verbose)`)}`);
734
- }
735
- console.log('');
736
- }
737
- // Tool call summary
738
- if (agentResult.toolCalls.length > 0) {
739
- const tools = agentResult.toolCalls.map(t => t.toolName);
740
- const unique = [...new Set(tools)];
741
- console.log(` ${DIM(` tools: ${unique.join(', ')} (${tools.length} calls)`)}`);
742
- }
743
- }
744
- // Summary
745
- const usage = result.totalTokenUsage;
746
- const summary = getCompletionSummary();
747
- const tokenInfo = `${usage.input_tokens} in / ${usage.output_tokens} out`;
748
- console.log(` ${DIM(summary)} ${DIM('·')} ${DIM(tokenInfo)}`);
749
- console.log(` ${result.success ? GREEN('Team completed successfully') : RED('Team had failures')}`);
750
- }
751
- catch (err) {
752
- stopSpinner();
753
- const msg = err instanceof Error ? err.message : String(err);
754
- console.error(renderError(msg));
755
- }
756
- console.log(GREEN_DIM('─'.repeat(60)));
757
- console.log('');
758
285
  }
759
286
  //# sourceMappingURL=repl.js.map