agent.libx.js 0.93.29 → 0.93.31
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 +100 -33
- package/dist/{Agent-kWrJvtZM.d.ts → Agent-uWtu_WFY.d.ts} +11 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +373 -108
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -3
- package/dist/index.js +86 -11
- 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
|
|
@@ -211,7 +213,7 @@ Flags:
|
|
|
211
213
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
212
214
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O — mic in,
|
|
213
215
|
spoken replies out (echo-cancelled; speak over it to interrupt)
|
|
214
|
-
--voice-model <id> with --duplex: the fast voice model (default groq/openai/gpt-oss-
|
|
216
|
+
--voice-model <id> with --duplex: the fast voice model (default groq/openai/gpt-oss-120b)
|
|
215
217
|
--think-model <id> with --duplex: the premium deep-reasoning model (default anthropic/claude-opus-4-6)
|
|
216
218
|
--no-think with --duplex: disable the Think tier (Act handles everything)
|
|
217
219
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
@@ -243,7 +245,7 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
|
|
|
243
245
|
Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
|
|
244
246
|
|
|
245
247
|
REPL shortcuts: !<cmd> runs a shell command inline · #<note> saves a memory · @path inlines a file
|
|
246
|
-
REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /voice-model /think-model)
|
|
248
|
+
REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /voice /voice-model /think-model)
|
|
247
249
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu — ↑/↓ select, ⏎/Tab accept, Esc dismiss.
|
|
248
250
|
REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
|
|
249
251
|
REPL shortcuts: Shift+Tab cycles permission posture (ask → accept-edits → plan) · Alt+T toggles reasoning · Alt+P switches model · Ctrl+O toggles verbose tool output · → or Tab accepts the dim history ghost-suggestion · Alt+S/Ctrl+S stash/unstash.
|
|
@@ -707,6 +709,7 @@ function optsFor(args: Args, ai: ChatLike, cfg: Partial<AgentConfig> = {}, extra
|
|
|
707
709
|
boddb: args.boddb,
|
|
708
710
|
seed: args.seed,
|
|
709
711
|
realShell: args.shell, // undefined → core.ts defaults (on for disk, off for sandbox/boddb)
|
|
712
|
+
scratch: args.scratch,
|
|
710
713
|
appendSystemPrompt: args.appendSystemPrompt,
|
|
711
714
|
addDirs: args.addDirs,
|
|
712
715
|
stream: args.stream,
|
|
@@ -1044,6 +1047,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1044
1047
|
const duplex = args.duplex;
|
|
1045
1048
|
let dx: DuplexAgent | undefined;
|
|
1046
1049
|
let voiceIO: VoiceIO | undefined; // real voice I/O (--voice + keys): mic→STT in, text_delta→TTS out
|
|
1050
|
+
let toggleVoice: (() => Promise<void>) | undefined; // bound below (duplex + TTY): /voice flips mic on/off live
|
|
1047
1051
|
let editorRef: LineEditor | undefined; // bound once the line editor exists — async chrome repaints the prompt via it
|
|
1048
1052
|
// During a turn the user's type-ahead lives on a "stash ›" line (no active editor to own it). Async
|
|
1049
1053
|
// chrome (streamed deltas, task events) lands on top of it — repaint the stash below, so it survives.
|
|
@@ -1099,7 +1103,10 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1099
1103
|
if (duplex) {
|
|
1100
1104
|
// Workers must not stream into the voice channel — strip the host/stream seam (display hooks stay:
|
|
1101
1105
|
// worker tool activity prints as dim stderr chrome). `signal` is per-task (duplex.ts owns it).
|
|
1102
|
-
|
|
1106
|
+
// Drop providerOptions too: they're computed for the MAIN agent's model (e.g. cursor's cwd/cursorSession)
|
|
1107
|
+
// and must never leak onto a worker whose model differs — DuplexAgent recomputes them per worker model
|
|
1108
|
+
// via providerOptionsFor below. (A cursor cursorSession leaking onto an anthropic worker is a hard 400.)
|
|
1109
|
+
const { host: _host, stream: _stream, signal: _signal, providerOptions: _po, ...wo } = agent.options;
|
|
1103
1110
|
workerOptions = wo as AgentOptions;
|
|
1104
1111
|
if (workerOptions.permissions)
|
|
1105
1112
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: undefined, ask: duplexAsk });
|
|
@@ -1194,6 +1201,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1194
1201
|
...((args.voiceModel ?? cfg.reflexModel) ? { reflexModel: resolveModelOrNewest((args.voiceModel ?? cfg.reflexModel)!) } : {}),
|
|
1195
1202
|
actModel: agent.options.model,
|
|
1196
1203
|
actOptions: workerOptions,
|
|
1204
|
+
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
1197
1205
|
...((args.thinkModel ?? cfg.thinkModel) !== undefined ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {}),
|
|
1198
1206
|
host,
|
|
1199
1207
|
...(args.voice ? { voiceStyle: 'conversational' as const, progressUpdates: true, askRelay: true } : {}), // voice: progress asides + worker questions relayed through the conversation
|
|
@@ -1278,11 +1286,22 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1278
1286
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
1279
1287
|
return img ? { display: 'Image', ref: '@' + img.path, path: img.path } : null;
|
|
1280
1288
|
};
|
|
1281
|
-
//
|
|
1289
|
+
// Hard teardown — used by the SIGINT path and the keypress force-quit escalation. Never let the
|
|
1290
|
+
// user get trapped: even if a turn's abort() fails to unwind (wedged stream/tool), this exits.
|
|
1291
|
+
const forceQuit = (code = 130) => {
|
|
1292
|
+
try { voiceIO?.stop(); } catch { /* mic/player may already be down */ }
|
|
1293
|
+
try { disposeCursorSessions(); } catch { /* reap warm cursor helpers, best-effort */ }
|
|
1294
|
+
void closeMcp(mounted);
|
|
1295
|
+
process.exit(code);
|
|
1296
|
+
};
|
|
1297
|
+
// Ctrl-C: cancel the in-flight turn if one's running; a second Ctrl-C while already cancelling
|
|
1298
|
+
// (i.e. the abort didn't unwind) force-quits. No active turn → clean up MCP children and exit.
|
|
1282
1299
|
process.on('SIGINT', () => {
|
|
1283
|
-
if (activeTurn) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1300
|
+
if (activeTurn) {
|
|
1301
|
+
if (aborting) { err(red('\n ⏻ force-quit\n')); forceQuit(); return; } // already cancelling → hard escape
|
|
1302
|
+
activeTurn.abort(); voiceIO?.interrupt(); return;
|
|
1303
|
+
}
|
|
1304
|
+
forceQuit();
|
|
1286
1305
|
});
|
|
1287
1306
|
installCancelGuards(mounted);
|
|
1288
1307
|
const store = new SessionStore(cwd);
|
|
@@ -1600,6 +1619,12 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1600
1619
|
if (a[0] === 'full' || a[0] === 'minimal') { workerChrome = a[0]; err(green(` ✓ worker chrome → ${a[0]}\n`)); return; }
|
|
1601
1620
|
err(dim(` worker chrome: ${workerChrome} (use /workers full|minimal)\n`));
|
|
1602
1621
|
},
|
|
1622
|
+
}, voice: {
|
|
1623
|
+
desc: 'toggle live voice I/O on/off mid-session (needs SONIOX/CARTESIA keys + a TTY)',
|
|
1624
|
+
run: async () => {
|
|
1625
|
+
if (!toggleVoice) { err(dim(' (voice needs --duplex on a TTY)\n')); return; }
|
|
1626
|
+
await toggleVoice();
|
|
1627
|
+
},
|
|
1603
1628
|
}, 'voice-model': {
|
|
1604
1629
|
desc: 'switch the reflex (voice) model — /voice-model <id>, or alone for a picker',
|
|
1605
1630
|
run: async (a: string[]) => {
|
|
@@ -1749,7 +1774,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1749
1774
|
commands: { desc: 'pick a custom slash command to run (./.agent/commands)', run: () => pickAndRun('command') },
|
|
1750
1775
|
skills: { desc: 'pick a skill to run (./.agent/skills)', run: () => pickAndRun('skill') },
|
|
1751
1776
|
mcp: {
|
|
1752
|
-
desc: 'manage MCP servers — /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [resources [name]]',
|
|
1777
|
+
desc: 'manage MCP servers — /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [tools [name]] [resources [name]]',
|
|
1753
1778
|
run: async (a) => {
|
|
1754
1779
|
const sub = a[0]?.toLowerCase();
|
|
1755
1780
|
if (sub === 'login') {
|
|
@@ -1793,6 +1818,16 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1793
1818
|
err(dim(` removed "${name}"\n`));
|
|
1794
1819
|
return;
|
|
1795
1820
|
}
|
|
1821
|
+
if (sub === 'tools') {
|
|
1822
|
+
const filter = a[1];
|
|
1823
|
+
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
1824
|
+
if (!targets.length) { err(dim(` (no ${filter ? `MCP server "${filter}"` : 'MCP servers'} found)\n`)); return; }
|
|
1825
|
+
for (const m of targets) {
|
|
1826
|
+
err(` ${cyan(m.name)} ${dim(`(${m.tools.length} tools)`)}\n`);
|
|
1827
|
+
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, '')}${t.description ? ' — ' + t.description.split('\n')[0] : ''}\n`));
|
|
1828
|
+
}
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1796
1831
|
if (sub === 'resources') {
|
|
1797
1832
|
const filter = a[1];
|
|
1798
1833
|
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
@@ -1812,14 +1847,15 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1812
1847
|
if (!mounted.length) { err(dim(' (no MCP servers mounted)\n')); return; }
|
|
1813
1848
|
for (const m of mounted) {
|
|
1814
1849
|
const ver = m.serverInfo?.name ? dim(` · ${m.serverInfo.name}${m.serverInfo.version ? ' v' + m.serverInfo.version : ''}`) : '';
|
|
1815
|
-
err(` ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}\n`);
|
|
1816
|
-
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, '')}${t.description ? ' — ' + t.description.split('\n')[0] : ''}\n`));
|
|
1850
|
+
err(` ${green('✓')} ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}\n`);
|
|
1817
1851
|
}
|
|
1852
|
+
err(dim(' /mcp tools [name] to list tools · /mcp resources [name] for resources\n'));
|
|
1818
1853
|
};
|
|
1819
1854
|
const items: SelectItem[] = [
|
|
1820
1855
|
{ label: 'list', value: 'list', desc: `show mounted servers (${mounted.length})` },
|
|
1821
1856
|
{ label: 'add', value: 'add', desc: 'mount a new MCP server' },
|
|
1822
1857
|
...(mounted.length ? [
|
|
1858
|
+
{ label: 'tools', value: 'tools', desc: 'list a server\'s tools' },
|
|
1823
1859
|
{ label: 'remove', value: 'remove', desc: 'unmount an MCP server' },
|
|
1824
1860
|
{ label: 'resources', value: 'resources', desc: 'list server resources' },
|
|
1825
1861
|
] : []),
|
|
@@ -1841,12 +1877,12 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1841
1877
|
const rv = await selectMenu(process.stderr, { title: 'remove server', items: mounted.map((m) => ({ label: m.name, value: m.name })) });
|
|
1842
1878
|
if (!rv) return;
|
|
1843
1879
|
a = ['remove', rv];
|
|
1844
|
-
} else if (picked === 'resources') {
|
|
1845
|
-
if (mounted.length === 1) { a = [
|
|
1880
|
+
} else if (picked === 'tools' || picked === 'resources') {
|
|
1881
|
+
if (mounted.length === 1) { a = [picked, mounted[0].name]; }
|
|
1846
1882
|
else {
|
|
1847
|
-
const rv = await selectMenu(process.stderr, { title:
|
|
1883
|
+
const rv = await selectMenu(process.stderr, { title: `server ${picked}`, items: [{ label: '(all)', value: '' }, ...mounted.map((m) => ({ label: m.name, value: m.name }))] });
|
|
1848
1884
|
if (rv === null) return;
|
|
1849
|
-
a = rv ? [
|
|
1885
|
+
a = rv ? [picked, rv] : [picked];
|
|
1850
1886
|
}
|
|
1851
1887
|
}
|
|
1852
1888
|
// fall through to re-run with the assembled args
|
|
@@ -1953,8 +1989,16 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1953
1989
|
const cancel = k === 'escape' || (key?.ctrl && k === 'c');
|
|
1954
1990
|
if (cancel) {
|
|
1955
1991
|
if (stashBuf) { stashBuf = ''; err('\r\x1b[K'); return; } // first Esc clears the stash line
|
|
1956
|
-
if (!aborting) {
|
|
1957
|
-
|
|
1992
|
+
if (!aborting) {
|
|
1993
|
+
aborting = true; activeTurn.abort(); voiceIO?.interrupt();
|
|
1994
|
+
err(yellow('\n ⎋ cancelling…') + dim(' (Ctrl-C again to force-quit)\n'));
|
|
1995
|
+
// Watchdog: if the turn hasn't unwound shortly, the abort is wedged — surface the escape hatch.
|
|
1996
|
+
setTimeout(() => {
|
|
1997
|
+
if (activeTurn) err(red(' ⚠ still cancelling — press Ctrl-C to force-quit\n'));
|
|
1998
|
+
}, 4000).unref?.();
|
|
1999
|
+
} else if (key?.ctrl && k === 'c') {
|
|
2000
|
+
err(red('\n ⏻ force-quit\n')); forceQuit(); // already cancelling + another Ctrl-C → never trapped
|
|
2001
|
+
} else if (k === 'escape' && !pendingRewind) { pendingRewind = true; err(dim(' ⎋⎋ jumping back to edit…\n')); }
|
|
1958
2002
|
return;
|
|
1959
2003
|
}
|
|
1960
2004
|
// Stash input: type ahead while the turn runs
|
|
@@ -2028,11 +2072,17 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2028
2072
|
// spoken via the host tap above. Missing keys → conversational text mode, one-line note.
|
|
2029
2073
|
let voicePartial = ''; // live partial transcript, rendered in the prompt footer
|
|
2030
2074
|
let partialRedraw: ReturnType<typeof setTimeout> | null = null;
|
|
2031
|
-
|
|
2075
|
+
// Spin VoiceIO up live (launch with --voice, or /voice mid-session). `greet` opens with a spoken
|
|
2076
|
+
// greeting turn (launch only); a manual toggle just turns the mic on quietly. Returns true if voice
|
|
2077
|
+
// is now live. Duplex + TTY only — bound to `toggleVoice` below so /voice can flip it off again.
|
|
2078
|
+
const startVoice = async (greet: boolean): Promise<boolean> => {
|
|
2079
|
+
if (voiceIO) return true;
|
|
2080
|
+
if (!duplex || !process.stdin.isTTY) { err(dim(' (voice needs --duplex on a TTY)\n')); return false; }
|
|
2032
2081
|
if (!VoiceIO.available()) {
|
|
2033
2082
|
err(dim(' (voice I/O off — set SONIOX_API_KEY, CARTESIA_API_KEY, CARTESIA_VOICE_ID to talk)\n'));
|
|
2034
|
-
|
|
2035
|
-
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
voiceIO = new VoiceIO({
|
|
2036
2086
|
// No ack phrase by default: a fixed "Mm-hm," every turn reads robotic, Haiku's TTFT doesn't
|
|
2037
2087
|
// need masking (~0.7-1.2s full turns), and the conversational register already opens with a
|
|
2038
2088
|
// natural reaction. The mechanism (+ echo-leak guard) stays for slower voice models.
|
|
@@ -2060,14 +2110,11 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2060
2110
|
err(`\r\x1b[K ${bold(cyan('🎤 ›'))} ${text}\n`);
|
|
2061
2111
|
void dispatchLine(text + note).then(async (r) => { if (r === 'quit') { await voiceIO?.awaitIdle(); editorRef?.abort(); } }).finally(() => editorRef?.redrawNow());
|
|
2062
2112
|
},
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
// mic/player children outlive the CLI and hold the microphone (verified leak in PTY testing).
|
|
2069
|
-
for (const sig of ['SIGHUP', 'SIGTERM'] as const) process.on(sig, () => { voiceIO?.stop(); process.exit(0); });
|
|
2070
|
-
err(dim(` 🎤 voice on (${voiceIO.usingAec ? 'echo-cancelled' : 'heuristic echo — headphones recommended'}) — just talk; speak over it to interrupt\n`));
|
|
2113
|
+
});
|
|
2114
|
+
try {
|
|
2115
|
+
await voiceIO.start();
|
|
2116
|
+
err(dim(` 🎤 voice on (${voiceIO.usingAec ? 'echo-cancelled' : 'heuristic echo — headphones recommended'}) — just talk; speak over it to interrupt\n`));
|
|
2117
|
+
if (greet) {
|
|
2071
2118
|
// Greeting: the agent makes the first turn — spoken, personalized from what it can see.
|
|
2072
2119
|
// Straight to turn() (not dispatchLine): the synthetic prompt must not enter ↑-history.
|
|
2073
2120
|
const where = cwd.split('/').pop();
|
|
@@ -2078,12 +2125,30 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2078
2125
|
`Context: working directory "${where}"${resumed ? '; this resumes an earlier conversation — glance at it and pick up naturally' : ''}. ` +
|
|
2079
2126
|
`Personalize from whatever you learned (memory, prior conversation). Then ask what they'd like to do.`,
|
|
2080
2127
|
).finally(() => editorRef?.redrawNow());
|
|
2081
|
-
} catch (e: any) {
|
|
2082
|
-
err(yellow(` ⚠ voice I/O failed to start: ${e?.message ?? e} — continuing text-only\n`));
|
|
2083
|
-
voiceIO = undefined;
|
|
2084
2128
|
}
|
|
2129
|
+
return true;
|
|
2130
|
+
} catch (e: any) {
|
|
2131
|
+
err(yellow(` ⚠ voice I/O failed to start: ${e?.message ?? e} — continuing text-only\n`));
|
|
2132
|
+
voiceIO = undefined;
|
|
2133
|
+
return false;
|
|
2085
2134
|
}
|
|
2135
|
+
};
|
|
2136
|
+
// Child cleanup, registered ONCE (not per start — toggling on/off must not stack listeners). They
|
|
2137
|
+
// close over the live `voiceIO`, so they cover whichever instance is up. SIGHUP/SIGTERM (terminal
|
|
2138
|
+
// closed, kill) bypass 'exit' handlers by default — without these the mic/player children outlive
|
|
2139
|
+
// the CLI and hold the microphone (verified leak in PTY testing).
|
|
2140
|
+
if (duplex && process.stdin.isTTY) {
|
|
2141
|
+
process.on('exit', () => voiceIO?.stop());
|
|
2142
|
+
for (const sig of ['SIGHUP', 'SIGTERM'] as const) process.on(sig, () => { voiceIO?.stop(); process.exit(0); });
|
|
2086
2143
|
}
|
|
2144
|
+
// /voice toggle: flip the mic on or off without leaving the session (kills STT/TTS children on off).
|
|
2145
|
+
if (duplex && process.stdin.isTTY) toggleVoice = async () => {
|
|
2146
|
+
if (voiceIO) { voiceIO.stop(); voiceIO = undefined; voicePartial = ''; err(dim(' 🔇 voice off\n')); editorRef?.redrawNow(); return; }
|
|
2147
|
+
await startVoice(false);
|
|
2148
|
+
editorRef?.redrawNow();
|
|
2149
|
+
};
|
|
2150
|
+
// Launch with --voice: start now, with the spoken greeting.
|
|
2151
|
+
if (args.voice && duplex && process.stdin.isTTY) await startVoice(true);
|
|
2087
2152
|
|
|
2088
2153
|
while (true) {
|
|
2089
2154
|
// Double-Esc fired during the just-finished turn → open the jump-back picker now (turn has unwound).
|
|
@@ -2193,6 +2258,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2193
2258
|
// (voice children / sockets / MCP handles can keep the process alive otherwise).
|
|
2194
2259
|
voiceIO?.stop();
|
|
2195
2260
|
releaseStdin();
|
|
2261
|
+
disposeCursorSessions();
|
|
2196
2262
|
await closeMcp(mounted);
|
|
2197
2263
|
process.exit(130);
|
|
2198
2264
|
}
|
|
@@ -2201,6 +2267,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
2201
2267
|
}
|
|
2202
2268
|
}
|
|
2203
2269
|
releaseStdin();
|
|
2270
|
+
disposeCursorSessions(); // reap any warm cursor helper (its stdout pipe would otherwise keep the loop alive → hang on exit)
|
|
2204
2271
|
await closeMcp(mounted);
|
|
2205
2272
|
}
|
|
2206
2273
|
|
|
@@ -228,6 +228,17 @@ declare class AgentOptions {
|
|
|
228
228
|
/** Token-aware backstop (~4 chars/token estimate). After note-taking, drop oldest messages from the
|
|
229
229
|
* sent context until the estimate is under this ceiling (pairing-safe). 0 = off. */
|
|
230
230
|
maxContextTokens: number;
|
|
231
|
+
/** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
|
|
232
|
+
* a marker telling the model it was cropped (refine the query, or page further). Guards against one
|
|
233
|
+
* Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
|
|
234
|
+
maxToolResultBytes: number;
|
|
235
|
+
/** Hook to handle an oversized tool result instead of the default lossy crop: receives the FULL output
|
|
236
|
+
* and returns the (cropped) string to put in context — e.g. spill to scratch and return a recoverable,
|
|
237
|
+
* paginated stub. Called only when a result exceeds `maxToolResultBytes`. */
|
|
238
|
+
capToolResult?: (full: string, info: {
|
|
239
|
+
tool: string;
|
|
240
|
+
args: any;
|
|
241
|
+
}) => string | Promise<string>;
|
|
231
242
|
/** VFS dir(s) of skills (`<dir>/<id>/SKILL.md`). If set: inject a catalog + add the `Skill` tool. Multiple dirs are merged (first wins on name collisions). */
|
|
232
243
|
skillsDir?: string | string[];
|
|
233
244
|
/** VFS dir(s) of slash-command templates (`<dir>/<name>.md`). If set: inject a catalog + add the `SlashCommand` tool. Multiple dirs are merged (first wins). */
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { h as RunResult, R as ReasoningEffort } from './Agent-
|
|
2
|
+
import { h as RunResult, R as ReasoningEffort } from './Agent-uWtu_WFY.js';
|
|
3
3
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
4
4
|
import { M as Message, c as ContentPart } from './tools-GPWp7oXq.js';
|
|
5
5
|
|
|
@@ -89,6 +89,7 @@ interface Args {
|
|
|
89
89
|
addDirs?: string[];
|
|
90
90
|
print?: boolean;
|
|
91
91
|
debug?: boolean;
|
|
92
|
+
scratch?: boolean;
|
|
92
93
|
}
|
|
93
94
|
declare function parseArgs(argv: string[]): Args;
|
|
94
95
|
/** Render a resumed conversation (like CC) so the user sees the context they're continuing: user
|