agent.libx.js 0.93.28 → 0.93.30
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/cli/cli.ts +70 -21
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +245 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +22 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/cli/cli.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, rea
|
|
|
24
24
|
import { homedir, tmpdir } from 'node:os';
|
|
25
25
|
import { grabClipboardImage } from './clipboard';
|
|
26
26
|
import { join, resolve, basename, extname, dirname } from 'node:path';
|
|
27
|
-
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported } from 'ai.libx.js';
|
|
27
|
+
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions } from 'ai.libx.js';
|
|
28
28
|
import {
|
|
29
29
|
PermissionPolicy, loadCommands, expandCommand, loadSkills, forComponent, composeHooks, reflectOnRun,
|
|
30
30
|
AgentOptions, exitSessionTool,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from '../src/index';
|
|
34
34
|
import { mountMcpServer, mountMcpServers, type MountedMcp, type McpServerConfig } from '../src/mcp.client';
|
|
35
35
|
import { McpOAuth } from './mcpOAuth';
|
|
36
|
-
import { buildAgent, summarizeCall, type CliOptions } from './core';
|
|
36
|
+
import { buildAgent, summarizeCall, cursorProviderOptions, type CliOptions } from './core';
|
|
37
37
|
import { DuplexAgent } from '../src/duplex';
|
|
38
38
|
import { VoiceIO } from './voice';
|
|
39
39
|
import { loadConfig, type AgentConfig } from './config';
|
|
@@ -95,7 +95,7 @@ interface Args {
|
|
|
95
95
|
duplex: boolean; voiceModel?: string; thinkModel?: string | false; voice: boolean;
|
|
96
96
|
cont: boolean; resume?: string; sessionId?: string; fork?: boolean; outputFormat: 'text' | 'json' | 'stream-json';
|
|
97
97
|
allowedTools?: string[]; disallowedTools?: string[]; appendSystemPrompt?: string; addDirs?: string[];
|
|
98
|
-
print?: boolean; debug?: boolean;
|
|
98
|
+
print?: boolean; debug?: boolean; scratch?: boolean;
|
|
99
99
|
}
|
|
100
100
|
/** Parse a numeric flag, failing fast on a missing/NaN/negative value so a typo can't silently disable a kill-switch. */
|
|
101
101
|
function numFlag(raw: string | undefined, flag: string): number {
|
|
@@ -141,6 +141,7 @@ export function parseArgs(argv: string[]): Args {
|
|
|
141
141
|
else if (x === '--ask') a.ask = true;
|
|
142
142
|
else if (x === '--yes' || x === '-y') a.yes = true;
|
|
143
143
|
else if (x === '--vfs' || x === '--sandbox') a.vfs = true;
|
|
144
|
+
else if (x === '--scratch') a.scratch = true;
|
|
144
145
|
else if (x === '--boddb') a.boddb = val(++i, x);
|
|
145
146
|
else if (x === '--seed') a.seed = true;
|
|
146
147
|
else if (x === '--shell') a.shell = true;
|
|
@@ -194,6 +195,7 @@ Flags:
|
|
|
194
195
|
--no-stream disable token streaming
|
|
195
196
|
(default: disk mode — full real filesystem access, like Claude Code)
|
|
196
197
|
--vfs, --sandbox sandbox mode: work over an in-memory copy of cwd — real disk is NEVER modified
|
|
198
|
+
--scratch spill big web outputs to scratch files (kept out of context; peek via Grep/Ask)
|
|
197
199
|
--boddb <dir> database-backed workspace: files live in a persistent bod-db store at <dir>,
|
|
198
200
|
surviving across runs — real disk is NEVER modified (DB-native; add --seed below)
|
|
199
201
|
--seed with --boddb: hydrate the store from cwd on the first run (empty DB) only
|
|
@@ -314,14 +316,19 @@ function makeHost(format: 'text' | 'json' | 'stream-json' = 'text', opts?: { str
|
|
|
314
316
|
const md = format === 'text' && useColor && opts?.stream ? new MarkdownStream({ bold, dim, cyan }, process.stdout.columns ?? 80) : null;
|
|
315
317
|
if (md) process.on('SIGWINCH', () => md.resize(process.stdout.columns ?? 80)); // reflow streamed markdown on resize
|
|
316
318
|
const flushText = () => { if (md && md.pending()) process.stdout.write(md.flush()); };
|
|
319
|
+
// `thinking_delta` streams to stderr WITHOUT a trailing newline (a continuous dimmed reasoning flow).
|
|
320
|
+
// Any other output that follows (the answer on stdout, a `· notice`, the next step header) must first
|
|
321
|
+
// close that open line — else it collides mid-word into the reasoning tail (`· step 2` → `step ya`).
|
|
322
|
+
let openReasonLine = false;
|
|
323
|
+
const closeReasonLine = () => { if (openReasonLine) { process.stderr.write('\n'); openReasonLine = false; } };
|
|
317
324
|
return {
|
|
318
325
|
flushText,
|
|
319
326
|
notify(e) {
|
|
320
327
|
spinner.stop(); // real output arriving → clear the spinner first
|
|
321
328
|
if (e.kind === 'text_delta') {
|
|
322
329
|
if (streamJson) process.stdout.write(JSON.stringify({ type: 'text', text: e.message }) + '\n');
|
|
323
|
-
else if (md) process.stdout.write(md.feed(e.message)); // line-buffered markdown render
|
|
324
|
-
else (cleanStdout ? process.stderr : process.stdout).write(e.message); // json keeps stdout clean → stderr
|
|
330
|
+
else if (md) { closeReasonLine(); process.stdout.write(md.feed(e.message)); } // line-buffered markdown render
|
|
331
|
+
else { if (!cleanStdout) closeReasonLine(); (cleanStdout ? process.stderr : process.stdout).write(e.message); } // json keeps stdout clean → stderr
|
|
325
332
|
return;
|
|
326
333
|
}
|
|
327
334
|
// extended-thinking (reasoning-native models): a dimmed stderr flow in text mode; a typed event in
|
|
@@ -333,12 +340,17 @@ function makeHost(format: 'text' | 'json' | 'stream-json' = 'text', opts?: { str
|
|
|
333
340
|
// stay in order — otherwise late-flushed markdown lands after the thinking it preceded.
|
|
334
341
|
if (md && md.pending()) process.stdout.write(md.flush() + '\n');
|
|
335
342
|
process.stderr.write(dim(e.message));
|
|
343
|
+
openReasonLine = true; // unterminated reasoning line — next output must close it
|
|
336
344
|
}
|
|
337
345
|
return;
|
|
338
346
|
}
|
|
347
|
+
// Structural events (tool_use/tool_result/tool_result_image/turn_start) have dedicated renderers
|
|
348
|
+
// (displayHooks ⚙ chrome, the step header) — never dump their raw `kind` as a `· notice`, or the
|
|
349
|
+
// result preview gets sandwiched between junk `· tool_use`/`· tool_result` labels.
|
|
350
|
+
if (!('message' in e)) return;
|
|
351
|
+
closeReasonLine(); // close an open reasoning line so the notice starts fresh (not glued mid-word)
|
|
339
352
|
if (md && md.pending()) process.stdout.write(md.flush() + '\n'); // finish the in-progress line before a notice
|
|
340
|
-
|
|
341
|
-
err(dim(` · ${notice}\n`)); // tool-activity notices stay on stderr (human chrome) for every format
|
|
353
|
+
err(dim(` · ${e.message}\n`)); // tool-activity notices stay on stderr (human chrome) for every format
|
|
342
354
|
},
|
|
343
355
|
async confirm(prompt) {
|
|
344
356
|
// arrow-select on a TTY; fall back to a typed y/N when piped (selectMenu → null)
|
|
@@ -697,6 +709,7 @@ function optsFor(args: Args, ai: ChatLike, cfg: Partial<AgentConfig> = {}, extra
|
|
|
697
709
|
boddb: args.boddb,
|
|
698
710
|
seed: args.seed,
|
|
699
711
|
realShell: args.shell, // undefined → core.ts defaults (on for disk, off for sandbox/boddb)
|
|
712
|
+
scratch: args.scratch,
|
|
700
713
|
appendSystemPrompt: args.appendSystemPrompt,
|
|
701
714
|
addDirs: args.addDirs,
|
|
702
715
|
stream: args.stream,
|
|
@@ -1089,7 +1102,10 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1089
1102
|
if (duplex) {
|
|
1090
1103
|
// Workers must not stream into the voice channel — strip the host/stream seam (display hooks stay:
|
|
1091
1104
|
// worker tool activity prints as dim stderr chrome). `signal` is per-task (duplex.ts owns it).
|
|
1092
|
-
|
|
1105
|
+
// Drop providerOptions too: they're computed for the MAIN agent's model (e.g. cursor's cwd/cursorSession)
|
|
1106
|
+
// and must never leak onto a worker whose model differs — DuplexAgent recomputes them per worker model
|
|
1107
|
+
// via providerOptionsFor below. (A cursor cursorSession leaking onto an anthropic worker is a hard 400.)
|
|
1108
|
+
const { host: _host, stream: _stream, signal: _signal, providerOptions: _po, ...wo } = agent.options;
|
|
1093
1109
|
workerOptions = wo as AgentOptions;
|
|
1094
1110
|
if (workerOptions.permissions)
|
|
1095
1111
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: undefined, ask: duplexAsk });
|
|
@@ -1184,6 +1200,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1184
1200
|
...((args.voiceModel ?? cfg.reflexModel) ? { reflexModel: resolveModelOrNewest((args.voiceModel ?? cfg.reflexModel)!) } : {}),
|
|
1185
1201
|
actModel: agent.options.model,
|
|
1186
1202
|
actOptions: workerOptions,
|
|
1203
|
+
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
1187
1204
|
...((args.thinkModel ?? cfg.thinkModel) !== undefined ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {}),
|
|
1188
1205
|
host,
|
|
1189
1206
|
...(args.voice ? { voiceStyle: 'conversational' as const, progressUpdates: true, askRelay: true } : {}), // voice: progress asides + worker questions relayed through the conversation
|
|
@@ -1268,11 +1285,22 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1268
1285
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
1269
1286
|
return img ? { display: 'Image', ref: '@' + img.path, path: img.path } : null;
|
|
1270
1287
|
};
|
|
1271
|
-
//
|
|
1288
|
+
// Hard teardown — used by the SIGINT path and the keypress force-quit escalation. Never let the
|
|
1289
|
+
// user get trapped: even if a turn's abort() fails to unwind (wedged stream/tool), this exits.
|
|
1290
|
+
const forceQuit = (code = 130) => {
|
|
1291
|
+
try { voiceIO?.stop(); } catch { /* mic/player may already be down */ }
|
|
1292
|
+
try { disposeCursorSessions(); } catch { /* reap warm cursor helpers, best-effort */ }
|
|
1293
|
+
void closeMcp(mounted);
|
|
1294
|
+
process.exit(code);
|
|
1295
|
+
};
|
|
1296
|
+
// Ctrl-C: cancel the in-flight turn if one's running; a second Ctrl-C while already cancelling
|
|
1297
|
+
// (i.e. the abort didn't unwind) force-quits. No active turn → clean up MCP children and exit.
|
|
1272
1298
|
process.on('SIGINT', () => {
|
|
1273
|
-
if (activeTurn) {
|
|
1274
|
-
|
|
1275
|
-
|
|
1299
|
+
if (activeTurn) {
|
|
1300
|
+
if (aborting) { err(red('\n ⏻ force-quit\n')); forceQuit(); return; } // already cancelling → hard escape
|
|
1301
|
+
activeTurn.abort(); voiceIO?.interrupt(); return;
|
|
1302
|
+
}
|
|
1303
|
+
forceQuit();
|
|
1276
1304
|
});
|
|
1277
1305
|
installCancelGuards(mounted);
|
|
1278
1306
|
const store = new SessionStore(cwd);
|
|
@@ -1739,7 +1767,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1739
1767
|
commands: { desc: 'pick a custom slash command to run (./.agent/commands)', run: () => pickAndRun('command') },
|
|
1740
1768
|
skills: { desc: 'pick a skill to run (./.agent/skills)', run: () => pickAndRun('skill') },
|
|
1741
1769
|
mcp: {
|
|
1742
|
-
desc: 'manage MCP servers — /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [resources [name]]',
|
|
1770
|
+
desc: 'manage MCP servers — /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [tools [name]] [resources [name]]',
|
|
1743
1771
|
run: async (a) => {
|
|
1744
1772
|
const sub = a[0]?.toLowerCase();
|
|
1745
1773
|
if (sub === 'login') {
|
|
@@ -1783,6 +1811,16 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1783
1811
|
err(dim(` removed "${name}"\n`));
|
|
1784
1812
|
return;
|
|
1785
1813
|
}
|
|
1814
|
+
if (sub === 'tools') {
|
|
1815
|
+
const filter = a[1];
|
|
1816
|
+
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
1817
|
+
if (!targets.length) { err(dim(` (no ${filter ? `MCP server "${filter}"` : 'MCP servers'} found)\n`)); return; }
|
|
1818
|
+
for (const m of targets) {
|
|
1819
|
+
err(` ${cyan(m.name)} ${dim(`(${m.tools.length} tools)`)}\n`);
|
|
1820
|
+
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, '')}${t.description ? ' — ' + t.description.split('\n')[0] : ''}\n`));
|
|
1821
|
+
}
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1786
1824
|
if (sub === 'resources') {
|
|
1787
1825
|
const filter = a[1];
|
|
1788
1826
|
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
@@ -1802,14 +1840,15 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1802
1840
|
if (!mounted.length) { err(dim(' (no MCP servers mounted)\n')); return; }
|
|
1803
1841
|
for (const m of mounted) {
|
|
1804
1842
|
const ver = m.serverInfo?.name ? dim(` · ${m.serverInfo.name}${m.serverInfo.version ? ' v' + m.serverInfo.version : ''}`) : '';
|
|
1805
|
-
err(` ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}\n`);
|
|
1806
|
-
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, '')}${t.description ? ' — ' + t.description.split('\n')[0] : ''}\n`));
|
|
1843
|
+
err(` ${green('✓')} ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}\n`);
|
|
1807
1844
|
}
|
|
1845
|
+
err(dim(' /mcp tools [name] to list tools · /mcp resources [name] for resources\n'));
|
|
1808
1846
|
};
|
|
1809
1847
|
const items: SelectItem[] = [
|
|
1810
1848
|
{ label: 'list', value: 'list', desc: `show mounted servers (${mounted.length})` },
|
|
1811
1849
|
{ label: 'add', value: 'add', desc: 'mount a new MCP server' },
|
|
1812
1850
|
...(mounted.length ? [
|
|
1851
|
+
{ label: 'tools', value: 'tools', desc: 'list a server\'s tools' },
|
|
1813
1852
|
{ label: 'remove', value: 'remove', desc: 'unmount an MCP server' },
|
|
1814
1853
|
{ label: 'resources', value: 'resources', desc: 'list server resources' },
|
|
1815
1854
|
] : []),
|
|
@@ -1831,12 +1870,12 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1831
1870
|
const rv = await selectMenu(process.stderr, { title: 'remove server', items: mounted.map((m) => ({ label: m.name, value: m.name })) });
|
|
1832
1871
|
if (!rv) return;
|
|
1833
1872
|
a = ['remove', rv];
|
|
1834
|
-
} else if (picked === 'resources') {
|
|
1835
|
-
if (mounted.length === 1) { a = [
|
|
1873
|
+
} else if (picked === 'tools' || picked === 'resources') {
|
|
1874
|
+
if (mounted.length === 1) { a = [picked, mounted[0].name]; }
|
|
1836
1875
|
else {
|
|
1837
|
-
const rv = await selectMenu(process.stderr, { title:
|
|
1876
|
+
const rv = await selectMenu(process.stderr, { title: `server ${picked}`, items: [{ label: '(all)', value: '' }, ...mounted.map((m) => ({ label: m.name, value: m.name }))] });
|
|
1838
1877
|
if (rv === null) return;
|
|
1839
|
-
a = rv ? [
|
|
1878
|
+
a = rv ? [picked, rv] : [picked];
|
|
1840
1879
|
}
|
|
1841
1880
|
}
|
|
1842
1881
|
// fall through to re-run with the assembled args
|
|
@@ -1943,8 +1982,16 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1943
1982
|
const cancel = k === 'escape' || (key?.ctrl && k === 'c');
|
|
1944
1983
|
if (cancel) {
|
|
1945
1984
|
if (stashBuf) { stashBuf = ''; err('\r\x1b[K'); return; } // first Esc clears the stash line
|
|
1946
|
-
if (!aborting) {
|
|
1947
|
-
|
|
1985
|
+
if (!aborting) {
|
|
1986
|
+
aborting = true; activeTurn.abort(); voiceIO?.interrupt();
|
|
1987
|
+
err(yellow('\n ⎋ cancelling…') + dim(' (Ctrl-C again to force-quit)\n'));
|
|
1988
|
+
// Watchdog: if the turn hasn't unwound shortly, the abort is wedged — surface the escape hatch.
|
|
1989
|
+
setTimeout(() => {
|
|
1990
|
+
if (activeTurn) err(red(' ⚠ still cancelling — press Ctrl-C to force-quit\n'));
|
|
1991
|
+
}, 4000).unref?.();
|
|
1992
|
+
} else if (key?.ctrl && k === 'c') {
|
|
1993
|
+
err(red('\n ⏻ force-quit\n')); forceQuit(); // already cancelling + another Ctrl-C → never trapped
|
|
1994
|
+
} else if (k === 'escape' && !pendingRewind) { pendingRewind = true; err(dim(' ⎋⎋ jumping back to edit…\n')); }
|
|
1948
1995
|
return;
|
|
1949
1996
|
}
|
|
1950
1997
|
// Stash input: type ahead while the turn runs
|
|
@@ -2183,6 +2230,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2183
2230
|
// (voice children / sockets / MCP handles can keep the process alive otherwise).
|
|
2184
2231
|
voiceIO?.stop();
|
|
2185
2232
|
releaseStdin();
|
|
2233
|
+
disposeCursorSessions();
|
|
2186
2234
|
await closeMcp(mounted);
|
|
2187
2235
|
process.exit(130);
|
|
2188
2236
|
}
|
|
@@ -2191,6 +2239,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2191
2239
|
}
|
|
2192
2240
|
}
|
|
2193
2241
|
releaseStdin();
|
|
2242
|
+
disposeCursorSessions(); // reap any warm cursor helper (its stdout pipe would otherwise keep the loop alive → hang on exit)
|
|
2194
2243
|
await closeMcp(mounted);
|
|
2195
2244
|
}
|
|
2196
2245
|
|
package/dist/cli.d.ts
CHANGED