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
@@ -0,0 +1,717 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Ink-based REPL application — replaces raw readline.
4
+ *
5
+ * Architecture:
6
+ * - <Static> renders all past output (scrollback, never re-rendered)
7
+ * - Dynamic section: active spinner OR input prompt
8
+ * - State machine: idle | processing | waiting_approval | exiting
9
+ */
10
+ import { useState, useCallback, useEffect, useRef } from 'react';
11
+ import { Box, Text, Static, useApp, useInput } from 'ink';
12
+ import TextInput from 'ink-text-input';
13
+ import { isSlashCommand, parseSlashCommand, getCommand } from '../commands.js';
14
+ import { saveSession, loadSession, } from '../../session/session-persistence.js';
15
+ import { getTeamPreset } from '../../core/presets.js';
16
+ // We use chalk directly for coloring since Ink <Text> color props are limited
17
+ import chalk from 'chalk';
18
+ const GREEN = chalk.hex('#00FF41');
19
+ const GREEN_DIM = chalk.hex('#00BB30');
20
+ const PURPLE = chalk.hex('#BF40FF');
21
+ const CYAN = chalk.hex('#00FFFF');
22
+ const DIM = chalk.hex('#555555');
23
+ const WHITE = chalk.hex('#E0E0E0');
24
+ const YELLOW = chalk.hex('#FFD700');
25
+ const RED = chalk.hex('#FF3333');
26
+ const SUCCESS_SYM = GREEN('✓');
27
+ const ERROR_SYM = RED('✗');
28
+ const TOOL_SYM = CYAN('⚡');
29
+ const SEPARATOR = GREEN_DIM('─'.repeat(60));
30
+ // ---------------------------------------------------------------------------
31
+ // Verb pool for spinner
32
+ // ---------------------------------------------------------------------------
33
+ const VERBS = [
34
+ 'Computing', 'Architecting', 'Bootstrapping', 'Compiling', 'Debugging',
35
+ 'Refactoring', 'Profiling', 'Indexing', 'Optimizing', 'Parsing',
36
+ 'Noodling', 'Percolating', 'Combobulating', 'Bamboozling', 'Cogitating',
37
+ 'Ruminating', 'Pondering', 'Brainstorming', 'Concocting', 'Devising',
38
+ 'Simmering', 'Marinating', 'Fermenting', 'Whisking', 'Reducing',
39
+ 'Moonwalking', 'Pirouetting', 'Sashaying', 'Waltzing', 'Commanding',
40
+ 'Strategizing', 'Maneuvering', 'Rallying', 'Scouting', 'Dispatching',
41
+ 'Crystallizing', 'Metamorphosing', 'Germinating', 'Blooming', 'Coalescing',
42
+ 'Weaving', 'Sculpting', 'Tinkering', 'Trailblazing', 'Questing',
43
+ ];
44
+ function pickVerb() {
45
+ return VERBS[Math.floor(Math.random() * VERBS.length)];
46
+ }
47
+ function toPastTense(verb) {
48
+ if (!verb.endsWith('ing'))
49
+ return verb;
50
+ const stem = verb.slice(0, -3);
51
+ const last = stem[stem.length - 1];
52
+ if (last && 'bcdfghjklmnpqrstvwxyz'.includes(last.toLowerCase())) {
53
+ return stem + 'ed';
54
+ }
55
+ return stem + 'ed';
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Tool result summary
59
+ // ---------------------------------------------------------------------------
60
+ function summarizeToolResult(toolName, input, content, isError) {
61
+ const lineCount = content.split('\n').length;
62
+ const prefix = isError ? ERROR_SYM : SUCCESS_SYM;
63
+ let summary;
64
+ switch (toolName) {
65
+ case 'file_read': {
66
+ const file = input.path ?? 'unknown';
67
+ const fname = file.split('/').pop() ?? file;
68
+ summary = `${fname} (${lineCount} lines)`;
69
+ break;
70
+ }
71
+ case 'glob': {
72
+ const pattern = input.pattern ?? '*';
73
+ const matches = content === '(no matches)' ? 0 : lineCount;
74
+ summary = `${pattern} (${matches} matches)`;
75
+ break;
76
+ }
77
+ case 'bash': {
78
+ const cmd = input.command ?? '';
79
+ const truncCmd = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
80
+ const exitMatch = content.match(/\[exit code: (\d+)\]/);
81
+ const exitCode = exitMatch ? exitMatch[1] : '0';
82
+ summary = `\`${truncCmd}\` exit=${exitCode} (${lineCount} lines)`;
83
+ break;
84
+ }
85
+ case 'grep': {
86
+ const pattern = input.pattern ?? '';
87
+ const matches = content === '(no matches)' ? 0 : lineCount;
88
+ summary = `/${pattern}/ (${matches} matches)`;
89
+ break;
90
+ }
91
+ case 'think': {
92
+ const thought = input.thought ?? '';
93
+ const preview = thought.length > 60 ? thought.slice(0, 57) + '...' : thought;
94
+ summary = preview;
95
+ break;
96
+ }
97
+ default: {
98
+ const bytes = Buffer.byteLength(content, 'utf-8');
99
+ summary = `${bytes > 1024 ? Math.round(bytes / 1024) + ' KB' : bytes + ' B'}`;
100
+ break;
101
+ }
102
+ }
103
+ return ` ${prefix} ${DIM(toolName)} ${DIM(summary)}`;
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Main App Component
107
+ // ---------------------------------------------------------------------------
108
+ let lineCounter = 0;
109
+ function nextId() {
110
+ return `line-${++lineCounter}`;
111
+ }
112
+ export default function App(props) {
113
+ const { agent, session, permissionManager, adapter, orchestrator, costTracker, undoManager, pluginManager, mcpClient, toolRegistry, ollamaUrl, verbose, doSave, autoSaver, } = props;
114
+ const { exit } = useApp();
115
+ const [state, setState] = useState('idle');
116
+ const [inputValue, setInputValue] = useState('');
117
+ const [outputLines, setOutputLines] = useState([]);
118
+ const [spinnerText, setSpinnerText] = useState('');
119
+ const [approval, setApproval] = useState(null);
120
+ const [approvalInput, setApprovalInput] = useState('');
121
+ const currentModelRef = useRef(props.model);
122
+ const activeTeamRef = useRef(props.activeTeamConfig);
123
+ const stateRef = useRef(state);
124
+ const lastSigintRef = useRef(0);
125
+ // Keep stateRef in sync
126
+ useEffect(() => {
127
+ stateRef.current = state;
128
+ }, [state]);
129
+ // Append output to scrollback
130
+ const appendOutput = useCallback((text) => {
131
+ setOutputLines(prev => [...prev, { id: nextId(), text }]);
132
+ }, []);
133
+ // Append multiple lines at once
134
+ const appendLines = useCallback((lines) => {
135
+ setOutputLines(prev => [
136
+ ...prev,
137
+ ...lines.map(text => ({ id: nextId(), text })),
138
+ ]);
139
+ }, []);
140
+ // ---------------------------------------------------------------------------
141
+ // Spinner management
142
+ // ---------------------------------------------------------------------------
143
+ const spinnerRef = useRef(null);
144
+ const verbRef = useRef(pickVerb());
145
+ const spinnerStartRef = useRef(0);
146
+ const spinnerFrameRef = useRef(0);
147
+ const SPINNER_FRAMES = ['◇ ', '◈ ', '◆ ', '◈ '];
148
+ const startSpinner = useCallback((mode, toolName) => {
149
+ stopSpinnerFn();
150
+ spinnerStartRef.current = Date.now();
151
+ verbRef.current = pickVerb();
152
+ spinnerFrameRef.current = 0;
153
+ const update = () => {
154
+ spinnerFrameRef.current = (spinnerFrameRef.current + 1) % SPINNER_FRAMES.length;
155
+ const frame = SPINNER_FRAMES[spinnerFrameRef.current];
156
+ const elapsed = Math.round((Date.now() - spinnerStartRef.current) / 1000);
157
+ if (mode === 'thinking') {
158
+ // Rotate verb every ~3s
159
+ if (elapsed > 0 && elapsed % 3 === 0) {
160
+ verbRef.current = pickVerb();
161
+ }
162
+ setSpinnerText(` ${PURPLE(frame)}${PURPLE(verbRef.current + '...')} ${DIM(`(${elapsed}s)`)}`);
163
+ }
164
+ else {
165
+ setSpinnerText(` ${CYAN('⚡')} ${CYAN(toolName ?? 'tool')} ${DIM('executing...')}`);
166
+ }
167
+ };
168
+ update();
169
+ spinnerRef.current = setInterval(update, 120);
170
+ }, []);
171
+ const stopSpinnerFn = useCallback(() => {
172
+ if (spinnerRef.current) {
173
+ clearInterval(spinnerRef.current);
174
+ spinnerRef.current = null;
175
+ }
176
+ setSpinnerText('');
177
+ }, []);
178
+ const getCompletionSummary = useCallback(() => {
179
+ const elapsed = Math.round((Date.now() - spinnerStartRef.current) / 1000);
180
+ return `${toPastTense(verbRef.current)} for ${elapsed}s`;
181
+ }, []);
182
+ // ---------------------------------------------------------------------------
183
+ // Cleanup & Exit
184
+ // ---------------------------------------------------------------------------
185
+ const cleanupAndExit = useCallback(async () => {
186
+ if (stateRef.current === 'exiting')
187
+ return;
188
+ setState('exiting');
189
+ stopSpinnerFn();
190
+ autoSaver.cancel();
191
+ session.syncFromAgent(agent.getHistory());
192
+ if (session.messages.length > 0) {
193
+ try {
194
+ const sid = await saveSession(session.getState(), currentModelRef.current);
195
+ appendOutput(`\n ${DIM('Session saved:')} ${DIM(sid)}`);
196
+ }
197
+ catch {
198
+ // best effort
199
+ }
200
+ }
201
+ appendOutput(`\n ${PURPLE('Goodbye.')} ${DIM('Session ended.')}\n`);
202
+ // Give Ink a moment to render the final output
203
+ setTimeout(() => {
204
+ exit();
205
+ process.exit(0);
206
+ }, 100);
207
+ }, [agent, session, autoSaver, stopSpinnerFn, appendOutput, exit]);
208
+ // Signal handlers
209
+ useEffect(() => {
210
+ const onSigterm = () => { cleanupAndExit(); };
211
+ process.on('SIGTERM', onSigterm);
212
+ process.on('SIGHUP', onSigterm);
213
+ return () => {
214
+ process.off('SIGTERM', onSigterm);
215
+ process.off('SIGHUP', onSigterm);
216
+ };
217
+ }, [cleanupAndExit]);
218
+ // ---------------------------------------------------------------------------
219
+ // Handle user message (streaming)
220
+ // ---------------------------------------------------------------------------
221
+ const handleUserMessage = useCallback(async (message) => {
222
+ appendOutput('');
223
+ startSpinner('thinking');
224
+ let fullOutput = '';
225
+ let firstText = true;
226
+ let currentTool = '';
227
+ let currentToolInput = {};
228
+ let toolCallCount = 0;
229
+ const callbacks = {
230
+ onToolApproval: (toolName, input, riskLevel) => {
231
+ return new Promise((resolve) => {
232
+ stopSpinnerFn();
233
+ setApproval({ toolName, input, riskLevel, resolve });
234
+ setState('waiting_approval');
235
+ });
236
+ },
237
+ };
238
+ try {
239
+ for await (const event of agent.stream(message, callbacks)) {
240
+ switch (event.type) {
241
+ case 'text': {
242
+ if (firstText) {
243
+ stopSpinnerFn();
244
+ firstText = false;
245
+ }
246
+ const chunk = event.data;
247
+ fullOutput += chunk;
248
+ // Stream text by appending to the last output line
249
+ // We'll accumulate and print at the end for cleaner output
250
+ break;
251
+ }
252
+ case 'tool_use': {
253
+ stopSpinnerFn();
254
+ if (!firstText) {
255
+ // Flush accumulated text
256
+ if (fullOutput) {
257
+ const formatted = fullOutput.split('\n').map(l => ` ${PURPLE('│')} ${l}`).join('\n');
258
+ appendOutput(formatted);
259
+ fullOutput = '';
260
+ }
261
+ firstText = true;
262
+ }
263
+ const block = event.data;
264
+ currentTool = block.name;
265
+ currentToolInput = block.input;
266
+ toolCallCount++;
267
+ if (undoManager && (block.name === 'file_write' || block.name === 'file_edit')) {
268
+ const filePath = (block.input.path ?? block.input.file_path);
269
+ if (filePath) {
270
+ await undoManager.recordBefore(filePath, block.name === 'file_write' ? 'write' : 'edit');
271
+ }
272
+ }
273
+ // Render tool exec line
274
+ const toolSummary = Object.entries(block.input)
275
+ .map(([k, v]) => {
276
+ const val = typeof v === 'string' ? v.slice(0, 80) : JSON.stringify(v).slice(0, 80);
277
+ return `${DIM(k + ':')} ${WHITE(val)}`;
278
+ })
279
+ .join(' ');
280
+ appendOutput(` ${TOOL_SYM} ${CYAN.bold(block.name)} ${toolSummary}`);
281
+ startSpinner('tool', block.name);
282
+ break;
283
+ }
284
+ case 'tool_result': {
285
+ stopSpinnerFn();
286
+ const block = event.data;
287
+ if (verbose) {
288
+ const truncated = block.content.length > 2000
289
+ ? block.content.slice(0, 2000) + DIM('\n... (truncated)')
290
+ : block.content;
291
+ const prefix = block.is_error ? ERROR_SYM : SUCCESS_SYM;
292
+ appendOutput(` ${prefix} ${DIM(currentTool + ':')} ${block.is_error ? RED(truncated) : DIM(truncated)}`);
293
+ }
294
+ else {
295
+ appendOutput(summarizeToolResult(currentTool, currentToolInput, block.content, block.is_error));
296
+ }
297
+ currentTool = '';
298
+ currentToolInput = {};
299
+ startSpinner('thinking');
300
+ firstText = true;
301
+ break;
302
+ }
303
+ case 'done': {
304
+ stopSpinnerFn();
305
+ break;
306
+ }
307
+ case 'error': {
308
+ stopSpinnerFn();
309
+ const err = event.data;
310
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(err.message)}\n`);
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ }
316
+ catch (err) {
317
+ stopSpinnerFn();
318
+ const msg = err instanceof Error ? err.message : String(err);
319
+ if (msg.includes('not found') || msg.includes('404') || (msg.includes('model') && msg.includes('pull'))) {
320
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(`Model '${currentModelRef.current}' not found. Run ${GREEN('/models')} to see available models or ${GREEN('/model <name>')} to switch.`)}\n`);
321
+ }
322
+ else {
323
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(msg)}\n`);
324
+ }
325
+ }
326
+ // Flush remaining text
327
+ if (fullOutput) {
328
+ const formatted = fullOutput.split('\n').map(l => ` ${PURPLE('│')} ${l}`).join('\n');
329
+ appendOutput(formatted);
330
+ }
331
+ if (fullOutput || !firstText) {
332
+ appendOutput('');
333
+ }
334
+ // Turn summary
335
+ const agentState = agent.getState();
336
+ const tokens = agentState.tokenUsage;
337
+ const summary = getCompletionSummary();
338
+ const tokenInfo = tokens.input_tokens > 0 || tokens.output_tokens > 0
339
+ ? ` ${DIM('·')} ${DIM(`${tokens.input_tokens} in / ${tokens.output_tokens} out`)}`
340
+ : '';
341
+ appendOutput(` ${DIM(summary)}${tokenInfo}`);
342
+ costTracker.record(currentModelRef.current, tokens.input_tokens, tokens.output_tokens, toolCallCount);
343
+ session.syncFromAgent(agent.getHistory());
344
+ // Auto-compact if needed
345
+ if (session.shouldCompact()) {
346
+ try {
347
+ const stats = await session.compact(adapter, currentModelRef.current);
348
+ agent.replaceMessages(session.messages);
349
+ appendOutput(` ${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`);
350
+ }
351
+ catch {
352
+ // best effort
353
+ }
354
+ }
355
+ autoSaver.schedule(doSave);
356
+ appendOutput(SEPARATOR);
357
+ appendOutput('');
358
+ }, [agent, session, adapter, costTracker, undoManager, verbose, autoSaver, doSave,
359
+ appendOutput, startSpinner, stopSpinnerFn, getCompletionSummary]);
360
+ // ---------------------------------------------------------------------------
361
+ // Handle team message
362
+ // ---------------------------------------------------------------------------
363
+ const handleTeamMessage = useCallback(async (goal, teamConfig) => {
364
+ appendOutput('');
365
+ appendOutput(` ${PURPLE('◈')} Running team ${PURPLE.bold(teamConfig.name)} with ${teamConfig.agents.length} agents...`);
366
+ appendOutput('');
367
+ startSpinner('thinking');
368
+ try {
369
+ const result = await orchestrator.runTeam(teamConfig, goal);
370
+ stopSpinnerFn();
371
+ for (const [agentName, agentResult] of result.agentResults) {
372
+ const status = agentResult.success ? GREEN('✓') : RED('✗');
373
+ appendOutput(` ${status} ${CYAN(agentName)}`);
374
+ if (agentResult.output) {
375
+ const lines = agentResult.output.split('\n');
376
+ const displayLines = verbose ? lines : lines.slice(0, 20);
377
+ for (const line of displayLines) {
378
+ appendOutput(` ${PURPLE('│')} ${line}`);
379
+ }
380
+ if (!verbose && lines.length > 20) {
381
+ appendOutput(` ${PURPLE('│')} ${DIM(`... ${lines.length - 20} more lines (use --verbose)`)}`);
382
+ }
383
+ appendOutput('');
384
+ }
385
+ if (agentResult.toolCalls.length > 0) {
386
+ const tools = agentResult.toolCalls.map(t => t.toolName);
387
+ const unique = [...new Set(tools)];
388
+ appendOutput(` ${DIM(` tools: ${unique.join(', ')} (${tools.length} calls)`)}`);
389
+ }
390
+ }
391
+ const usage = result.totalTokenUsage;
392
+ const summary = getCompletionSummary();
393
+ const tokenInfo = `${usage.input_tokens} in / ${usage.output_tokens} out`;
394
+ appendOutput(` ${DIM(summary)} ${DIM('·')} ${DIM(tokenInfo)}`);
395
+ appendOutput(` ${result.success ? GREEN('Team completed successfully') : RED('Team had failures')}`);
396
+ }
397
+ catch (err) {
398
+ stopSpinnerFn();
399
+ const msg = err instanceof Error ? err.message : String(err);
400
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(msg)}\n`);
401
+ }
402
+ appendOutput(SEPARATOR);
403
+ appendOutput('');
404
+ }, [orchestrator, verbose, appendOutput, startSpinner, stopSpinnerFn, getCompletionSummary]);
405
+ // ---------------------------------------------------------------------------
406
+ // Process slash commands
407
+ // ---------------------------------------------------------------------------
408
+ const processCommand = useCallback(async (input) => {
409
+ const { name, args } = parseSlashCommand(input);
410
+ const cmd = getCommand(name);
411
+ if (!cmd) {
412
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(`Unknown command: /${name}. Type /help for available commands.`)}\n`);
413
+ return false;
414
+ }
415
+ const result = await cmd.execute(args, {
416
+ session: session.getState(),
417
+ switchModel: (model) => { currentModelRef.current = model; },
418
+ clearHistory: () => {
419
+ session.clear();
420
+ agent.reset();
421
+ permissionManager.resetSession();
422
+ },
423
+ ollamaUrl,
424
+ adapter,
425
+ model: currentModelRef.current,
426
+ agentTokenUsage: agent.getState().tokenUsage,
427
+ permissionManager,
428
+ });
429
+ if (result === '__QUIT__') {
430
+ await cleanupAndExit();
431
+ return true;
432
+ }
433
+ if (result === '__COMPACT__') {
434
+ session.syncFromAgent(agent.getHistory());
435
+ const stats = await session.compact(adapter, currentModelRef.current);
436
+ agent.replaceMessages(session.messages);
437
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`)}`);
438
+ return false;
439
+ }
440
+ if (result === '__DIFF__') {
441
+ const gitTool = toolRegistry.get('git_diff');
442
+ if (gitTool) {
443
+ const diffResult = await gitTool.execute({ staged: false }, {
444
+ agent: { name: 'cmdr', role: 'assistant', model: currentModelRef.current },
445
+ cwd: process.cwd(),
446
+ });
447
+ appendOutput(`\n${WHITE(diffResult.data)}\n`);
448
+ }
449
+ return false;
450
+ }
451
+ if (result === '__SESSION_SAVE__') {
452
+ session.syncFromAgent(agent.getHistory());
453
+ if (session.messages.length > 0) {
454
+ const sid = await saveSession(session.getState(), currentModelRef.current);
455
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Session saved: ${DIM(sid)}`)}`);
456
+ }
457
+ else {
458
+ appendOutput(` ${DIM('ℹ')} ${WHITE('No messages to save.')}`);
459
+ }
460
+ return false;
461
+ }
462
+ if (typeof result === 'string' && result.startsWith('__SESSION_RESUME__:')) {
463
+ const sessionId = result.slice('__SESSION_RESUME__:'.length);
464
+ const saved = await loadSession(sessionId);
465
+ if (saved) {
466
+ agent.replaceMessages(saved.messages);
467
+ session.syncFromAgent(saved.messages);
468
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Resumed session ${DIM(saved.id)} (${saved.messages.length} messages)`)}`);
469
+ }
470
+ else {
471
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(`Session not found: ${sessionId}`)}\n`);
472
+ }
473
+ return false;
474
+ }
475
+ if (typeof result === 'string' && result.startsWith('__TEAM_SWITCH__:')) {
476
+ const preset = result.slice('__TEAM_SWITCH__:'.length);
477
+ const teamCfg = getTeamPreset(preset);
478
+ if (teamCfg) {
479
+ activeTeamRef.current = teamCfg;
480
+ const teamAgents = teamCfg.agents.map(a => a.name).join(', ');
481
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Switched to team: ${PURPLE(teamCfg.name)} (${teamAgents})`)}`);
482
+ }
483
+ else {
484
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(`Unknown team: ${preset}. Use: review, fullstack, security`)}\n`);
485
+ }
486
+ return false;
487
+ }
488
+ if (result === '__AGENTS_STATUS__') {
489
+ if (!activeTeamRef.current) {
490
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Solo mode (agent: ${GREEN('cmdr')}). Use /team <preset> for multi-agent.`)}`);
491
+ }
492
+ else {
493
+ const status = orchestrator.getStatus();
494
+ const lines = ['', ` ${PURPLE.bold(`Team: ${activeTeamRef.current.name}`)}`, ''];
495
+ for (const agentCfg of activeTeamRef.current.agents) {
496
+ const agentStatus = status?.agents.find(a => a.name === agentCfg.name);
497
+ const statusLabel = agentStatus ? DIM(agentStatus.status) : DIM('idle');
498
+ lines.push(` ${GREEN('•')} ${WHITE(agentCfg.name.padEnd(12))} ${statusLabel}`);
499
+ }
500
+ lines.push('');
501
+ appendLines(lines);
502
+ }
503
+ return false;
504
+ }
505
+ if (result === '__TASKS_STATUS__') {
506
+ const status = orchestrator.getStatus();
507
+ if (!status) {
508
+ appendOutput(` ${DIM('ℹ')} ${WHITE('No active team or tasks.')}`);
509
+ }
510
+ else {
511
+ const s = status.tasks;
512
+ if (s) {
513
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Tasks: ${GREEN(`${s.completed} done`)} · ${YELLOW(`${s.in_progress} running`)} · ${DIM(`${s.pending} pending`)} · ${s.failed > 0 ? RED(`${s.failed} failed`) : DIM('0 failed')}`)}`);
514
+ }
515
+ }
516
+ return false;
517
+ }
518
+ if (result === '__COST__') {
519
+ const summary = costTracker.getSummary();
520
+ const elapsed = costTracker.formatElapsed();
521
+ appendLines([
522
+ '',
523
+ ` ${PURPLE.bold('Token usage')}`,
524
+ '',
525
+ ` ${DIM('Model:')} ${WHITE(summary.model)}`,
526
+ ` ${DIM('Turns:')} ${WHITE(String(summary.turns))}`,
527
+ ` ${DIM('Input tokens:')} ${WHITE(String(summary.totalInputTokens))}`,
528
+ ` ${DIM('Output tokens:')} ${WHITE(String(summary.totalOutputTokens))}`,
529
+ ` ${DIM('Total tokens:')} ${GREEN(String(summary.totalTokens))}`,
530
+ ` ${DIM('Tool calls:')} ${WHITE(String(summary.totalToolCalls))}`,
531
+ ` ${DIM('Avg/turn:')} ${WHITE(String(summary.avgTokensPerTurn))}`,
532
+ ` ${DIM('Session time:')} ${WHITE(elapsed)}`,
533
+ '',
534
+ ]);
535
+ return false;
536
+ }
537
+ if (result === '__UNDO__') {
538
+ if (undoManager.count === 0) {
539
+ appendOutput(` ${DIM('ℹ')} ${WHITE('Nothing to undo.')}`);
540
+ }
541
+ else {
542
+ const change = await undoManager.undoLast();
543
+ if (change) {
544
+ const action = change.originalContent === null ? 'deleted' : 'restored';
545
+ const fname = change.path.split('/').pop() ?? change.path;
546
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Undid ${change.type} on ${GREEN(fname)} (${action})`)}`);
547
+ }
548
+ }
549
+ return false;
550
+ }
551
+ if (typeof result === 'string' && result.startsWith('__PLUGIN__:')) {
552
+ const sub = result.slice('__PLUGIN__:'.length).trim();
553
+ if (sub === 'list' || !sub) {
554
+ const plugins = pluginManager.list();
555
+ if (plugins.length === 0) {
556
+ appendOutput(` ${DIM('ℹ')} ${WHITE('No plugins loaded. Add plugins to ~/.cmdr/config.toml')}`);
557
+ }
558
+ else {
559
+ appendLines(['', ` ${PURPLE.bold('Loaded plugins')}`, '']);
560
+ for (const p of plugins) {
561
+ const hooks = p.hooks ? Object.keys(p.hooks).length : 0;
562
+ const tools = p.tools?.length ?? 0;
563
+ appendOutput(` ${GREEN('•')} ${WHITE(p.name)} v${p.version} ${DIM(`(${hooks} hooks, ${tools} tools)`)}`);
564
+ }
565
+ appendOutput('');
566
+ }
567
+ }
568
+ return false;
569
+ }
570
+ if (typeof result === 'string' && result.startsWith('__MCP__:')) {
571
+ const sub = result.slice('__MCP__:'.length).trim().split(/\s+/);
572
+ const action = sub[0];
573
+ if (action === 'list' || !action) {
574
+ const conns = mcpClient.listConnections();
575
+ if (conns.length === 0) {
576
+ appendOutput(` ${DIM('ℹ')} ${WHITE('No MCP servers connected. Add to ~/.cmdr/config.toml or use /mcp connect <name> <url>')}`);
577
+ }
578
+ else {
579
+ appendLines(['', ` ${PURPLE.bold('MCP servers')}`, '']);
580
+ for (const c of conns) {
581
+ const status = c.connected ? GREEN('connected') : RED('disconnected');
582
+ appendOutput(` ${GREEN('•')} ${WHITE(c.name)} ${DIM(c.url)} ${status} ${DIM(`(${c.tools} tools)`)}`);
583
+ }
584
+ appendOutput('');
585
+ }
586
+ }
587
+ else if (action === 'connect') {
588
+ const name = sub[1];
589
+ const url = sub[2];
590
+ if (!name || !url) {
591
+ appendOutput(` ${DIM('ℹ')} ${WHITE('Usage: /mcp connect <name> <url>')}`);
592
+ }
593
+ else {
594
+ try {
595
+ const tools = await mcpClient.connect({ name, url });
596
+ mcpClient.registerTools(toolRegistry);
597
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Connected to ${name}: ${tools.length} tools discovered`)}`);
598
+ }
599
+ catch (err) {
600
+ const msg = err instanceof Error ? err.message : String(err);
601
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(msg)}\n`);
602
+ }
603
+ }
604
+ }
605
+ else if (action === 'disconnect') {
606
+ const name = sub[1];
607
+ if (name && mcpClient.disconnect(name)) {
608
+ appendOutput(` ${DIM('ℹ')} ${WHITE(`Disconnected from ${name}`)}`);
609
+ }
610
+ else {
611
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(`MCP server "${name}" not found`)}\n`);
612
+ }
613
+ }
614
+ return false;
615
+ }
616
+ if (result)
617
+ appendOutput(String(result));
618
+ return false;
619
+ }, [agent, session, permissionManager, adapter, ollamaUrl, toolRegistry,
620
+ orchestrator, costTracker, undoManager, pluginManager, mcpClient,
621
+ cleanupAndExit, appendOutput, appendLines]);
622
+ // ---------------------------------------------------------------------------
623
+ // Submit handler
624
+ // ---------------------------------------------------------------------------
625
+ const handleSubmit = useCallback(async (value) => {
626
+ const input = value.trim();
627
+ setInputValue('');
628
+ if (!input)
629
+ return;
630
+ if (stateRef.current !== 'idle')
631
+ return;
632
+ // Echo user input
633
+ appendOutput(`${GREEN.bold('❯')} ${WHITE(input)}`);
634
+ setState('processing');
635
+ try {
636
+ if (isSlashCommand(input)) {
637
+ const shouldExit = await processCommand(input);
638
+ if (shouldExit)
639
+ return;
640
+ }
641
+ else if (activeTeamRef.current) {
642
+ await handleTeamMessage(input, activeTeamRef.current);
643
+ }
644
+ else {
645
+ await handleUserMessage(input);
646
+ }
647
+ }
648
+ catch (err) {
649
+ const msg = err instanceof Error ? err.message : String(err);
650
+ appendOutput(`\n ${ERROR_SYM} ${RED.bold(msg)}\n`);
651
+ }
652
+ finally {
653
+ // stateRef.current may have changed during async processing (TS narrows incorrectly here)
654
+ if (stateRef.current !== 'exiting') {
655
+ setState('idle');
656
+ }
657
+ }
658
+ }, [appendOutput, processCommand, handleUserMessage, handleTeamMessage]);
659
+ // ---------------------------------------------------------------------------
660
+ // Keyboard input handling (Ctrl+C, Ctrl+D)
661
+ // ---------------------------------------------------------------------------
662
+ useInput((input, key) => {
663
+ // Ctrl+C handling
664
+ if (key.ctrl && input === 'c') {
665
+ const now = Date.now();
666
+ if (now - lastSigintRef.current < 1000) {
667
+ cleanupAndExit();
668
+ return;
669
+ }
670
+ lastSigintRef.current = now;
671
+ if (stateRef.current === 'processing') {
672
+ appendOutput(`\n ${DIM('Interrupt — press Ctrl+C again to exit.')}`);
673
+ }
674
+ else if (stateRef.current === 'idle') {
675
+ appendOutput(` ${DIM('Press Ctrl+C again to exit.')}`);
676
+ }
677
+ return;
678
+ }
679
+ // Ctrl+D — ignore (don't close)
680
+ if (key.ctrl && input === 'd') {
681
+ return;
682
+ }
683
+ });
684
+ // ---------------------------------------------------------------------------
685
+ // Handle approval input
686
+ // ---------------------------------------------------------------------------
687
+ const handleApprovalSubmit = useCallback((value) => {
688
+ if (!approval)
689
+ return;
690
+ const trimmed = value.trim().toLowerCase();
691
+ setApprovalInput('');
692
+ let decision;
693
+ if (trimmed === 'y' || trimmed === 'yes' || trimmed === '') {
694
+ decision = 'allow';
695
+ }
696
+ else if (trimmed === 'a' || trimmed === 'always') {
697
+ decision = 'allow-always';
698
+ }
699
+ else {
700
+ decision = 'deny';
701
+ }
702
+ const resolve = approval.resolve;
703
+ setApproval(null);
704
+ setState('processing');
705
+ resolve(decision);
706
+ }, [approval]);
707
+ // ---------------------------------------------------------------------------
708
+ // Render
709
+ // ---------------------------------------------------------------------------
710
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: outputLines, children: (line) => (_jsx(Text, { children: line.text }, line.id)) }), state === 'processing' && spinnerText && (_jsx(Text, { children: spinnerText })), state === 'waiting_approval' && approval && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: '' }), _jsx(Text, { children: ` ${YELLOW('⚠')} ${WHITE('Tool approval required')} ${DIM('[')}${approval.riskLevel === 'dangerous' ? RED(approval.riskLevel.toUpperCase()) : YELLOW(approval.riskLevel.toUpperCase())}${DIM(']')}` }), _jsx(Text, { children: ` ${DIM('Tool:')} ${CYAN(approval.toolName)}` }), Object.entries(approval.input).map(([key, val]) => {
711
+ const display = typeof val === 'string'
712
+ ? val.length > 120 ? val.slice(0, 120) + DIM('...') : val
713
+ : JSON.stringify(val).slice(0, 120);
714
+ return _jsx(Text, { children: ` ${DIM(key + ':')} ${WHITE(display)}` }, key);
715
+ }), _jsx(Text, { children: '' }), _jsx(Text, { children: ` ${GREEN('y')}${DIM('es')} ${DIM('/')} ${RED('n')}${DIM('o')} ${DIM('/')} ${PURPLE('a')}${DIM('lways allow this tool')}` }), _jsxs(Box, { children: [_jsx(Text, { children: ` ${YELLOW('?')} ` }), _jsx(TextInput, { value: approvalInput, onChange: setApprovalInput, onSubmit: handleApprovalSubmit })] })] })), state === 'idle' && (_jsxs(Box, { children: [_jsxs(Text, { children: [GREEN.bold('❯'), " "] }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit })] }))] }));
716
+ }
717
+ //# sourceMappingURL=App.js.map