cmdr-agent 1.0.1 → 1.1.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 +35 -18
- package/dist/bin/cmdr.js.map +1 -1
- package/dist/src/cli/args.js +1 -1
- package/dist/src/cli/commands.js +29 -4
- 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 +55 -530
- 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/package.json +5 -1
package/dist/src/cli/repl.js
CHANGED
|
@@ -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
|
|
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,7 @@ 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 {
|
|
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 { renderWelcome, renderError, GREEN, PURPLE, DIM, WHITE, renderInfo, YELLOW, RED, } from './theme.js';
|
|
20
22
|
import { PermissionManager } from '../core/permissions.js';
|
|
21
23
|
import { saveSession, loadSession, findRecentSession, DebouncedSaver } from '../session/session-persistence.js';
|
|
22
24
|
import { PluginManager } from '../plugins/plugin-manager.js';
|
|
@@ -24,6 +26,8 @@ import { McpClient } from '../plugins/mcp-client.js';
|
|
|
24
26
|
import { loadConfig } from '../config/config-loader.js';
|
|
25
27
|
import { CostTracker } from '../session/cost-tracker.js';
|
|
26
28
|
import { UndoManager } from '../session/undo-manager.js';
|
|
29
|
+
import { startThinking, stopSpinner, spinnerSuccess, spinnerFail, getCompletionSummary, startToolExec } from './spinner.js';
|
|
30
|
+
import App from './ink/App.js';
|
|
27
31
|
export async function startRepl(options) {
|
|
28
32
|
const cwd = process.cwd();
|
|
29
33
|
const verbose = options.verbose ?? false;
|
|
@@ -86,7 +90,6 @@ export async function startRepl(options) {
|
|
|
86
90
|
// Permission manager
|
|
87
91
|
const permissionManager = new PermissionManager(options.dangerouslySkipPermissions ? 'yolo' : 'normal');
|
|
88
92
|
await permissionManager.loadSettings();
|
|
89
|
-
// CLI flag overrides persisted mode
|
|
90
93
|
if (options.dangerouslySkipPermissions) {
|
|
91
94
|
permissionManager.setMode('yolo');
|
|
92
95
|
}
|
|
@@ -104,21 +107,20 @@ export async function startRepl(options) {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
// Create agent
|
|
107
|
-
|
|
110
|
+
const currentModel = options.model;
|
|
108
111
|
const agent = new Agent({ ...SOLO_CODER, model: currentModel, systemPrompt }, adapter, toolRegistry, cwd, permissionManager);
|
|
109
|
-
// --- Welcome ---
|
|
112
|
+
// --- Welcome banner (prints to normal terminal before Ink takes over) ---
|
|
110
113
|
const modeLabel = permissionManager.getMode() === 'yolo'
|
|
111
114
|
? YELLOW('⚠ yolo (all tools auto-approved)')
|
|
112
115
|
: permissionManager.getMode() === 'strict'
|
|
113
116
|
? RED('strict (all tools require approval)')
|
|
114
117
|
: GREEN('normal (write tools require approval)');
|
|
115
|
-
console.log(renderWelcome(currentModel, projectInfo));
|
|
118
|
+
console.log(renderWelcome(currentModel, projectInfo, options.version));
|
|
116
119
|
console.log(` ${DIM('Permissions:')} ${modeLabel}`);
|
|
117
120
|
if (activeTeamConfig) {
|
|
118
121
|
const teamAgents = activeTeamConfig.agents.map(a => a.name).join(', ');
|
|
119
122
|
console.log(` ${DIM('Team:')} ${PURPLE(activeTeamConfig.name)} ${DIM(`(${teamAgents})`)}`);
|
|
120
123
|
}
|
|
121
|
-
// Show CMDR.md loading status
|
|
122
124
|
if (projectContext.cmdrInstructions) {
|
|
123
125
|
const lineCount = projectContext.cmdrInstructions.split('\n').length;
|
|
124
126
|
console.log(` ${DIM(`CMDR.md loaded (${lineCount} lines)`)}`);
|
|
@@ -155,438 +157,49 @@ export async function startRepl(options) {
|
|
|
155
157
|
}
|
|
156
158
|
};
|
|
157
159
|
console.log('');
|
|
158
|
-
// --- Handle
|
|
160
|
+
// --- Handle one-shot prompt (non-interactive) ---
|
|
159
161
|
if (options.initialPrompt) {
|
|
160
|
-
await
|
|
162
|
+
await handleOneShot(options.initialPrompt, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager);
|
|
161
163
|
await doSave();
|
|
162
164
|
return;
|
|
163
165
|
}
|
|
164
|
-
// --- Interactive REPL ---
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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);
|
|
166
|
+
// --- Interactive REPL via Ink ---
|
|
167
|
+
const app = render(React.createElement(App, {
|
|
168
|
+
agent,
|
|
169
|
+
session,
|
|
170
|
+
model: currentModel,
|
|
171
|
+
permissionManager,
|
|
172
|
+
adapter,
|
|
173
|
+
orchestrator,
|
|
174
|
+
activeTeamConfig,
|
|
175
|
+
costTracker,
|
|
176
|
+
undoManager,
|
|
177
|
+
pluginManager,
|
|
178
|
+
mcpClient,
|
|
179
|
+
toolRegistry,
|
|
180
|
+
ollamaUrl: options.ollamaUrl,
|
|
181
|
+
verbose,
|
|
182
|
+
doSave,
|
|
183
|
+
autoSaver,
|
|
184
|
+
}), {
|
|
185
|
+
exitOnCtrlC: false, // We handle Ctrl+C ourselves
|
|
186
|
+
patchConsole: false, // Ban console logs from being intercepted during banner
|
|
459
187
|
});
|
|
460
|
-
|
|
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)}`;
|
|
188
|
+
await app.waitUntilExit();
|
|
572
189
|
}
|
|
573
190
|
// ---------------------------------------------------------------------------
|
|
574
|
-
//
|
|
191
|
+
// One-shot handler (non-interactive --prompt mode)
|
|
575
192
|
// ---------------------------------------------------------------------------
|
|
576
|
-
async function
|
|
577
|
-
console.log('');
|
|
193
|
+
async function handleOneShot(message, agent, session, model, permissionManager, verbose, adapter, costTracker, undoManager) {
|
|
194
|
+
console.log('');
|
|
578
195
|
startThinking();
|
|
579
196
|
let fullOutput = '';
|
|
580
197
|
let firstText = true;
|
|
581
198
|
let currentTool = '';
|
|
582
199
|
let currentToolInput = {};
|
|
583
200
|
let toolCallCount = 0;
|
|
584
|
-
// Build callbacks with the approval gate
|
|
585
|
-
const callbacks = {
|
|
586
|
-
onToolApproval: (toolName, input, riskLevel) => promptApproval(toolName, input, riskLevel),
|
|
587
|
-
};
|
|
588
201
|
try {
|
|
589
|
-
for await (const event of agent.stream(message
|
|
202
|
+
for await (const event of agent.stream(message)) {
|
|
590
203
|
switch (event.type) {
|
|
591
204
|
case 'text': {
|
|
592
205
|
if (firstText) {
|
|
@@ -596,8 +209,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
596
209
|
}
|
|
597
210
|
const chunk = event.data;
|
|
598
211
|
fullOutput += chunk;
|
|
599
|
-
// Stream raw text token-by-token (no markdown on partial chunks)
|
|
600
|
-
// Handle newlines by adding the prefix
|
|
601
212
|
const formatted = chunk.replace(/\n/g, `\n ${PURPLE('│')} `);
|
|
602
213
|
process.stdout.write(formatted);
|
|
603
214
|
break;
|
|
@@ -605,7 +216,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
605
216
|
case 'tool_use': {
|
|
606
217
|
stopSpinner();
|
|
607
218
|
if (!firstText) {
|
|
608
|
-
// Terminate previous text stream line
|
|
609
219
|
process.stdout.write('\n');
|
|
610
220
|
firstText = true;
|
|
611
221
|
}
|
|
@@ -613,46 +223,36 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
613
223
|
currentTool = block.name;
|
|
614
224
|
currentToolInput = block.input;
|
|
615
225
|
toolCallCount++;
|
|
616
|
-
|
|
617
|
-
if (undoManager && (block.name === 'file_write' || block.name === 'file_edit')) {
|
|
226
|
+
if (block.name === 'file_write' || block.name === 'file_edit') {
|
|
618
227
|
const filePath = (block.input.path ?? block.input.file_path);
|
|
619
|
-
if (filePath)
|
|
228
|
+
if (filePath)
|
|
620
229
|
await undoManager.recordBefore(filePath, block.name === 'file_write' ? 'write' : 'edit');
|
|
621
|
-
}
|
|
622
230
|
}
|
|
623
|
-
|
|
231
|
+
const toolSummary = Object.entries(block.input)
|
|
232
|
+
.map(([k, v]) => {
|
|
233
|
+
const val = typeof v === 'string' ? v.slice(0, 80) : JSON.stringify(v).slice(0, 80);
|
|
234
|
+
return `${DIM(k + ':')} ${WHITE(val)}`;
|
|
235
|
+
})
|
|
236
|
+
.join(' ');
|
|
237
|
+
console.log(` ${GREEN('⚡')} ${GREEN.bold(block.name)} ${toolSummary}`);
|
|
624
238
|
startToolExec(block.name);
|
|
625
239
|
break;
|
|
626
240
|
}
|
|
627
241
|
case 'tool_result': {
|
|
628
242
|
const block = event.data;
|
|
629
|
-
if (block.is_error)
|
|
243
|
+
if (block.is_error)
|
|
630
244
|
spinnerFail(currentTool);
|
|
631
|
-
|
|
632
|
-
else {
|
|
245
|
+
else
|
|
633
246
|
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
247
|
currentTool = '';
|
|
647
248
|
currentToolInput = {};
|
|
648
249
|
startThinking();
|
|
649
250
|
firstText = true;
|
|
650
251
|
break;
|
|
651
252
|
}
|
|
652
|
-
case 'done':
|
|
253
|
+
case 'done':
|
|
653
254
|
stopSpinner();
|
|
654
255
|
break;
|
|
655
|
-
}
|
|
656
256
|
case 'error': {
|
|
657
257
|
stopSpinner();
|
|
658
258
|
const err = event.data;
|
|
@@ -665,22 +265,12 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
665
265
|
catch (err) {
|
|
666
266
|
stopSpinner();
|
|
667
267
|
const msg = err instanceof Error ? err.message : String(err);
|
|
668
|
-
|
|
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
|
-
}
|
|
268
|
+
console.error(renderError(msg));
|
|
674
269
|
}
|
|
675
|
-
|
|
676
|
-
if (!firstText) {
|
|
270
|
+
if (!firstText)
|
|
677
271
|
process.stdout.write('\n');
|
|
678
|
-
|
|
679
|
-
// Add spacing after response
|
|
680
|
-
if (fullOutput) {
|
|
272
|
+
if (fullOutput)
|
|
681
273
|
console.log('');
|
|
682
|
-
}
|
|
683
|
-
// Show turn summary with whimsical verb
|
|
684
274
|
const state = agent.getState();
|
|
685
275
|
const tokens = state.tokenUsage;
|
|
686
276
|
const summary = getCompletionSummary();
|
|
@@ -688,72 +278,7 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
688
278
|
? ` ${DIM('·')} ${DIM(`${tokens.input_tokens} in / ${tokens.output_tokens} out`)}`
|
|
689
279
|
: '';
|
|
690
280
|
console.log(` ${DIM(summary)}${tokenInfo}`);
|
|
691
|
-
|
|
692
|
-
costTracker?.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
|
|
693
|
-
// Sync agent messages into session for compaction tracking
|
|
281
|
+
costTracker.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
|
|
694
282
|
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
283
|
}
|
|
759
284
|
//# sourceMappingURL=repl.js.map
|