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.
- package/dist/bin/cmdr.js +2 -1
- package/dist/bin/cmdr.js.map +1 -1
- package/dist/src/cli/args.js +1 -1
- package/dist/src/cli/commands.js +26 -0
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/ink/App.d.ts +40 -0
- package/dist/src/cli/ink/App.d.ts.map +1 -0
- package/dist/src/cli/ink/App.js +717 -0
- package/dist/src/cli/ink/App.js.map +1 -0
- package/dist/src/cli/repl.d.ts +4 -0
- package/dist/src/cli/repl.d.ts.map +1 -1
- package/dist/src/cli/repl.js +59 -532
- package/dist/src/cli/repl.js.map +1 -1
- package/dist/src/cli/theme.d.ts +1 -1
- package/dist/src/cli/theme.d.ts.map +1 -1
- package/dist/src/cli/theme.js +2 -2
- package/dist/src/cli/theme.js.map +1 -1
- package/dist/src/core/types.d.ts +6 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/llm/model-registry.d.ts +5 -0
- package/dist/src/llm/model-registry.d.ts.map +1 -1
- package/dist/src/llm/model-registry.js +43 -0
- package/dist/src/llm/model-registry.js.map +1 -1
- package/dist/src/llm/ollama.d.ts.map +1 -1
- package/dist/src/llm/ollama.js +6 -0
- package/dist/src/llm/ollama.js.map +1 -1
- package/dist/src/session/prompt-builder.d.ts.map +1 -1
- package/dist/src/session/prompt-builder.js +9 -0
- package/dist/src/session/prompt-builder.js.map +1 -1
- package/dist/src/skills/injector.d.ts +19 -0
- package/dist/src/skills/injector.d.ts.map +1 -0
- package/dist/src/skills/injector.js +67 -0
- package/dist/src/skills/injector.js.map +1 -0
- package/dist/src/skills/loader.d.ts +31 -0
- package/dist/src/skills/loader.d.ts.map +1 -0
- package/dist/src/skills/loader.js +147 -0
- package/dist/src/skills/loader.js.map +1 -0
- 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
|