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
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,8 @@ import { Orchestrator } from '../core/orchestrator.js';
|
|
|
14
18
|
import { SessionManager } from '../session/session-manager.js';
|
|
15
19
|
import { discoverProject } from '../session/project-context.js';
|
|
16
20
|
import { buildSystemPrompt } from '../session/prompt-builder.js';
|
|
17
|
-
import {
|
|
18
|
-
import { renderWelcome,
|
|
19
|
-
import { isSlashCommand, parseSlashCommand, getCommand, } from './commands.js';
|
|
21
|
+
import { resolveContextLength } from '../llm/model-registry.js';
|
|
22
|
+
import { renderWelcome, renderError, GREEN, PURPLE, DIM, WHITE, renderInfo, YELLOW, RED, } from './theme.js';
|
|
20
23
|
import { PermissionManager } from '../core/permissions.js';
|
|
21
24
|
import { saveSession, loadSession, findRecentSession, DebouncedSaver } from '../session/session-persistence.js';
|
|
22
25
|
import { PluginManager } from '../plugins/plugin-manager.js';
|
|
@@ -24,6 +27,8 @@ import { McpClient } from '../plugins/mcp-client.js';
|
|
|
24
27
|
import { loadConfig } from '../config/config-loader.js';
|
|
25
28
|
import { CostTracker } from '../session/cost-tracker.js';
|
|
26
29
|
import { UndoManager } from '../session/undo-manager.js';
|
|
30
|
+
import { startThinking, stopSpinner, spinnerSuccess, spinnerFail, getCompletionSummary, startToolExec } from './spinner.js';
|
|
31
|
+
import App from './ink/App.js';
|
|
27
32
|
export async function startRepl(options) {
|
|
28
33
|
const cwd = process.cwd();
|
|
29
34
|
const verbose = options.verbose ?? false;
|
|
@@ -42,8 +47,9 @@ export async function startRepl(options) {
|
|
|
42
47
|
const projectInfo = projectContext.language !== 'unknown'
|
|
43
48
|
? `${projectContext.language}${projectContext.framework ? ' / ' + projectContext.framework : ''}`
|
|
44
49
|
: cwd.split('/').pop() || 'unknown';
|
|
45
|
-
// Session
|
|
46
|
-
const
|
|
50
|
+
// Session — resolve model's actual context length
|
|
51
|
+
const modelContextLength = await resolveContextLength(options.model, options.ollamaUrl);
|
|
52
|
+
const session = new SessionManager(projectContext, modelContextLength);
|
|
47
53
|
// Build system prompt with project context
|
|
48
54
|
const systemPrompt = buildSystemPrompt({
|
|
49
55
|
basePrompt: SOLO_CODER.systemPrompt,
|
|
@@ -86,7 +92,6 @@ export async function startRepl(options) {
|
|
|
86
92
|
// Permission manager
|
|
87
93
|
const permissionManager = new PermissionManager(options.dangerouslySkipPermissions ? 'yolo' : 'normal');
|
|
88
94
|
await permissionManager.loadSettings();
|
|
89
|
-
// CLI flag overrides persisted mode
|
|
90
95
|
if (options.dangerouslySkipPermissions) {
|
|
91
96
|
permissionManager.setMode('yolo');
|
|
92
97
|
}
|
|
@@ -104,21 +109,20 @@ export async function startRepl(options) {
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
// Create agent
|
|
107
|
-
|
|
112
|
+
const currentModel = options.model;
|
|
108
113
|
const agent = new Agent({ ...SOLO_CODER, model: currentModel, systemPrompt }, adapter, toolRegistry, cwd, permissionManager);
|
|
109
|
-
// --- Welcome ---
|
|
114
|
+
// --- Welcome banner (prints to normal terminal before Ink takes over) ---
|
|
110
115
|
const modeLabel = permissionManager.getMode() === 'yolo'
|
|
111
116
|
? YELLOW('⚠ yolo (all tools auto-approved)')
|
|
112
117
|
: permissionManager.getMode() === 'strict'
|
|
113
118
|
? RED('strict (all tools require approval)')
|
|
114
119
|
: GREEN('normal (write tools require approval)');
|
|
115
|
-
console.log(renderWelcome(currentModel, projectInfo));
|
|
120
|
+
console.log(renderWelcome(currentModel, projectInfo, options.version));
|
|
116
121
|
console.log(` ${DIM('Permissions:')} ${modeLabel}`);
|
|
117
122
|
if (activeTeamConfig) {
|
|
118
123
|
const teamAgents = activeTeamConfig.agents.map(a => a.name).join(', ');
|
|
119
124
|
console.log(` ${DIM('Team:')} ${PURPLE(activeTeamConfig.name)} ${DIM(`(${teamAgents})`)}`);
|
|
120
125
|
}
|
|
121
|
-
// Show CMDR.md loading status
|
|
122
126
|
if (projectContext.cmdrInstructions) {
|
|
123
127
|
const lineCount = projectContext.cmdrInstructions.split('\n').length;
|
|
124
128
|
console.log(` ${DIM(`CMDR.md loaded (${lineCount} lines)`)}`);
|
|
@@ -155,438 +159,49 @@ export async function startRepl(options) {
|
|
|
155
159
|
}
|
|
156
160
|
};
|
|
157
161
|
console.log('');
|
|
158
|
-
// --- Handle
|
|
162
|
+
// --- Handle one-shot prompt (non-interactive) ---
|
|
159
163
|
if (options.initialPrompt) {
|
|
160
|
-
await
|
|
164
|
+
await handleOneShot(options.initialPrompt, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager);
|
|
161
165
|
await doSave();
|
|
162
166
|
return;
|
|
163
167
|
}
|
|
164
|
-
// --- Interactive REPL ---
|
|
165
|
-
const
|
|
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);
|
|
168
|
+
// --- Interactive REPL via Ink ---
|
|
169
|
+
const app = render(React.createElement(App, {
|
|
170
|
+
agent,
|
|
171
|
+
session,
|
|
172
|
+
model: currentModel,
|
|
173
|
+
permissionManager,
|
|
174
|
+
adapter,
|
|
175
|
+
orchestrator,
|
|
176
|
+
activeTeamConfig,
|
|
177
|
+
costTracker,
|
|
178
|
+
undoManager,
|
|
179
|
+
pluginManager,
|
|
180
|
+
mcpClient,
|
|
181
|
+
toolRegistry,
|
|
182
|
+
ollamaUrl: options.ollamaUrl,
|
|
183
|
+
verbose,
|
|
184
|
+
doSave,
|
|
185
|
+
autoSaver,
|
|
186
|
+
}), {
|
|
187
|
+
exitOnCtrlC: false, // We handle Ctrl+C ourselves
|
|
188
|
+
patchConsole: false, // Ban console logs from being intercepted during banner
|
|
459
189
|
});
|
|
460
|
-
|
|
461
|
-
autoSaver.cancel();
|
|
462
|
-
if (!closed) {
|
|
463
|
-
session.syncFromAgent(agent.getHistory());
|
|
464
|
-
if (session.messages.length > 0) {
|
|
465
|
-
try {
|
|
466
|
-
const sid = await saveSession(session.getState(), currentModel);
|
|
467
|
-
console.log(`\n ${DIM('Session saved:')} ${DIM(sid)}`);
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
// best effort
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
console.log(`\n ${PURPLE('Goodbye.')} ${DIM('Session ended.')}\n`);
|
|
474
|
-
}
|
|
475
|
-
if (processing) {
|
|
476
|
-
closed = true;
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
process.exit(0);
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
// ---------------------------------------------------------------------------
|
|
484
|
-
// Approval prompt — asks the user to approve a tool call
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
|
-
function promptApproval(toolName, input, riskLevel) {
|
|
487
|
-
return new Promise((resolve) => {
|
|
488
|
-
const riskColor = riskLevel === 'dangerous' ? RED : YELLOW;
|
|
489
|
-
const riskLabel = riskColor(riskLevel.toUpperCase());
|
|
490
|
-
// Show the tool call details
|
|
491
|
-
console.log('');
|
|
492
|
-
console.log(` ${YELLOW('⚠')} ${WHITE('Tool approval required')} ${DIM('[')}${riskLabel}${DIM(']')}`);
|
|
493
|
-
console.log(` ${DIM('Tool:')} ${CYAN(toolName)}`);
|
|
494
|
-
// Show a summary of key arguments
|
|
495
|
-
for (const [key, val] of Object.entries(input)) {
|
|
496
|
-
const display = typeof val === 'string'
|
|
497
|
-
? val.length > 120 ? val.slice(0, 120) + DIM('...') : val
|
|
498
|
-
: JSON.stringify(val).slice(0, 120);
|
|
499
|
-
console.log(` ${DIM(key + ':')} ${WHITE(display)}`);
|
|
500
|
-
}
|
|
501
|
-
console.log('');
|
|
502
|
-
console.log(` ${GREEN('y')}${DIM('es')} ${DIM('/')} ${RED('n')}${DIM('o')} ${DIM('/')} ${PURPLE('a')}${DIM('lways allow this tool')}`);
|
|
503
|
-
// Create a one-shot readline for the approval prompt
|
|
504
|
-
const approvalRl = readline.createInterface({
|
|
505
|
-
input: process.stdin,
|
|
506
|
-
output: process.stdout,
|
|
507
|
-
terminal: true,
|
|
508
|
-
});
|
|
509
|
-
approvalRl.question(` ${YELLOW('?')} `, (answer) => {
|
|
510
|
-
approvalRl.close();
|
|
511
|
-
const trimmed = answer.trim().toLowerCase();
|
|
512
|
-
if (trimmed === 'y' || trimmed === 'yes' || trimmed === '') {
|
|
513
|
-
resolve('allow');
|
|
514
|
-
}
|
|
515
|
-
else if (trimmed === 'a' || trimmed === 'always') {
|
|
516
|
-
resolve('allow-always');
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
resolve('deny');
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
// ---------------------------------------------------------------------------
|
|
525
|
-
// Tool result summary — collapsed single-line display
|
|
526
|
-
// ---------------------------------------------------------------------------
|
|
527
|
-
function summarizeToolResult(toolName, input, content, isError) {
|
|
528
|
-
const lineCount = content.split('\n').length;
|
|
529
|
-
const prefix = isError ? ERROR_SYMBOL : SUCCESS_SYMBOL;
|
|
530
|
-
let summary;
|
|
531
|
-
switch (toolName) {
|
|
532
|
-
case 'file_read': {
|
|
533
|
-
const file = input.path ?? 'unknown';
|
|
534
|
-
const fname = file.split('/').pop() ?? file;
|
|
535
|
-
summary = `${fname} (${lineCount} lines)`;
|
|
536
|
-
break;
|
|
537
|
-
}
|
|
538
|
-
case 'glob': {
|
|
539
|
-
const pattern = input.pattern ?? '*';
|
|
540
|
-
const matches = content === '(no matches)' ? 0 : lineCount;
|
|
541
|
-
summary = `${pattern} (${matches} matches)`;
|
|
542
|
-
break;
|
|
543
|
-
}
|
|
544
|
-
case 'bash': {
|
|
545
|
-
const cmd = input.command ?? '';
|
|
546
|
-
const truncCmd = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
|
|
547
|
-
// Extract exit code from result if present
|
|
548
|
-
const exitMatch = content.match(/\[exit code: (\d+)\]/);
|
|
549
|
-
const exitCode = exitMatch ? exitMatch[1] : '0';
|
|
550
|
-
summary = `\`${truncCmd}\` exit=${exitCode} (${lineCount} lines)`;
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
553
|
-
case 'grep': {
|
|
554
|
-
const pattern = input.pattern ?? '';
|
|
555
|
-
const matches = content === '(no matches)' ? 0 : lineCount;
|
|
556
|
-
summary = `/${pattern}/ (${matches} matches)`;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
case 'think': {
|
|
560
|
-
const thought = input.thought ?? '';
|
|
561
|
-
const preview = thought.length > 60 ? thought.slice(0, 57) + '...' : thought;
|
|
562
|
-
summary = preview;
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
default: {
|
|
566
|
-
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
567
|
-
summary = `${bytes > 1024 ? Math.round(bytes / 1024) + ' KB' : bytes + ' B'}`;
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return ` ${prefix} ${DIM(toolName)} ${DIM(summary)}`;
|
|
190
|
+
await app.waitUntilExit();
|
|
572
191
|
}
|
|
573
192
|
// ---------------------------------------------------------------------------
|
|
574
|
-
//
|
|
193
|
+
// One-shot handler (non-interactive --prompt mode)
|
|
575
194
|
// ---------------------------------------------------------------------------
|
|
576
|
-
async function
|
|
577
|
-
console.log('');
|
|
195
|
+
async function handleOneShot(message, agent, session, model, permissionManager, verbose, adapter, costTracker, undoManager) {
|
|
196
|
+
console.log('');
|
|
578
197
|
startThinking();
|
|
579
198
|
let fullOutput = '';
|
|
580
199
|
let firstText = true;
|
|
581
200
|
let currentTool = '';
|
|
582
201
|
let currentToolInput = {};
|
|
583
202
|
let toolCallCount = 0;
|
|
584
|
-
// Build callbacks with the approval gate
|
|
585
|
-
const callbacks = {
|
|
586
|
-
onToolApproval: (toolName, input, riskLevel) => promptApproval(toolName, input, riskLevel),
|
|
587
|
-
};
|
|
588
203
|
try {
|
|
589
|
-
for await (const event of agent.stream(message
|
|
204
|
+
for await (const event of agent.stream(message)) {
|
|
590
205
|
switch (event.type) {
|
|
591
206
|
case 'text': {
|
|
592
207
|
if (firstText) {
|
|
@@ -596,8 +211,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
596
211
|
}
|
|
597
212
|
const chunk = event.data;
|
|
598
213
|
fullOutput += chunk;
|
|
599
|
-
// Stream raw text token-by-token (no markdown on partial chunks)
|
|
600
|
-
// Handle newlines by adding the prefix
|
|
601
214
|
const formatted = chunk.replace(/\n/g, `\n ${PURPLE('│')} `);
|
|
602
215
|
process.stdout.write(formatted);
|
|
603
216
|
break;
|
|
@@ -605,7 +218,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
605
218
|
case 'tool_use': {
|
|
606
219
|
stopSpinner();
|
|
607
220
|
if (!firstText) {
|
|
608
|
-
// Terminate previous text stream line
|
|
609
221
|
process.stdout.write('\n');
|
|
610
222
|
firstText = true;
|
|
611
223
|
}
|
|
@@ -613,46 +225,36 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
613
225
|
currentTool = block.name;
|
|
614
226
|
currentToolInput = block.input;
|
|
615
227
|
toolCallCount++;
|
|
616
|
-
|
|
617
|
-
if (undoManager && (block.name === 'file_write' || block.name === 'file_edit')) {
|
|
228
|
+
if (block.name === 'file_write' || block.name === 'file_edit') {
|
|
618
229
|
const filePath = (block.input.path ?? block.input.file_path);
|
|
619
|
-
if (filePath)
|
|
230
|
+
if (filePath)
|
|
620
231
|
await undoManager.recordBefore(filePath, block.name === 'file_write' ? 'write' : 'edit');
|
|
621
|
-
}
|
|
622
232
|
}
|
|
623
|
-
|
|
233
|
+
const toolSummary = Object.entries(block.input)
|
|
234
|
+
.map(([k, v]) => {
|
|
235
|
+
const val = typeof v === 'string' ? v.slice(0, 80) : JSON.stringify(v).slice(0, 80);
|
|
236
|
+
return `${DIM(k + ':')} ${WHITE(val)}`;
|
|
237
|
+
})
|
|
238
|
+
.join(' ');
|
|
239
|
+
console.log(` ${GREEN('⚡')} ${GREEN.bold(block.name)} ${toolSummary}`);
|
|
624
240
|
startToolExec(block.name);
|
|
625
241
|
break;
|
|
626
242
|
}
|
|
627
243
|
case 'tool_result': {
|
|
628
244
|
const block = event.data;
|
|
629
|
-
if (block.is_error)
|
|
245
|
+
if (block.is_error)
|
|
630
246
|
spinnerFail(currentTool);
|
|
631
|
-
|
|
632
|
-
else {
|
|
247
|
+
else
|
|
633
248
|
spinnerSuccess(currentTool);
|
|
634
|
-
}
|
|
635
|
-
if (verbose) {
|
|
636
|
-
// Full output in verbose mode
|
|
637
|
-
const truncated = block.content.length > 2000
|
|
638
|
-
? block.content.slice(0, 2000) + DIM('\n... (truncated)')
|
|
639
|
-
: block.content;
|
|
640
|
-
const prefix = block.is_error ? ERROR_SYMBOL : SUCCESS_SYMBOL;
|
|
641
|
-
console.log(` ${prefix} ${DIM(currentTool + ':')} ${block.is_error ? RED(truncated) : DIM(truncated)}`);
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
console.log(summarizeToolResult(currentTool, currentToolInput, block.content, block.is_error));
|
|
645
|
-
}
|
|
646
249
|
currentTool = '';
|
|
647
250
|
currentToolInput = {};
|
|
648
251
|
startThinking();
|
|
649
252
|
firstText = true;
|
|
650
253
|
break;
|
|
651
254
|
}
|
|
652
|
-
case 'done':
|
|
255
|
+
case 'done':
|
|
653
256
|
stopSpinner();
|
|
654
257
|
break;
|
|
655
|
-
}
|
|
656
258
|
case 'error': {
|
|
657
259
|
stopSpinner();
|
|
658
260
|
const err = event.data;
|
|
@@ -665,22 +267,12 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
665
267
|
catch (err) {
|
|
666
268
|
stopSpinner();
|
|
667
269
|
const msg = err instanceof Error ? err.message : String(err);
|
|
668
|
-
|
|
669
|
-
console.error(renderError(`Model '${model}' not found. Run ${GREEN('/models')} to see available models or ${GREEN('/model <name>')} to switch.`));
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
console.error(renderError(msg));
|
|
673
|
-
}
|
|
270
|
+
console.error(renderError(msg));
|
|
674
271
|
}
|
|
675
|
-
|
|
676
|
-
if (!firstText) {
|
|
272
|
+
if (!firstText)
|
|
677
273
|
process.stdout.write('\n');
|
|
678
|
-
|
|
679
|
-
// Add spacing after response
|
|
680
|
-
if (fullOutput) {
|
|
274
|
+
if (fullOutput)
|
|
681
275
|
console.log('');
|
|
682
|
-
}
|
|
683
|
-
// Show turn summary with whimsical verb
|
|
684
276
|
const state = agent.getState();
|
|
685
277
|
const tokens = state.tokenUsage;
|
|
686
278
|
const summary = getCompletionSummary();
|
|
@@ -688,72 +280,7 @@ async function handleUserMessage(message, agent, session, model, permissionManag
|
|
|
688
280
|
? ` ${DIM('·')} ${DIM(`${tokens.input_tokens} in / ${tokens.output_tokens} out`)}`
|
|
689
281
|
: '';
|
|
690
282
|
console.log(` ${DIM(summary)}${tokenInfo}`);
|
|
691
|
-
|
|
692
|
-
costTracker?.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
|
|
693
|
-
// Sync agent messages into session for compaction tracking
|
|
283
|
+
costTracker.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
|
|
694
284
|
session.syncFromAgent(agent.getHistory());
|
|
695
|
-
// Auto-compact if context is getting full
|
|
696
|
-
if (session.shouldCompact()) {
|
|
697
|
-
try {
|
|
698
|
-
const stats = await session.compact(adapter, model);
|
|
699
|
-
agent.replaceMessages(session.messages);
|
|
700
|
-
console.log(` ${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`);
|
|
701
|
-
}
|
|
702
|
-
catch {
|
|
703
|
-
// best effort — don't break the REPL
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
// Trigger debounced auto-save
|
|
707
|
-
onAfterResponse?.();
|
|
708
|
-
console.log(GREEN_DIM('─'.repeat(60)));
|
|
709
|
-
console.log('');
|
|
710
|
-
}
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
|
-
// Team message handler — runs goal through the orchestrator
|
|
713
|
-
// ---------------------------------------------------------------------------
|
|
714
|
-
async function handleTeamMessage(goal, orchestrator, teamConfig, model, verbose) {
|
|
715
|
-
console.log('');
|
|
716
|
-
console.log(` ${PURPLE('◈')} Running team ${PURPLE.bold(teamConfig.name)} with ${teamConfig.agents.length} agents...`);
|
|
717
|
-
console.log('');
|
|
718
|
-
startThinking();
|
|
719
|
-
try {
|
|
720
|
-
const result = await orchestrator.runTeam(teamConfig, goal);
|
|
721
|
-
stopSpinner();
|
|
722
|
-
// Display results from each agent
|
|
723
|
-
for (const [agentName, agentResult] of result.agentResults) {
|
|
724
|
-
const status = agentResult.success ? GREEN('✓') : RED('✗');
|
|
725
|
-
console.log(` ${status} ${CYAN(agentName)}`);
|
|
726
|
-
if (agentResult.output) {
|
|
727
|
-
const lines = agentResult.output.split('\n');
|
|
728
|
-
const displayLines = verbose ? lines : lines.slice(0, 20);
|
|
729
|
-
for (const line of displayLines) {
|
|
730
|
-
console.log(` ${PURPLE('│')} ${line}`);
|
|
731
|
-
}
|
|
732
|
-
if (!verbose && lines.length > 20) {
|
|
733
|
-
console.log(` ${PURPLE('│')} ${DIM(`... ${lines.length - 20} more lines (use --verbose)`)}`);
|
|
734
|
-
}
|
|
735
|
-
console.log('');
|
|
736
|
-
}
|
|
737
|
-
// Tool call summary
|
|
738
|
-
if (agentResult.toolCalls.length > 0) {
|
|
739
|
-
const tools = agentResult.toolCalls.map(t => t.toolName);
|
|
740
|
-
const unique = [...new Set(tools)];
|
|
741
|
-
console.log(` ${DIM(` tools: ${unique.join(', ')} (${tools.length} calls)`)}`);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
// Summary
|
|
745
|
-
const usage = result.totalTokenUsage;
|
|
746
|
-
const summary = getCompletionSummary();
|
|
747
|
-
const tokenInfo = `${usage.input_tokens} in / ${usage.output_tokens} out`;
|
|
748
|
-
console.log(` ${DIM(summary)} ${DIM('·')} ${DIM(tokenInfo)}`);
|
|
749
|
-
console.log(` ${result.success ? GREEN('Team completed successfully') : RED('Team had failures')}`);
|
|
750
|
-
}
|
|
751
|
-
catch (err) {
|
|
752
|
-
stopSpinner();
|
|
753
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
754
|
-
console.error(renderError(msg));
|
|
755
|
-
}
|
|
756
|
-
console.log(GREEN_DIM('─'.repeat(60)));
|
|
757
|
-
console.log('');
|
|
758
285
|
}
|
|
759
286
|
//# sourceMappingURL=repl.js.map
|