agent.libx.js 0.89.5 → 0.89.7
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/README.md +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +533 -135
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +84 -5
- package/dist/index.js +201 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1397,7 +1397,7 @@ function makeRealShellTool(options) {
|
|
|
1397
1397
|
proc.stderr?.on("data", collect);
|
|
1398
1398
|
proc.on("error", (err2) => {
|
|
1399
1399
|
if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
1400
|
-
|
|
1400
|
+
log8.debug("shell spawn error", err2);
|
|
1401
1401
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
|
|
1402
1402
|
});
|
|
1403
1403
|
proc.on("close", (code) => {
|
|
@@ -1457,14 +1457,14 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
1457
1457
|
}
|
|
1458
1458
|
];
|
|
1459
1459
|
}
|
|
1460
|
-
var
|
|
1460
|
+
var log8, clean, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
|
|
1461
1461
|
var init_tools_shell = __esm({
|
|
1462
1462
|
"src/tools.shell.ts"() {
|
|
1463
1463
|
"use strict";
|
|
1464
1464
|
init_tools();
|
|
1465
1465
|
init_redact();
|
|
1466
1466
|
init_logging();
|
|
1467
|
-
|
|
1467
|
+
log8 = forComponent("shell");
|
|
1468
1468
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
1469
1469
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
1470
1470
|
ShellJobRegistry = class {
|
|
@@ -3348,6 +3348,202 @@ function digestRun(messages, maxChars) {
|
|
|
3348
3348
|
return out.length > maxChars ? out.slice(0, maxChars) + "\n\u2026 (truncated)" : out;
|
|
3349
3349
|
}
|
|
3350
3350
|
|
|
3351
|
+
// src/duplex.ts
|
|
3352
|
+
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
3353
|
+
init_logging();
|
|
3354
|
+
var log6 = forComponent("DuplexAgent");
|
|
3355
|
+
var DuplexAgentOptions = class {
|
|
3356
|
+
/** Any ai.libx.js AIClient — shared by the voice and worker agents (routed by model). */
|
|
3357
|
+
ai;
|
|
3358
|
+
/** The WORKER's filesystem. If omitted the worker keeps Agent's jailed-disk-at-cwd default. */
|
|
3359
|
+
fs;
|
|
3360
|
+
voiceModel = "anthropic/claude-haiku-4-5";
|
|
3361
|
+
workerModel = "anthropic/claude-sonnet-4-6";
|
|
3362
|
+
/** Escape hatches merged over the derived per-agent options. */
|
|
3363
|
+
voiceOptions;
|
|
3364
|
+
workerOptions;
|
|
3365
|
+
/** Receives the voice text_delta stream + task lifecycle events. */
|
|
3366
|
+
host;
|
|
3367
|
+
/** How many recent transcript messages are rendered into a worker's brief. */
|
|
3368
|
+
excerptTurns = 6;
|
|
3369
|
+
/** Voice register: 'neutral' = clean spoken style; 'conversational' = human-like — fillers,
|
|
3370
|
+
* backchannels, impulsive first reactions before content (mimics real duplex conversation). */
|
|
3371
|
+
voiceStyle = "neutral";
|
|
3372
|
+
/** Awaited BEFORE a delegated worker spawns — open a per-task checkpoint frame, audit, etc.
|
|
3373
|
+
* (post-spawn would race the worker's first edits). */
|
|
3374
|
+
onTaskStart;
|
|
3375
|
+
};
|
|
3376
|
+
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou work in a pair: you talk, and a background worker with FULL access to the user\'s environment (files, shell, web) does the hands-on work. You can find out or do ANYTHING by calling `Delegate` with a clear, self-contained brief \u2014 so NEVER tell the user you can\'t see, access, or do something. Delegate and find out. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), delegate IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nAfter calling Delegate, tell the user you are on it in one short sentence, then end your turn. Do not wait for the result.\nResults arrive later as events like "[task t1 completed] \u2026" or "[task t1 failed] \u2026". When one arrives, summarize it for the ear in one or two short sentences. Never read raw file paths, diffs, or code aloud verbatim.\nDo not fire a second Delegate for work already in flight \u2014 check `TaskStatus` first. Use `CancelTask` when the user asks to stop something.';
|
|
3377
|
+
var VOICE_STYLE_CONVERSATIONAL = `Speak like a person in a live conversation, not an assistant reading a script. React first, then deliver: a quick impulsive beat ("oh nice", "hmm, hold on", "ah, got it") before the substance. Use contractions always. Vary sentence length \u2014 some very short. Light fillers and backchannels are fine ("mm-hm", "right", "let's see") but at most one per reply \u2014 never stack them. When you delegate, say it like a human would ("hang on, let me actually dig into that \u2014 gimme a minute") instead of announcing a task. When a result comes back, react to it like you just found out ("okay so \u2014 turns out\u2026"). Match the user's energy: a quick question gets a quick answer \u2014 a few words is a perfectly good turn. Prefer a short answer plus an offer ("want the details?") over covering everything. Never narrate your own mechanics (no "I will now delegate", no task ids out loud).`;
|
|
3378
|
+
var DuplexAgent = class {
|
|
3379
|
+
options;
|
|
3380
|
+
voice;
|
|
3381
|
+
tasks = /* @__PURE__ */ new Map();
|
|
3382
|
+
queue = Promise.resolve();
|
|
3383
|
+
seq = 0;
|
|
3384
|
+
pendingEvents = [];
|
|
3385
|
+
flushQueued = false;
|
|
3386
|
+
constructor(options) {
|
|
3387
|
+
this.options = { ...new DuplexAgentOptions(), ...options };
|
|
3388
|
+
const o = this.options;
|
|
3389
|
+
this.voice = new Agent({
|
|
3390
|
+
ai: o.ai,
|
|
3391
|
+
fs: new MemFilesystem2(),
|
|
3392
|
+
// scratch — NOT Agent's jailed-disk default (voice has no fs tools; edge-safe)
|
|
3393
|
+
model: o.voiceModel,
|
|
3394
|
+
stream: true,
|
|
3395
|
+
host: o.host,
|
|
3396
|
+
systemPrompt: VOICE_SYSTEM_PROMPT + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : ""),
|
|
3397
|
+
instructionFiles: false,
|
|
3398
|
+
maxSteps: 8,
|
|
3399
|
+
// a voice turn should never loop
|
|
3400
|
+
timeoutMs: 3e4,
|
|
3401
|
+
...o.voiceOptions,
|
|
3402
|
+
// no defaultTools() — the voice can only Delegate, never touch files itself. Set AFTER the
|
|
3403
|
+
// voiceOptions spread (addTools() would be clobbered by the first prepare()); extra voice
|
|
3404
|
+
// tools come in via voiceOptions.tools and are merged here.
|
|
3405
|
+
tools: [...o.voiceOptions?.tools ?? [], this.delegateTool(), this.taskStatusTool(), this.cancelTaskTool()]
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
/** One user turn: the voice agent streams the reply (and may Delegate). Serialized with re-voice turns. */
|
|
3409
|
+
send(content) {
|
|
3410
|
+
return this.enqueue(() => this.voice.send(content));
|
|
3411
|
+
}
|
|
3412
|
+
/** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
|
|
3413
|
+
async idle() {
|
|
3414
|
+
while (true) {
|
|
3415
|
+
const q2 = this.queue;
|
|
3416
|
+
await q2.catch(() => {
|
|
3417
|
+
});
|
|
3418
|
+
await Promise.all([...this.tasks.values()].map((t) => t.promise));
|
|
3419
|
+
if (this.queue === q2 && ![...this.tasks.values()].some((t) => t.status === "running")) return;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
/** Promise-chain mutex: turns run strictly one at a time; a failed turn doesn't poison the chain. */
|
|
3423
|
+
enqueue(fn) {
|
|
3424
|
+
const run = this.queue.then(fn, fn);
|
|
3425
|
+
this.queue = run.then(() => {
|
|
3426
|
+
}, () => {
|
|
3427
|
+
});
|
|
3428
|
+
return run;
|
|
3429
|
+
}
|
|
3430
|
+
notify(kind, message, data) {
|
|
3431
|
+
this.options.host?.notify?.({ kind, message, data });
|
|
3432
|
+
}
|
|
3433
|
+
/** Queue a `[task …]` event for re-voicing. Events arriving while the voice is busy coalesce into ONE turn. */
|
|
3434
|
+
queueRevoice(event) {
|
|
3435
|
+
this.pendingEvents.push(event);
|
|
3436
|
+
if (this.flushQueued) return;
|
|
3437
|
+
this.flushQueued = true;
|
|
3438
|
+
void this.enqueue(async () => {
|
|
3439
|
+
this.flushQueued = false;
|
|
3440
|
+
const events = this.pendingEvents.splice(0);
|
|
3441
|
+
if (!events.length) return;
|
|
3442
|
+
await this.voice.send(events.join("\n"));
|
|
3443
|
+
this.notify("revoice_done", "");
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
/** The worker's brief: the Delegate args + a STATIC text snapshot of the recent conversation. */
|
|
3447
|
+
buildBrief(brief) {
|
|
3448
|
+
const recent = this.voice.transcript.filter((m) => (m.role === "user" || m.role === "assistant") && contentText(m.content).trim()).slice(-this.options.excerptTurns).map((m) => `${m.role}: ${contentText(m.content)}`).join("\n");
|
|
3449
|
+
return recent ? `${brief}
|
|
3450
|
+
|
|
3451
|
+
## Recent conversation (for context)
|
|
3452
|
+
${recent}` : brief;
|
|
3453
|
+
}
|
|
3454
|
+
/** Spawn a detached worker for task `id`; its settlement notifies + enqueues the re-voice turn. */
|
|
3455
|
+
spawnWorker(id, label, briefText) {
|
|
3456
|
+
const o = this.options;
|
|
3457
|
+
const controller = new AbortController();
|
|
3458
|
+
const worker = new Agent({
|
|
3459
|
+
ai: o.ai,
|
|
3460
|
+
fs: o.fs,
|
|
3461
|
+
model: o.workerModel,
|
|
3462
|
+
...o.workerOptions,
|
|
3463
|
+
// may override ai/fs/model/tools/… —
|
|
3464
|
+
signal: controller.signal
|
|
3465
|
+
// …but never the per-task cancellation signal
|
|
3466
|
+
});
|
|
3467
|
+
const promise = worker.run(briefText).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
|
|
3468
|
+
this.tasks.set(id, { id, label, status: "running", controller, promise });
|
|
3469
|
+
}
|
|
3470
|
+
onWorkerSettled(id, res) {
|
|
3471
|
+
const rec = this.tasks.get(id);
|
|
3472
|
+
if (res.finishReason === "aborted" || rec.status === "cancelled") {
|
|
3473
|
+
rec.status = "cancelled";
|
|
3474
|
+
this.notify("task_cancelled", `task ${id} (${rec.label}) cancelled`);
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
if (res.finishReason === "error") {
|
|
3478
|
+
const msg = res.error instanceof Error ? res.error.message : String(res.error ?? "unknown error");
|
|
3479
|
+
return this.failTask(rec, msg);
|
|
3480
|
+
}
|
|
3481
|
+
rec.status = "done";
|
|
3482
|
+
log6.verbose(`task ${id} done (${res.steps} steps)`);
|
|
3483
|
+
this.notify("task_done", `task ${id} (${rec.label}) completed`, { id, text: res.text, usage: res.usage, usageEstimated: res.usageEstimated });
|
|
3484
|
+
this.queueRevoice(`[task ${id} completed] ${res.text}`);
|
|
3485
|
+
}
|
|
3486
|
+
onWorkerFailed(id, err2) {
|
|
3487
|
+
this.failTask(this.tasks.get(id), err2 instanceof Error ? err2.message : String(err2));
|
|
3488
|
+
}
|
|
3489
|
+
failTask(rec, msg) {
|
|
3490
|
+
rec.status = "error";
|
|
3491
|
+
log6.warn(`task ${rec.id} failed: ${msg}`);
|
|
3492
|
+
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
3493
|
+
this.queueRevoice(`[task ${rec.id} failed] ${msg}`);
|
|
3494
|
+
}
|
|
3495
|
+
// --- the three voice tools (closures over this instance) ---
|
|
3496
|
+
delegateTool() {
|
|
3497
|
+
return {
|
|
3498
|
+
name: "Delegate",
|
|
3499
|
+
description: 'Escalate real work (reading/editing files, searching, running tasks, deep analysis) to a background worker agent. Returns immediately with a task id; the result arrives later as a "[task <id> completed]" event. Provide a clear, self-contained `brief` (the worker does not hear the live conversation).',
|
|
3500
|
+
parameters: {
|
|
3501
|
+
type: "object",
|
|
3502
|
+
required: ["brief"],
|
|
3503
|
+
properties: {
|
|
3504
|
+
brief: { type: "string", description: "full, self-contained instructions for the worker" },
|
|
3505
|
+
label: { type: "string", description: "a short (2-4 word) label for the task" }
|
|
3506
|
+
}
|
|
3507
|
+
},
|
|
3508
|
+
run: async ({ brief, label }) => {
|
|
3509
|
+
const id = `t${++this.seq}`;
|
|
3510
|
+
const lbl = String(label ?? "task");
|
|
3511
|
+
await this.options.onTaskStart?.(id, lbl);
|
|
3512
|
+
this.spawnWorker(id, lbl, this.buildBrief(String(brief ?? "")));
|
|
3513
|
+
this.notify("task_started", `task ${id} (${lbl}) started`, { id, brief });
|
|
3514
|
+
return `Delegated as task ${id}. Acknowledge briefly; the result will arrive as a [task ${id} completed] event.`;
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
taskStatusTool() {
|
|
3519
|
+
return {
|
|
3520
|
+
name: "TaskStatus",
|
|
3521
|
+
description: "Status of background tasks. Pass `id` for one task, or omit it to list all.",
|
|
3522
|
+
parameters: { type: "object", properties: { id: { type: "string" } } },
|
|
3523
|
+
run: async ({ id }) => {
|
|
3524
|
+
const list = id ? [this.tasks.get(String(id))].filter(Boolean) : [...this.tasks.values()];
|
|
3525
|
+
if (!list.length) return id ? `No task '${id}'.` : "No background tasks.";
|
|
3526
|
+
return list.map((t) => `${t.id} (${t.label}): ${t.status}`).join("\n");
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
cancelTaskTool() {
|
|
3531
|
+
return {
|
|
3532
|
+
name: "CancelTask",
|
|
3533
|
+
description: "Cancel a running background task by id.",
|
|
3534
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
3535
|
+
run: async ({ id }) => {
|
|
3536
|
+
const rec = this.tasks.get(String(id));
|
|
3537
|
+
if (!rec) return `No task '${id}'.`;
|
|
3538
|
+
if (rec.status !== "running") return `Task ${rec.id} is already ${rec.status}.`;
|
|
3539
|
+
rec.status = "cancelled";
|
|
3540
|
+
rec.controller.abort();
|
|
3541
|
+
return `Task ${rec.id} (${rec.label}) cancelled.`;
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
|
|
3351
3547
|
// src/mcp.ts
|
|
3352
3548
|
function toText(result) {
|
|
3353
3549
|
if (result == null) return "";
|
|
@@ -3375,12 +3571,12 @@ function mcpToolsToAgentTools(specs, callTool, prefix = "mcp__", filter) {
|
|
|
3375
3571
|
|
|
3376
3572
|
// src/index.ts
|
|
3377
3573
|
init_logging();
|
|
3378
|
-
import { MemFilesystem as
|
|
3574
|
+
import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
|
|
3379
3575
|
|
|
3380
3576
|
// src/mcp.client.ts
|
|
3381
3577
|
init_logging();
|
|
3382
3578
|
import { spawn } from "child_process";
|
|
3383
|
-
var
|
|
3579
|
+
var log7 = forComponent("mcp");
|
|
3384
3580
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
3385
3581
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
3386
3582
|
var StdioTransport = class {
|
|
@@ -3399,7 +3595,7 @@ var StdioTransport = class {
|
|
|
3399
3595
|
proc.stdout.setEncoding("utf8");
|
|
3400
3596
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
3401
3597
|
proc.stderr.setEncoding("utf8");
|
|
3402
|
-
proc.stderr.on("data", (chunk) =>
|
|
3598
|
+
proc.stderr.on("data", (chunk) => log7.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
3403
3599
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
3404
3600
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
3405
3601
|
}
|
|
@@ -3413,7 +3609,7 @@ var StdioTransport = class {
|
|
|
3413
3609
|
try {
|
|
3414
3610
|
this.dispatch(JSON.parse(line));
|
|
3415
3611
|
} catch (e) {
|
|
3416
|
-
|
|
3612
|
+
log7.debug("dropping non-JSON line from MCP server:", line, e);
|
|
3417
3613
|
}
|
|
3418
3614
|
}
|
|
3419
3615
|
}
|
|
@@ -3462,7 +3658,7 @@ var StdioTransport = class {
|
|
|
3462
3658
|
try {
|
|
3463
3659
|
this.proc?.stdin?.end();
|
|
3464
3660
|
} catch (e) {
|
|
3465
|
-
|
|
3661
|
+
log7.debug("stdin end failed", e);
|
|
3466
3662
|
}
|
|
3467
3663
|
this.proc?.kill();
|
|
3468
3664
|
}
|
|
@@ -3531,7 +3727,7 @@ function parseSseResponse(body) {
|
|
|
3531
3727
|
const obj = JSON.parse(trimmed.slice(5).trim());
|
|
3532
3728
|
if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
|
|
3533
3729
|
} catch (e) {
|
|
3534
|
-
|
|
3730
|
+
log7.debug("skipping unparseable SSE data line", e);
|
|
3535
3731
|
}
|
|
3536
3732
|
}
|
|
3537
3733
|
return {};
|
|
@@ -3585,16 +3781,16 @@ async function mountMcpServers(servers = {}) {
|
|
|
3585
3781
|
for (const [name, cfg] of Object.entries(servers)) {
|
|
3586
3782
|
if (!cfg || cfg.disabled) continue;
|
|
3587
3783
|
if (!cfg.command && !cfg.url) {
|
|
3588
|
-
|
|
3784
|
+
log7.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
3589
3785
|
continue;
|
|
3590
3786
|
}
|
|
3591
3787
|
try {
|
|
3592
3788
|
const m = await mountMcpServer(name, cfg);
|
|
3593
3789
|
out.push(m);
|
|
3594
|
-
|
|
3790
|
+
log7.info(`MCP "${name}" mounted \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}`);
|
|
3595
3791
|
} catch (e) {
|
|
3596
|
-
if (e instanceof McpAuthError)
|
|
3597
|
-
else
|
|
3792
|
+
if (e instanceof McpAuthError) log7.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
3793
|
+
else log7.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
3598
3794
|
}
|
|
3599
3795
|
}
|
|
3600
3796
|
return out;
|
|
@@ -3827,6 +4023,21 @@ async function hydrate(from, to, dir = "/") {
|
|
|
3827
4023
|
}
|
|
3828
4024
|
return n;
|
|
3829
4025
|
}
|
|
4026
|
+
function toCursorMcp(servers) {
|
|
4027
|
+
if (!servers) return void 0;
|
|
4028
|
+
const out = {};
|
|
4029
|
+
for (const [name, s] of Object.entries(servers)) {
|
|
4030
|
+
if (!s || s.disabled) continue;
|
|
4031
|
+
if (s.command) {
|
|
4032
|
+
out[name] = { type: "stdio", command: s.command, ...s.args ? { args: s.args } : {}, ...s.env ? { env: s.env } : {}, ...s.cwd ? { cwd: s.cwd } : {} };
|
|
4033
|
+
} else if (s.url) {
|
|
4034
|
+
if (s.auth === "oauth" && !s.bearerToken) continue;
|
|
4035
|
+
const headers = { ...s.headers ?? {}, ...s.bearerToken ? { Authorization: `Bearer ${s.bearerToken}` } : {} };
|
|
4036
|
+
out[name] = { url: s.url, ...Object.keys(headers).length ? { headers } : {} };
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
return Object.keys(out).length ? { mcpServers: out } : void 0;
|
|
4040
|
+
}
|
|
3830
4041
|
async function buildAgent(o) {
|
|
3831
4042
|
if (typeof o.sandbox !== "boolean")
|
|
3832
4043
|
throw new Error(
|
|
@@ -3838,9 +4049,14 @@ async function buildAgent(o) {
|
|
|
3838
4049
|
const jailedDisk = new JailedFilesystem(disk);
|
|
3839
4050
|
jailedDisk.setCwd(cwd);
|
|
3840
4051
|
const virtual = o.sandbox || !!o.boddb;
|
|
4052
|
+
const isCursor = (o.model ?? "").startsWith("cursor/");
|
|
4053
|
+
if (virtual && isCursor)
|
|
4054
|
+
throw new Error(
|
|
4055
|
+
"cursor/* models cannot run in --sandbox/--boddb: the Cursor agent runs its own real-disk tools and bypasses the VFS jail. Use disk mode (default)."
|
|
4056
|
+
);
|
|
3841
4057
|
let fs = jailedDisk;
|
|
3842
4058
|
if (o.sandbox) {
|
|
3843
|
-
const mem = new
|
|
4059
|
+
const mem = new MemFilesystem3();
|
|
3844
4060
|
await mkdirp(mem, cwd);
|
|
3845
4061
|
await hydrate(jailedDisk, mem, cwd);
|
|
3846
4062
|
mem.setCwd(cwd);
|
|
@@ -3899,6 +4115,11 @@ Reference files in them by their mount path (the left side).`;
|
|
|
3899
4115
|
ai: o.ai,
|
|
3900
4116
|
fs,
|
|
3901
4117
|
model: o.model ?? "anthropic/claude-sonnet-4-6",
|
|
4118
|
+
// Anchor cursor to the launch dir (its adapter defaults to TMPDIR otherwise) and forward the
|
|
4119
|
+
// host's MCP servers so the delegated cursor agent runs in the same environment. Gated to cursor:
|
|
4120
|
+
// openai/google adapters Object.assign providerOptions into the request body, so a blanket cwd
|
|
4121
|
+
// would corrupt those calls.
|
|
4122
|
+
...isCursor ? { providerOptions: { cwd, ...toCursorMcp(o.mcpServers) ?? {} } } : {},
|
|
3902
4123
|
...(() => {
|
|
3903
4124
|
const now = /* @__PURE__ */ new Date();
|
|
3904
4125
|
const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
|
|
@@ -3962,6 +4183,7 @@ function summarizeCall(name, args) {
|
|
|
3962
4183
|
if (args.command) return String(args.command);
|
|
3963
4184
|
if (args.pattern) return `/${args.pattern}/${args.glob ? " in " + args.glob : ""}`;
|
|
3964
4185
|
if (args.prompt) return trunc(String(args.description ?? args.prompt), 50);
|
|
4186
|
+
if (args.brief) return trunc(String(args.label ? `${args.label}: ${args.brief}` : args.brief), 70);
|
|
3965
4187
|
return trunc(JSON.stringify(args), 60);
|
|
3966
4188
|
}
|
|
3967
4189
|
var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0, n) + "\u2026" : String(s)).replace(/\n/g, "\u23CE");
|
|
@@ -4024,7 +4246,7 @@ async function loadConfig(cwd) {
|
|
|
4024
4246
|
|
|
4025
4247
|
// cli/hooks-config.ts
|
|
4026
4248
|
import { spawnSync } from "child_process";
|
|
4027
|
-
var
|
|
4249
|
+
var log9 = forComponent("hooks");
|
|
4028
4250
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
4029
4251
|
function ruleMatches(rule, toolName) {
|
|
4030
4252
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -4041,7 +4263,7 @@ function runCmd(rule, env) {
|
|
|
4041
4263
|
});
|
|
4042
4264
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
4043
4265
|
} catch (e) {
|
|
4044
|
-
|
|
4266
|
+
log9.debug(`hook command failed: ${rule.command}`, e);
|
|
4045
4267
|
return { code: 1, out: String(e?.message ?? e) };
|
|
4046
4268
|
}
|
|
4047
4269
|
}
|
|
@@ -4147,7 +4369,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
4147
4369
|
// cli/session.ts
|
|
4148
4370
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync } from "fs";
|
|
4149
4371
|
import { join as join5 } from "path";
|
|
4150
|
-
var
|
|
4372
|
+
var log10 = forComponent("session");
|
|
4151
4373
|
var SessionStore = class {
|
|
4152
4374
|
dir;
|
|
4153
4375
|
constructor(cwd) {
|
|
@@ -4173,7 +4395,7 @@ var SessionStore = class {
|
|
|
4173
4395
|
}
|
|
4174
4396
|
load(id) {
|
|
4175
4397
|
if (!this.safeId(id)) {
|
|
4176
|
-
|
|
4398
|
+
log10.debug(`rejecting unsafe session id: ${id}`);
|
|
4177
4399
|
return void 0;
|
|
4178
4400
|
}
|
|
4179
4401
|
const path = join5(this.dir, `${id}.json`);
|
|
@@ -4181,7 +4403,7 @@ var SessionStore = class {
|
|
|
4181
4403
|
try {
|
|
4182
4404
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4183
4405
|
} catch (e) {
|
|
4184
|
-
|
|
4406
|
+
log10.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
4185
4407
|
return void 0;
|
|
4186
4408
|
}
|
|
4187
4409
|
}
|
|
@@ -4194,7 +4416,7 @@ var SessionStore = class {
|
|
|
4194
4416
|
try {
|
|
4195
4417
|
metas.push(JSON.parse(readFileSync3(join5(this.dir, f), "utf8")).meta);
|
|
4196
4418
|
} catch (e) {
|
|
4197
|
-
|
|
4419
|
+
log10.debug(`skipping unreadable session file ${f}`, e);
|
|
4198
4420
|
}
|
|
4199
4421
|
}
|
|
4200
4422
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -4288,7 +4510,7 @@ import { execFile } from "child_process";
|
|
|
4288
4510
|
import { promisify } from "util";
|
|
4289
4511
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
4290
4512
|
import { join as join6, resolve as resolve2, sep as sep2 } from "path";
|
|
4291
|
-
var
|
|
4513
|
+
var log11 = forComponent("checkpoints");
|
|
4292
4514
|
var exec = promisify(execFile);
|
|
4293
4515
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
4294
4516
|
var ShadowRepo = class {
|
|
@@ -4322,7 +4544,7 @@ var ShadowRepo = class {
|
|
|
4322
4544
|
writeFileSync4(join6(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
4323
4545
|
this.ready = true;
|
|
4324
4546
|
} catch (e) {
|
|
4325
|
-
|
|
4547
|
+
log11.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
4326
4548
|
this.ready = false;
|
|
4327
4549
|
}
|
|
4328
4550
|
return this.ready;
|
|
@@ -4385,7 +4607,7 @@ var ShadowRepo = class {
|
|
|
4385
4607
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
4386
4608
|
});
|
|
4387
4609
|
} catch (e) {
|
|
4388
|
-
|
|
4610
|
+
log11.debug("checkpoint prune failed", e);
|
|
4389
4611
|
}
|
|
4390
4612
|
}
|
|
4391
4613
|
};
|
|
@@ -4442,7 +4664,7 @@ var GitCheckpoints = class {
|
|
|
4442
4664
|
use(sessionId) {
|
|
4443
4665
|
if (sessionId === this.session) return;
|
|
4444
4666
|
this.session = sessionId;
|
|
4445
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
4667
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log11.debug("re-point failed", e));
|
|
4446
4668
|
}
|
|
4447
4669
|
async begin(label) {
|
|
4448
4670
|
if (!await this.start()) return;
|
|
@@ -4453,7 +4675,7 @@ var GitCheckpoints = class {
|
|
|
4453
4675
|
try {
|
|
4454
4676
|
await r.commit(msg);
|
|
4455
4677
|
} catch (e) {
|
|
4456
|
-
|
|
4678
|
+
log11.debug("checkpoint commit failed", e);
|
|
4457
4679
|
}
|
|
4458
4680
|
}
|
|
4459
4681
|
if (slow) clearTimeout(slow);
|
|
@@ -5402,6 +5624,9 @@ function createLineEditor(out) {
|
|
|
5402
5624
|
process.on("SIGWINCH", onResize);
|
|
5403
5625
|
return new Promise((resolve4) => {
|
|
5404
5626
|
const redraw = () => render(s, opts.prompt, maxVisible, opts.status);
|
|
5627
|
+
const ticker = opts.statusTickMs && opts.status ? setInterval(() => {
|
|
5628
|
+
if (!s.pasting) redraw();
|
|
5629
|
+
}, opts.statusTickMs) : void 0;
|
|
5405
5630
|
const onKey = (str, key) => {
|
|
5406
5631
|
if (key?.ctrl && key.name === "l") {
|
|
5407
5632
|
out.write("\x1B[2J\x1B[3J\x1B[H");
|
|
@@ -5419,6 +5644,11 @@ function createLineEditor(out) {
|
|
|
5419
5644
|
redraw();
|
|
5420
5645
|
return;
|
|
5421
5646
|
}
|
|
5647
|
+
if (key?.ctrl && key.name === "o" && opts.onToggleVerbose) {
|
|
5648
|
+
opts.onToggleVerbose();
|
|
5649
|
+
redraw();
|
|
5650
|
+
return;
|
|
5651
|
+
}
|
|
5422
5652
|
if (key?.meta && key.name === "p" && opts.onPickModel) {
|
|
5423
5653
|
process.stdin.off("keypress", onKey);
|
|
5424
5654
|
void opts.onPickModel().finally(() => {
|
|
@@ -5455,6 +5685,7 @@ function createLineEditor(out) {
|
|
|
5455
5685
|
render(s, opts.prompt, maxVisible, opts.status);
|
|
5456
5686
|
};
|
|
5457
5687
|
const finish = () => {
|
|
5688
|
+
if (ticker) clearInterval(ticker);
|
|
5458
5689
|
process.stdin.off("keypress", onKey);
|
|
5459
5690
|
process.removeListener("SIGWINCH", onResize);
|
|
5460
5691
|
out.write("\x1B[?2004l");
|
|
@@ -5767,7 +5998,7 @@ var red = C("31");
|
|
|
5767
5998
|
var bold = C("1");
|
|
5768
5999
|
var yellow = C("33");
|
|
5769
6000
|
var err = (s) => process.stderr.write(s);
|
|
5770
|
-
var
|
|
6001
|
+
var log12 = forComponent("cli");
|
|
5771
6002
|
var VERSION = (() => {
|
|
5772
6003
|
try {
|
|
5773
6004
|
return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -5806,7 +6037,7 @@ function parseReasoning(raw) {
|
|
|
5806
6037
|
throw new Error(`invalid --reasoning: ${raw} (use off|low|medium|high or a token budget)`);
|
|
5807
6038
|
}
|
|
5808
6039
|
function parseArgs(argv) {
|
|
5809
|
-
const a = { stream: true, plan: false, ask: false, yes: false, vfs: false, shell: void 0, seed: false, subagents: false, help: false, version: false, cont: false, outputFormat: "text" };
|
|
6040
|
+
const a = { stream: true, plan: false, ask: false, yes: false, vfs: false, shell: void 0, seed: false, subagents: false, help: false, version: false, cont: false, outputFormat: "text", duplex: false, voice: false };
|
|
5810
6041
|
const rest = [];
|
|
5811
6042
|
const val = (i, flag) => {
|
|
5812
6043
|
const v = argv[i];
|
|
@@ -5838,6 +6069,11 @@ function parseArgs(argv) {
|
|
|
5838
6069
|
else if (x === "--shell") a.shell = true;
|
|
5839
6070
|
else if (x === "--no-shell") a.shell = false;
|
|
5840
6071
|
else if (x === "--subagents") a.subagents = true;
|
|
6072
|
+
else if (x === "--duplex") a.duplex = true;
|
|
6073
|
+
else if (x === "--conversational" || x === "--convo" || x === "--voice") {
|
|
6074
|
+
a.voice = true;
|
|
6075
|
+
a.duplex = true;
|
|
6076
|
+
} else if (x === "--voice-model") a.voiceModel = val(++i, x);
|
|
5841
6077
|
else if (x === "--allowedTools" || x === "--allowed-tools") a.allowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
|
|
5842
6078
|
else if (x === "--disallowedTools" || x === "--disallowed-tools") a.disallowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
|
|
5843
6079
|
else if (x === "--append-system-prompt") a.appendSystemPrompt = val(++i, x);
|
|
@@ -5857,6 +6093,9 @@ function parseArgs(argv) {
|
|
|
5857
6093
|
if (!a.task && rest.length) a.task = rest.join(" ");
|
|
5858
6094
|
if (a.boddb && a.vfs) throw new Error("--boddb and --sandbox are mutually exclusive (both are non-disk filesystems; pick one)");
|
|
5859
6095
|
if (a.seed && !a.boddb) throw new Error("--seed only applies with --boddb (it seeds the database from cwd on first run)");
|
|
6096
|
+
if (a.duplex && (a.task || a.print)) throw new Error("--duplex is interactive-only (a conversational mode) \u2014 drop the task/-p");
|
|
6097
|
+
if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
|
|
6098
|
+
if (a.voiceModel && !a.duplex) throw new Error("--voice-model only applies with --duplex");
|
|
5860
6099
|
return a;
|
|
5861
6100
|
}
|
|
5862
6101
|
var HELP = `agentx \u2014 agent.libx.js CLI
|
|
@@ -5886,6 +6125,11 @@ Flags:
|
|
|
5886
6125
|
--allowedTools <l> comma-list of tools to allow w/o asking, e.g. "Edit,Shell(git *)"
|
|
5887
6126
|
--disallowedTools <l> comma-list of tools to deny outright (wins over allow), e.g. "Shell(rm *)"
|
|
5888
6127
|
--append-system-prompt <t> extra instructions appended to the system prompt for this run
|
|
6128
|
+
--duplex duplex mode: a fast voice model replies instantly and delegates real work
|
|
6129
|
+
to a background worker agent (-m model); results are re-voiced when ready
|
|
6130
|
+
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
6131
|
+
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
6132
|
+
--voice-model <id> with --duplex: the fast voice model (default anthropic/claude-haiku-4-5)
|
|
5889
6133
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
5890
6134
|
--subagents allow the Task tool (spawn child agents)
|
|
5891
6135
|
--reasoning <e> extended thinking: off|low|medium|high or a token budget (anthropic/openai)
|
|
@@ -5918,7 +6162,7 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
|
|
|
5918
6162
|
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 /exit
|
|
5919
6163
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
|
|
5920
6164
|
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.
|
|
5921
|
-
REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 \u2192 or Tab accepts the dim history ghost-suggestion.
|
|
6165
|
+
REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 \u2192 or Tab accepts the dim history ghost-suggestion.
|
|
5922
6166
|
REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
|
|
5923
6167
|
REPL paste: large/multi-line pastes collapse to a [Pasted text +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; /paste grabs a clipboard image (macOS).`;
|
|
5924
6168
|
function newestModel() {
|
|
@@ -5980,7 +6224,10 @@ function makeHost(format = "text", opts) {
|
|
|
5980
6224
|
}
|
|
5981
6225
|
if (e.kind === "thinking_delta") {
|
|
5982
6226
|
if (streamJson) process.stdout.write(JSON.stringify({ type: "thinking", text: e.message }) + "\n");
|
|
5983
|
-
else if (!cleanStdout)
|
|
6227
|
+
else if (!cleanStdout) {
|
|
6228
|
+
if (md && md.pending()) process.stdout.write(md.flush() + "\n");
|
|
6229
|
+
process.stderr.write(dim(e.message));
|
|
6230
|
+
}
|
|
5984
6231
|
return;
|
|
5985
6232
|
}
|
|
5986
6233
|
if (md && md.pending()) process.stdout.write(md.flush() + "\n");
|
|
@@ -6054,7 +6301,7 @@ function displayHooks(fs) {
|
|
|
6054
6301
|
const text = String(result).replace(/\s+$/, "");
|
|
6055
6302
|
if (text && !/^Edited|^Wrote|^Applied/.test(text)) {
|
|
6056
6303
|
const lines = text.split("\n");
|
|
6057
|
-
const shown = lines.slice(0,
|
|
6304
|
+
const shown = lines.slice(0, previewLines());
|
|
6058
6305
|
for (const ln of shown) err(dim(` ${ln.length > 200 ? ln.slice(0, 200) + "\u2026" : ln}
|
|
6059
6306
|
`));
|
|
6060
6307
|
const more = lines.length - shown.length;
|
|
@@ -6182,6 +6429,14 @@ async function appendMemoryNote(fs, dir, text) {
|
|
|
6182
6429
|
}
|
|
6183
6430
|
var ASK_MUTATING = ["bash", "Shell", "Write", "Edit", "MultiEdit", "deleteFile"];
|
|
6184
6431
|
var RESULT_PREVIEW_LINES = 6;
|
|
6432
|
+
var verboseOutput = false;
|
|
6433
|
+
var previewLines = () => verboseOutput ? Number.MAX_SAFE_INTEGER : RESULT_PREVIEW_LINES;
|
|
6434
|
+
var toggleVerbose = () => {
|
|
6435
|
+
verboseOutput = !verboseOutput;
|
|
6436
|
+
err(dim(` \u2303O verbose output ${verboseOutput ? "on \u2014 full tool results" : "off \u2014 previews"}
|
|
6437
|
+
`));
|
|
6438
|
+
return verboseOutput ? "verbose" : "preview";
|
|
6439
|
+
};
|
|
6185
6440
|
var canPrompt = !!(process.stdin.isTTY && process.stderr.isTTY);
|
|
6186
6441
|
function resolvePermMode(args, interactiveCapable) {
|
|
6187
6442
|
if (args.yes) return { gate: "allow" };
|
|
@@ -6262,7 +6517,10 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
|
|
|
6262
6517
|
maxToolCalls: cfg.maxToolCalls,
|
|
6263
6518
|
keepToolOutputs: cfg.keepToolOutputs,
|
|
6264
6519
|
maxContextTokens: cfg.maxContextTokens,
|
|
6265
|
-
learnFromMistakes: cfg.learnFromMistakes
|
|
6520
|
+
learnFromMistakes: cfg.learnFromMistakes,
|
|
6521
|
+
// Forwarded to cursor/* delegations for environment parity (chat-model providers ignore it).
|
|
6522
|
+
// Raw config (pre-OAuth): unresolved-oauth http servers are skipped by the cursor mapper.
|
|
6523
|
+
mcpServers: cfg.mcpServers
|
|
6266
6524
|
};
|
|
6267
6525
|
}
|
|
6268
6526
|
async function makeAgent(args, ai, cfg, extraTools = []) {
|
|
@@ -6306,7 +6564,7 @@ async function mountMcp(cfg, oauth) {
|
|
|
6306
6564
|
return mounted;
|
|
6307
6565
|
}
|
|
6308
6566
|
async function closeMcp(mounted) {
|
|
6309
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
6567
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log12.debug("mcp close failed", e))));
|
|
6310
6568
|
}
|
|
6311
6569
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
6312
6570
|
function readImageParts(cwd, line) {
|
|
@@ -6386,7 +6644,7 @@ async function readMultiline(readLine) {
|
|
|
6386
6644
|
return parts.join("\n");
|
|
6387
6645
|
}
|
|
6388
6646
|
}
|
|
6389
|
-
async function runTurn(agent, store, session, task, cp, cwd = process.cwd()) {
|
|
6647
|
+
async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sendFn) {
|
|
6390
6648
|
const t0 = Date.now();
|
|
6391
6649
|
await cp?.begin(task);
|
|
6392
6650
|
const { text, loaded, missing } = await expandMentions(agent.options.fs, task);
|
|
@@ -6406,9 +6664,9 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd()) {
|
|
|
6406
6664
|
agent.options.signal = ctrl.signal;
|
|
6407
6665
|
const content = images.length ? [{ type: "text", text }, ...images] : text;
|
|
6408
6666
|
let res;
|
|
6409
|
-
spinner.start();
|
|
6667
|
+
spinner.start(sendFn ? "voice\u2026" : void 0);
|
|
6410
6668
|
try {
|
|
6411
|
-
res = await agent.send(content);
|
|
6669
|
+
res = await (sendFn ? sendFn(content) : agent.send(content));
|
|
6412
6670
|
} catch (e) {
|
|
6413
6671
|
spinner.stop();
|
|
6414
6672
|
err(red(` error: ${e?.message ?? e}
|
|
@@ -6529,25 +6787,127 @@ function persistSetting(cwd, key, value) {
|
|
|
6529
6787
|
}
|
|
6530
6788
|
}
|
|
6531
6789
|
var persistModel = (cwd, model) => persistSetting(cwd, "model", model);
|
|
6790
|
+
var isCancelTeardown = (e) => {
|
|
6791
|
+
if (!e) return false;
|
|
6792
|
+
if (e.code === 20 || e.cause?.code === 20) return true;
|
|
6793
|
+
const blob = `${e.code ?? ""} ${e.name ?? ""} ${e.cause?.name ?? ""} ${e.rawMessage ?? ""} ${e.message ?? ""}`;
|
|
6794
|
+
return /NGHTTP2_FRAME_SIZE_ERROR|ERR_HTTP2_STREAM_ERROR|operation was aborted|\bAbortError\b/i.test(blob);
|
|
6795
|
+
};
|
|
6796
|
+
function installCancelGuards(mounted) {
|
|
6797
|
+
process.on("unhandledRejection", (e) => {
|
|
6798
|
+
if (isCancelTeardown(e)) {
|
|
6799
|
+
log12.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
6802
|
+
log12.error("unhandledRejection", e);
|
|
6803
|
+
});
|
|
6804
|
+
process.on("uncaughtException", (e) => {
|
|
6805
|
+
if (isCancelTeardown(e)) {
|
|
6806
|
+
log12.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
6807
|
+
return;
|
|
6808
|
+
}
|
|
6809
|
+
console.error(e);
|
|
6810
|
+
void closeMcp(mounted);
|
|
6811
|
+
process.exit(1);
|
|
6812
|
+
});
|
|
6813
|
+
}
|
|
6532
6814
|
async function repl(args, ai, cfg, cwd) {
|
|
6533
6815
|
const oauth = new McpOAuth({ storePath: join8(cwd, ".agent", "mcp-auth.json") });
|
|
6534
6816
|
const mounted = await mountMcp(cfg, oauth);
|
|
6535
6817
|
const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
|
|
6818
|
+
const duplex = args.duplex;
|
|
6819
|
+
let dx;
|
|
6820
|
+
let workerOptions;
|
|
6821
|
+
let duplexPersist = () => {
|
|
6822
|
+
};
|
|
6823
|
+
let duplexAccount = () => {
|
|
6824
|
+
};
|
|
6825
|
+
const duplexAsk = async (call) => {
|
|
6826
|
+
err(yellow(` \u2298 worker asked to run ${call.name} \u2014 auto-denied (no interactive approval in duplex; use --yes or --allowedTools)
|
|
6827
|
+
`));
|
|
6828
|
+
return { decision: "deny" };
|
|
6829
|
+
};
|
|
6830
|
+
if (duplex) {
|
|
6831
|
+
const { host: _host, stream: _stream, signal: _signal, ...wo } = agent.options;
|
|
6832
|
+
workerOptions = wo;
|
|
6833
|
+
if (workerOptions.permissions)
|
|
6834
|
+
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: void 0, ask: duplexAsk });
|
|
6835
|
+
workerOptions.planMode = false;
|
|
6836
|
+
const base = makeHost("text", { stream: true });
|
|
6837
|
+
const host = {
|
|
6838
|
+
...base,
|
|
6839
|
+
notify(e) {
|
|
6840
|
+
if (e.kind === "revoice_done") {
|
|
6841
|
+
base.flushText();
|
|
6842
|
+
process.stdout.write("\n");
|
|
6843
|
+
duplexPersist();
|
|
6844
|
+
return;
|
|
6845
|
+
}
|
|
6846
|
+
if (e.kind === "task_done" && e.data?.text) {
|
|
6847
|
+
const lines = String(e.data.text).split("\n");
|
|
6848
|
+
const shown = lines.slice(0, previewLines());
|
|
6849
|
+
err("\n" + dim(` \u29BF ${e.message}
|
|
6850
|
+
`) + shown.map((l) => dim(` ${l}
|
|
6851
|
+
`)).join(""));
|
|
6852
|
+
if (lines.length > shown.length) err(dim(` \u2026 (+${lines.length - shown.length} more lines)
|
|
6853
|
+
`));
|
|
6854
|
+
duplexAccount(e.data);
|
|
6855
|
+
return;
|
|
6856
|
+
}
|
|
6857
|
+
base.notify(e);
|
|
6858
|
+
}
|
|
6859
|
+
};
|
|
6860
|
+
const rewindFilesTool = {
|
|
6861
|
+
name: "RewindFiles",
|
|
6862
|
+
description: "Undo file changes made by delegated tasks: roll back the last N task checkpoints (default 1). Use when the user asks to undo/revert what a task changed.",
|
|
6863
|
+
parameters: { type: "object", properties: { steps: { type: "number", description: "how many task checkpoints to undo (default 1)" } } },
|
|
6864
|
+
run: async ({ steps }) => {
|
|
6865
|
+
if (!checkpoints.size) return "No file checkpoints to rewind yet.";
|
|
6866
|
+
if ([...dx?.tasks.values() ?? []].some((t) => t.status === "running"))
|
|
6867
|
+
return "A task is still running \u2014 cancel it first (CancelTask), then rewind.";
|
|
6868
|
+
const n = Math.min(Math.max(1, Number(steps ?? 1)), checkpoints.size);
|
|
6869
|
+
const r = await checkpoints.rewindTo(checkpoints.size - n);
|
|
6870
|
+
return `Rewound ${n} task checkpoint(s): restored ${r.restored} file(s), deleted ${r.deleted}.`;
|
|
6871
|
+
}
|
|
6872
|
+
};
|
|
6873
|
+
dx = new DuplexAgent({
|
|
6874
|
+
ai,
|
|
6875
|
+
fs: agent.options.fs,
|
|
6876
|
+
...args.voiceModel ? { voiceModel: resolveModelOrNewest(args.voiceModel) } : {},
|
|
6877
|
+
workerModel: agent.options.model,
|
|
6878
|
+
workerOptions,
|
|
6879
|
+
host,
|
|
6880
|
+
...args.voice ? { voiceStyle: "conversational" } : {},
|
|
6881
|
+
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
6882
|
+
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
6883
|
+
onTaskStart: async (_id, label) => {
|
|
6884
|
+
await checkpoints.begin(label);
|
|
6885
|
+
},
|
|
6886
|
+
// The voice runs on the REAL fs (it has no fs tools — harmless) so @mentions, !cmd and #note
|
|
6887
|
+
// resolve against the project; + CC-parity chrome for its own tool calls (⚙ Delegate …).
|
|
6888
|
+
voiceOptions: { fs: agent.options.fs, hooks: displayHooks(agent.options.fs), tools: [rewindFilesTool] }
|
|
6889
|
+
});
|
|
6890
|
+
}
|
|
6891
|
+
const face = dx ? dx.voice : agent;
|
|
6892
|
+
const work = workerOptions ?? agent.options;
|
|
6893
|
+
const sendVia = dx ? (c) => dx.send(c) : void 0;
|
|
6536
6894
|
const baseRules = () => [
|
|
6537
6895
|
...parsePermRules({ deny: args.disallowedTools }),
|
|
6538
6896
|
...parsePermRules({ allow: args.allowedTools }),
|
|
6539
6897
|
...parsePermRules(mergePerms(loadPersistedRules(cwd), cfg.permissions))
|
|
6540
6898
|
];
|
|
6541
6899
|
const askFor = (tools) => tools.map((t) => ({ tool: t, decision: "ask" }));
|
|
6542
|
-
const POSTURES = ["default", "acceptEdits", "plan"];
|
|
6543
|
-
let posture = args.plan || cfg.permissionMode === "plan" ? "plan" : cfg.permissionMode === "acceptEdits" ? "acceptEdits" : "default";
|
|
6900
|
+
const POSTURES = duplex ? ["default", "acceptEdits"] : ["default", "acceptEdits", "plan"];
|
|
6901
|
+
let posture = !duplex && (args.plan || cfg.permissionMode === "plan") ? "plan" : cfg.permissionMode === "acceptEdits" ? "acceptEdits" : "default";
|
|
6544
6902
|
const postureLabel = () => posture === "default" ? "ask (default)" : posture === "acceptEdits" ? "accept edits" : "plan mode";
|
|
6545
6903
|
const applyPosture = (p) => {
|
|
6546
6904
|
posture = p;
|
|
6547
6905
|
const ask = p === "acceptEdits" ? askFor(["bash", "Shell"]) : askFor(ASK_MUTATING);
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6906
|
+
work.permissions = new PermissionPolicy({ rules: [...baseRules(), ...ask], default: "allow", host: duplex ? void 0 : makeHost(), ask: duplex ? duplexAsk : makeAskResolver(cwd) });
|
|
6907
|
+
if (!duplex) {
|
|
6908
|
+
agent.options.planMode = p === "plan";
|
|
6909
|
+
agent.reprepare();
|
|
6910
|
+
}
|
|
6551
6911
|
};
|
|
6552
6912
|
const cyclePosture = () => {
|
|
6553
6913
|
applyPosture(POSTURES[(POSTURES.indexOf(posture) + 1) % POSTURES.length]);
|
|
@@ -6558,13 +6918,27 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6558
6918
|
if (!args.yes) applyPosture(posture);
|
|
6559
6919
|
const REASONING_CYCLE = ["off", "low", "medium", "high"];
|
|
6560
6920
|
const toggleReasoning = () => {
|
|
6561
|
-
const cur = String(
|
|
6921
|
+
const cur = String(work.reasoning ?? "off");
|
|
6562
6922
|
const next = REASONING_CYCLE[(Math.max(0, REASONING_CYCLE.indexOf(cur)) + 1) % REASONING_CYCLE.length];
|
|
6563
|
-
|
|
6923
|
+
work.reasoning = next;
|
|
6564
6924
|
err(dim(` ~ reasoning \u2192 ${next}
|
|
6565
6925
|
`));
|
|
6566
6926
|
return next;
|
|
6567
6927
|
};
|
|
6928
|
+
const setModel = (m) => {
|
|
6929
|
+
work.model = m;
|
|
6930
|
+
if (dx) dx.options.workerModel = m;
|
|
6931
|
+
persistModel(cwd, m);
|
|
6932
|
+
err(dim(" model \u2192 " + m + "\n"));
|
|
6933
|
+
};
|
|
6934
|
+
const addWorkTools = (ts) => {
|
|
6935
|
+
if (duplex) work.tools = [...work.tools ?? [], ...ts];
|
|
6936
|
+
else agent.addTools(ts);
|
|
6937
|
+
};
|
|
6938
|
+
const removeWorkTools = (names) => {
|
|
6939
|
+
if (duplex) work.tools = (work.tools ?? []).filter((t) => !names.includes(t.name));
|
|
6940
|
+
else agent.removeTools(names);
|
|
6941
|
+
};
|
|
6568
6942
|
const pendingImages = [];
|
|
6569
6943
|
const grabClipboardAttachment = () => {
|
|
6570
6944
|
const dir = join8(tmpdir(), "agentx-pasted");
|
|
@@ -6583,33 +6957,33 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6583
6957
|
void closeMcp(mounted);
|
|
6584
6958
|
process.exit(130);
|
|
6585
6959
|
});
|
|
6586
|
-
|
|
6587
|
-
if (!e) return false;
|
|
6588
|
-
if (e.code === 20 || e.cause?.code === 20) return true;
|
|
6589
|
-
const blob = `${e.code ?? ""} ${e.name ?? ""} ${e.cause?.name ?? ""} ${e.rawMessage ?? ""} ${e.message ?? ""}`;
|
|
6590
|
-
return /NGHTTP2_FRAME_SIZE_ERROR|ERR_HTTP2_STREAM_ERROR|operation was aborted|\bAbortError\b/i.test(blob);
|
|
6591
|
-
};
|
|
6592
|
-
process.on("unhandledRejection", (e) => {
|
|
6593
|
-
if (isCancelTeardown(e)) {
|
|
6594
|
-
log11.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
6595
|
-
return;
|
|
6596
|
-
}
|
|
6597
|
-
log11.error("unhandledRejection", e);
|
|
6598
|
-
});
|
|
6599
|
-
process.on("uncaughtException", (e) => {
|
|
6600
|
-
if (isCancelTeardown(e)) {
|
|
6601
|
-
log11.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
6602
|
-
return;
|
|
6603
|
-
}
|
|
6604
|
-
console.error(e);
|
|
6605
|
-
void closeMcp(mounted);
|
|
6606
|
-
process.exit(1);
|
|
6607
|
-
});
|
|
6960
|
+
installCancelGuards(mounted);
|
|
6608
6961
|
const store = new SessionStore(cwd);
|
|
6609
|
-
let session = startSession(args, store,
|
|
6962
|
+
let session = startSession(args, store, face, cwd);
|
|
6610
6963
|
const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join8(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
|
|
6611
6964
|
const cpHooks = checkpoints.hooks?.();
|
|
6612
|
-
if (cpHooks)
|
|
6965
|
+
if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
|
|
6966
|
+
duplexPersist = () => {
|
|
6967
|
+
session.messages = face.transcript;
|
|
6968
|
+
session.meta.updated = Date.now();
|
|
6969
|
+
if (!session.meta.title) session.meta.title = titleOf(face.transcript);
|
|
6970
|
+
try {
|
|
6971
|
+
store.save(session);
|
|
6972
|
+
} catch (e) {
|
|
6973
|
+
err(dim(` (session not saved: ${e?.message ?? e})
|
|
6974
|
+
`));
|
|
6975
|
+
}
|
|
6976
|
+
};
|
|
6977
|
+
duplexAccount = (data) => {
|
|
6978
|
+
if (!data?.usage?.totalTokens) return;
|
|
6979
|
+
session.meta.tokens = (session.meta.tokens ?? 0) + data.usage.totalTokens;
|
|
6980
|
+
session.meta.costUsd = (session.meta.costUsd ?? 0) + turnCost(work.model, data.usage);
|
|
6981
|
+
if (data.usageEstimated) session.meta.costEstimated = true;
|
|
6982
|
+
try {
|
|
6983
|
+
store.save(session);
|
|
6984
|
+
} catch {
|
|
6985
|
+
}
|
|
6986
|
+
};
|
|
6613
6987
|
const fs = agent.options.fs;
|
|
6614
6988
|
const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
|
|
6615
6989
|
const adot = (sub) => `${fsBase}/.agent/${sub}`;
|
|
@@ -6623,7 +6997,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6623
6997
|
mkdirSync6(join8(cwd, ".agent"), { recursive: true });
|
|
6624
6998
|
appendFileSync(histPath, line + "\n");
|
|
6625
6999
|
} catch (e) {
|
|
6626
|
-
|
|
7000
|
+
log12.debug("history write failed", e);
|
|
6627
7001
|
}
|
|
6628
7002
|
};
|
|
6629
7003
|
const ago = (t) => {
|
|
@@ -6631,7 +7005,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6631
7005
|
return s < 60 ? "just now" : s < 3600 ? `${Math.floor(s / 60)}m ago` : s < 86400 ? `${Math.floor(s / 3600)}h ago` : `${Math.floor(s / 86400)}d ago`;
|
|
6632
7006
|
};
|
|
6633
7007
|
const resumeInto = (data) => {
|
|
6634
|
-
|
|
7008
|
+
face.transcript = data.messages;
|
|
6635
7009
|
session = data;
|
|
6636
7010
|
checkpoints.use?.(data.meta.id);
|
|
6637
7011
|
err(dim(` resumed ${data.meta.id} (${data.meta.turns} turns)${data.meta.title ? " \u2014 " + data.meta.title : ""}
|
|
@@ -6639,7 +7013,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6639
7013
|
printHistory(data.messages);
|
|
6640
7014
|
};
|
|
6641
7015
|
const rewindToMessage = async () => {
|
|
6642
|
-
const users =
|
|
7016
|
+
const users = face.transcript.map((m, i) => ({ m, i })).filter((x) => x.m.role === "user");
|
|
6643
7017
|
if (!users.length) {
|
|
6644
7018
|
err(dim(" (no earlier messages to jump back to)\n"));
|
|
6645
7019
|
return void 0;
|
|
@@ -6655,7 +7029,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6655
7029
|
const idx = users[p].i;
|
|
6656
7030
|
const frame = p - (users.length - checkpoints.size);
|
|
6657
7031
|
let mode = "convo";
|
|
6658
|
-
if (frame >= 0 && frame < checkpoints.size) {
|
|
7032
|
+
if (!duplex && frame >= 0 && frame < checkpoints.size) {
|
|
6659
7033
|
const m = await selectMenu(process.stderr, { title: "Restore\u2026", items: [
|
|
6660
7034
|
{ label: "Conversation and code", value: "both" },
|
|
6661
7035
|
{ label: "Conversation only", value: "convo" },
|
|
@@ -6664,7 +7038,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6664
7038
|
if (m == null) return void 0;
|
|
6665
7039
|
mode = m;
|
|
6666
7040
|
}
|
|
6667
|
-
const text = contentText(
|
|
7041
|
+
const text = contentText(face.transcript[idx].content).split("\n\n--- @")[0].trim();
|
|
6668
7042
|
if (mode === "code" || mode === "both") {
|
|
6669
7043
|
try {
|
|
6670
7044
|
const { restored, deleted } = await checkpoints.rewindTo(frame);
|
|
@@ -6676,14 +7050,14 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6676
7050
|
}
|
|
6677
7051
|
}
|
|
6678
7052
|
if (mode === "convo" || mode === "both") {
|
|
6679
|
-
|
|
6680
|
-
session.messages =
|
|
7053
|
+
face.transcript = face.transcript.slice(0, idx);
|
|
7054
|
+
session.messages = face.transcript;
|
|
6681
7055
|
try {
|
|
6682
7056
|
store.save(session);
|
|
6683
7057
|
} catch (e) {
|
|
6684
|
-
|
|
7058
|
+
log12.debug("session save after rewind failed", e);
|
|
6685
7059
|
}
|
|
6686
|
-
err(green(" \u27F2 jumped back") + dim(` \u2014 ${
|
|
7060
|
+
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
6687
7061
|
`));
|
|
6688
7062
|
return text;
|
|
6689
7063
|
}
|
|
@@ -6702,19 +7076,20 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6702
7076
|
if (data) resumeInto(data);
|
|
6703
7077
|
else err(red(" no such session\n"));
|
|
6704
7078
|
};
|
|
7079
|
+
const turn = (task) => runTurn(face, store, session, task, duplex ? void 0 : checkpoints, cwd, sendVia);
|
|
6705
7080
|
const runSkill = async (sk, extra = "") => {
|
|
6706
7081
|
try {
|
|
6707
7082
|
const body = await fs.readFile(sk.path);
|
|
6708
|
-
await
|
|
7083
|
+
await turn(extra ? `${body}
|
|
6709
7084
|
|
|
6710
|
-
${extra}` : body
|
|
7085
|
+
${extra}` : body);
|
|
6711
7086
|
} catch (e) {
|
|
6712
7087
|
err(red(` couldn't load skill ${sk.name}: ${e?.message ?? e}
|
|
6713
7088
|
`));
|
|
6714
7089
|
}
|
|
6715
7090
|
};
|
|
6716
7091
|
const runCommand = async (c, extra = "") => {
|
|
6717
|
-
await
|
|
7092
|
+
await turn(await expandCommand(fs, c, extra));
|
|
6718
7093
|
};
|
|
6719
7094
|
const pickAndRun = async (kind) => {
|
|
6720
7095
|
const pool = kind === "skill" ? skills : cmds;
|
|
@@ -6771,15 +7146,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6771
7146
|
desc: "show CLI version + runtime",
|
|
6772
7147
|
run: () => {
|
|
6773
7148
|
const rt = process.versions.bun ? `bun ${process.versions.bun}` : `node ${process.versions.node}`;
|
|
6774
|
-
err(` ${bold("agent.libx.js")} ${cyan("v" + VERSION)}${dim(` \xB7 ${
|
|
7149
|
+
err(` ${bold("agent.libx.js")} ${cyan("v" + VERSION)}${dim(` \xB7 ${duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model} \xB7 ${rt}`)}
|
|
6775
7150
|
`);
|
|
6776
7151
|
}
|
|
6777
7152
|
},
|
|
6778
|
-
tools: {
|
|
7153
|
+
tools: {
|
|
7154
|
+
desc: "list available tools",
|
|
7155
|
+
run: () => {
|
|
7156
|
+
if (duplex) err(dim(" voice: " + face.options.tools.map((t) => t.name).join(", ") + "\n worker: " + (work.tools ?? []).map((t) => t.name).join(", ") + "\n"));
|
|
7157
|
+
else err(dim(" " + agent.options.tools.map((t) => t.name).join(", ") + "\n"));
|
|
7158
|
+
}
|
|
7159
|
+
},
|
|
6779
7160
|
permissions: {
|
|
6780
7161
|
desc: "show the active permission rules + default posture",
|
|
6781
7162
|
run: () => {
|
|
6782
|
-
const pol =
|
|
7163
|
+
const pol = work.permissions;
|
|
6783
7164
|
const rules = pol?.options.rules ?? [];
|
|
6784
7165
|
if (!rules.length) err(dim(" (no rules \u2014 default: " + (pol?.options.default ?? "allow") + ")\n"));
|
|
6785
7166
|
else {
|
|
@@ -6793,19 +7174,22 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6793
7174
|
desc: "session status \u2014 model, dir, fs-mode, permissions, tools, usage",
|
|
6794
7175
|
run: () => {
|
|
6795
7176
|
const mode = args.vfs ? "sandbox (VFS \u2014 disk untouched)" : args.boddb ? `boddb (database workspace at ${args.boddb} \u2014 disk untouched)` : args.shell ? "disk + real /bin/sh" : "disk (full real FS, like Claude Code)";
|
|
6796
|
-
const pol =
|
|
7177
|
+
const pol = work.permissions;
|
|
6797
7178
|
const perm = !pol ? "allow all (unattended)" : `${pol.options.rules.length} rule(s), default ${pol.options.default}`;
|
|
6798
|
-
|
|
7179
|
+
const model = duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model;
|
|
7180
|
+
err(formatStatus({ model, cwd, mode, tools: (duplex ? work.tools ?? [] : agent.options.tools).map((t) => t.name), permissions: perm, turns: session.meta.turns, tokens: session.meta.tokens ?? 0, sessionId: session.meta.id, estimated: session.meta.costEstimated ?? false }));
|
|
7181
|
+
if (duplex && dx.tasks.size) err(dim(` tasks: ${[...dx.tasks.values()].map((t) => `${t.id}:${t.status}`).join(" ")}
|
|
7182
|
+
`));
|
|
6799
7183
|
}
|
|
6800
7184
|
},
|
|
6801
7185
|
cost: {
|
|
6802
7186
|
desc: "cumulative cost + token usage for this session",
|
|
6803
7187
|
run: () => {
|
|
6804
7188
|
const t = session.meta.tokens ?? 0, usd = session.meta.costUsd ?? 0;
|
|
6805
|
-
const priced = getModelInfo(
|
|
7189
|
+
const priced = getModelInfo(work.model)?.pricing;
|
|
6806
7190
|
const est = session.meta.costEstimated ?? false;
|
|
6807
7191
|
const m = est ? "~" : "";
|
|
6808
|
-
const note = priced ? est ? " (estimated \u2014 some turns streamed without usage)" : " (exact \u2014 provider-reported usage)" : ` (no pricing for ${
|
|
7192
|
+
const note = priced ? est ? " (estimated \u2014 some turns streamed without usage)" : " (exact \u2014 provider-reported usage)" : ` (no pricing for ${work.model})`;
|
|
6809
7193
|
err(dim(` ${usd > 0 ? m + fmtUsd(usd) + " \xB7 " : ""}${m}${(t / 1e3).toFixed(1)}k tokens across ${session.meta.turns} turn(s)${note}
|
|
6810
7194
|
`));
|
|
6811
7195
|
}
|
|
@@ -6813,9 +7197,9 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6813
7197
|
context: {
|
|
6814
7198
|
desc: "context-window usage (messages + estimated tokens)",
|
|
6815
7199
|
run: () => {
|
|
6816
|
-
const est = estimateTranscriptTokens(
|
|
6817
|
-
const cap =
|
|
6818
|
-
err(dim(` ${
|
|
7200
|
+
const est = estimateTranscriptTokens(face.transcript);
|
|
7201
|
+
const cap = face.options.maxTokens || 2e5;
|
|
7202
|
+
err(dim(` ${face.transcript.length} message(s) \xB7 ~${(est / 1e3).toFixed(1)}k tokens (~${Math.round(est / cap * 100)}% of ${Math.round(cap / 1e3)}k budget)
|
|
6819
7203
|
`));
|
|
6820
7204
|
}
|
|
6821
7205
|
},
|
|
@@ -6846,26 +7230,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6846
7230
|
}
|
|
6847
7231
|
},
|
|
6848
7232
|
model: {
|
|
6849
|
-
desc: "switch model \u2014 /model <id>, or /model alone for an interactive picker",
|
|
7233
|
+
desc: "switch model \u2014 /model <id>, or /model alone for an interactive picker (duplex: the worker model)",
|
|
6850
7234
|
run: async (a) => {
|
|
6851
7235
|
if (a[0]) {
|
|
6852
|
-
|
|
6853
|
-
persistModel(cwd, a[0]);
|
|
6854
|
-
err(dim(" model \u2192 " + a[0] + "\n"));
|
|
7236
|
+
setModel(a[0]);
|
|
6855
7237
|
return;
|
|
6856
7238
|
}
|
|
6857
|
-
const picked = await pickModel(
|
|
6858
|
-
if (picked)
|
|
6859
|
-
|
|
6860
|
-
persistModel(cwd, picked);
|
|
6861
|
-
err(dim(" model \u2192 " + picked + "\n"));
|
|
6862
|
-
} else err(dim(" " + agent.options.model + "\n"));
|
|
7239
|
+
const picked = await pickModel(work.model);
|
|
7240
|
+
if (picked) setModel(picked);
|
|
7241
|
+
else err(dim(" " + (duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model) + "\n"));
|
|
6863
7242
|
}
|
|
6864
7243
|
},
|
|
6865
7244
|
reasoning: {
|
|
6866
|
-
desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker",
|
|
7245
|
+
desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker (duplex: the workers')",
|
|
6867
7246
|
run: async (a) => {
|
|
6868
|
-
const current = String(
|
|
7247
|
+
const current = String(work.reasoning ?? "off");
|
|
6869
7248
|
let next;
|
|
6870
7249
|
if (a[0]) {
|
|
6871
7250
|
try {
|
|
@@ -6888,9 +7267,9 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6888
7267
|
}
|
|
6889
7268
|
next = picked;
|
|
6890
7269
|
}
|
|
6891
|
-
|
|
6892
|
-
if (next !== "off" && getModelInfo(
|
|
6893
|
-
err(yellow(` note: ${
|
|
7270
|
+
work.reasoning = next;
|
|
7271
|
+
if (next !== "off" && getModelInfo(work.model)?.reasoning === false)
|
|
7272
|
+
err(yellow(` note: ${work.model} has no reasoning capability \u2014 setting may be ignored
|
|
6894
7273
|
`));
|
|
6895
7274
|
err(dim(" reasoning \u2192 " + next + "\n"));
|
|
6896
7275
|
}
|
|
@@ -6900,23 +7279,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6900
7279
|
run: async () => {
|
|
6901
7280
|
for (; ; ) {
|
|
6902
7281
|
const items = [
|
|
6903
|
-
{ label: "model", value: "model", desc:
|
|
6904
|
-
{ label: "reasoning", value: "reasoning", desc: String(
|
|
7282
|
+
{ label: "model", value: "model", desc: work.model },
|
|
7283
|
+
{ label: "reasoning", value: "reasoning", desc: String(work.reasoning ?? "off") },
|
|
6905
7284
|
{ label: "permission posture", value: "posture", desc: postureLabel() + " (Shift+Tab)" },
|
|
6906
|
-
|
|
7285
|
+
// streaming is the voice's lifeblood in duplex (always on) — only a normal-mode knob
|
|
7286
|
+
...duplex ? [] : [{ label: "streaming", value: "stream", desc: agent.options.stream ? "on" : "off" }],
|
|
6907
7287
|
{ label: "editor mode", value: "editor", desc: cfg.editorMode === "vim" ? "vim" : "normal" }
|
|
6908
7288
|
];
|
|
6909
7289
|
const pick = await selectMenu(process.stderr, { title: "Settings \xB7 \u21B5 change \xB7 esc close", items });
|
|
6910
7290
|
if (!pick) return;
|
|
6911
7291
|
if (pick === "model") {
|
|
6912
|
-
const m = await pickModel(
|
|
6913
|
-
if (m)
|
|
6914
|
-
agent.options.model = m;
|
|
6915
|
-
persistModel(cwd, m);
|
|
6916
|
-
}
|
|
7292
|
+
const m = await pickModel(work.model);
|
|
7293
|
+
if (m) setModel(m);
|
|
6917
7294
|
} else if (pick === "reasoning") {
|
|
6918
7295
|
await builtins.reasoning.run([]);
|
|
6919
|
-
persistSetting(cwd, "reasoning",
|
|
7296
|
+
persistSetting(cwd, "reasoning", work.reasoning ?? "off");
|
|
6920
7297
|
} else if (pick === "posture") {
|
|
6921
7298
|
cyclePosture();
|
|
6922
7299
|
persistSetting(cwd, "permissionMode", posture);
|
|
@@ -6951,8 +7328,8 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6951
7328
|
compact: {
|
|
6952
7329
|
desc: "summarize older context to free up the window",
|
|
6953
7330
|
run: () => {
|
|
6954
|
-
const n =
|
|
6955
|
-
session.messages =
|
|
7331
|
+
const n = face.compactNow();
|
|
7332
|
+
session.messages = face.transcript;
|
|
6956
7333
|
try {
|
|
6957
7334
|
store.save(session);
|
|
6958
7335
|
} catch {
|
|
@@ -6999,8 +7376,8 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6999
7376
|
clear: {
|
|
7000
7377
|
desc: "start a fresh conversation (and clear the screen)",
|
|
7001
7378
|
run: () => {
|
|
7002
|
-
|
|
7003
|
-
session = startSession({ ...args, cont: false, resume: void 0 }, store,
|
|
7379
|
+
face.transcript = [];
|
|
7380
|
+
session = startSession({ ...args, cont: false, resume: void 0 }, store, face, cwd);
|
|
7004
7381
|
err("\x1Bc");
|
|
7005
7382
|
}
|
|
7006
7383
|
},
|
|
@@ -7038,13 +7415,13 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7038
7415
|
err(green(` \u2713 authorized "${name}"`) + dim(" \u2014 remounting with the new token\n"));
|
|
7039
7416
|
const idx = mounted.findIndex((m2) => m2.name === name);
|
|
7040
7417
|
if (idx >= 0) {
|
|
7041
|
-
|
|
7418
|
+
removeWorkTools(mounted[idx].tools.map((t) => t.name));
|
|
7042
7419
|
await mounted.splice(idx, 1)[0].client.close().catch(() => {
|
|
7043
7420
|
});
|
|
7044
7421
|
}
|
|
7045
7422
|
const m = await mountMcpServer(name, { ...target, bearerToken: await oauth.tokenFor(target.url) });
|
|
7046
7423
|
mounted.push(m);
|
|
7047
|
-
|
|
7424
|
+
addWorkTools(m.tools);
|
|
7048
7425
|
err(green(` \u2713 ${m.name}`) + dim(` \u2014 ${m.tools.length} tool(s)
|
|
7049
7426
|
`));
|
|
7050
7427
|
} catch (e) {
|
|
@@ -7069,7 +7446,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7069
7446
|
try {
|
|
7070
7447
|
const m = await mountMcpServer(name, cfg2);
|
|
7071
7448
|
mounted.push(m);
|
|
7072
|
-
|
|
7449
|
+
addWorkTools(m.tools);
|
|
7073
7450
|
err(green(` \u2713 ${m.name}`) + dim(` \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}
|
|
7074
7451
|
`));
|
|
7075
7452
|
} catch (e) {
|
|
@@ -7091,8 +7468,8 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7091
7468
|
return;
|
|
7092
7469
|
}
|
|
7093
7470
|
const m = mounted.splice(idx, 1)[0];
|
|
7094
|
-
|
|
7095
|
-
await m.client.close().catch((e) =>
|
|
7471
|
+
removeWorkTools(m.tools.map((t) => t.name));
|
|
7472
|
+
await m.client.close().catch((e) => log12.debug("mcp close failed", e));
|
|
7096
7473
|
err(dim(` removed "${name}"
|
|
7097
7474
|
`));
|
|
7098
7475
|
return;
|
|
@@ -7191,7 +7568,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7191
7568
|
return;
|
|
7192
7569
|
}
|
|
7193
7570
|
const msg = a.join(" ").trim();
|
|
7194
|
-
if (msg) await
|
|
7571
|
+
if (msg) await turn(`${msg} ${att.ref}`);
|
|
7195
7572
|
else {
|
|
7196
7573
|
pendingImages.push(att.path);
|
|
7197
7574
|
err(green(` \u2713 image attached (#${pendingImages.length})`) + dim(" \u2014 type your message to send it\n"));
|
|
@@ -7201,7 +7578,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7201
7578
|
export: {
|
|
7202
7579
|
desc: "save this conversation to Markdown \u2014 /export [path] (default ./.agent/exports/<id>.md)",
|
|
7203
7580
|
run: (a) => {
|
|
7204
|
-
const shown =
|
|
7581
|
+
const shown = face.transcript.filter((m) => m.role !== "system");
|
|
7205
7582
|
if (!shown.length) {
|
|
7206
7583
|
err(dim(" (nothing to export yet)\n"));
|
|
7207
7584
|
return;
|
|
@@ -7224,14 +7601,16 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7224
7601
|
exit: { desc: "quit", run: () => true },
|
|
7225
7602
|
quit: { desc: "quit", run: () => true }
|
|
7226
7603
|
};
|
|
7227
|
-
err(bold("agent.libx.js") + cyan(" v" + VERSION) + dim(` \u2014 ${
|
|
7604
|
+
err(bold("agent.libx.js") + cyan(" v" + VERSION) + dim(` \u2014 ${work.model} \xB7 ${cwd}
|
|
7228
7605
|
`));
|
|
7229
7606
|
err(dim("Type a task, or /help. Type / or @ for live suggestions (\u2191/\u2193 \u23CE). Esc cancels/clears; double-Esc jumps back; Ctrl-D exits.\n"));
|
|
7607
|
+
if (dx) err(dim(`\u25D1 duplex \u2014 voice: ${dx.options.voiceModel} \xB7 worker: ${work.model} (real work runs in background tasks, re-voiced when done)
|
|
7608
|
+
`));
|
|
7230
7609
|
const listDir = (absDir) => {
|
|
7231
7610
|
try {
|
|
7232
7611
|
return readdirSync2(join8(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
7233
7612
|
} catch (e) {
|
|
7234
|
-
|
|
7613
|
+
log12.debug("completion readdir failed", absDir, e);
|
|
7235
7614
|
return null;
|
|
7236
7615
|
}
|
|
7237
7616
|
};
|
|
@@ -7247,6 +7626,10 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7247
7626
|
if (process.stdin.isTTY) {
|
|
7248
7627
|
process.stdin.on("keypress", (_s, key) => {
|
|
7249
7628
|
if (!activeTurn) return;
|
|
7629
|
+
if (key?.ctrl && key?.name === "o") {
|
|
7630
|
+
toggleVerbose();
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7250
7633
|
const cancel = key?.name === "escape" || key?.ctrl && key?.name === "c";
|
|
7251
7634
|
if (!cancel) return;
|
|
7252
7635
|
if (!aborting) {
|
|
@@ -7272,6 +7655,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7272
7655
|
process.stdin.pause();
|
|
7273
7656
|
};
|
|
7274
7657
|
let prefill;
|
|
7658
|
+
let tick = 0;
|
|
7275
7659
|
while (true) {
|
|
7276
7660
|
if (pendingRewind) {
|
|
7277
7661
|
pendingRewind = false;
|
|
@@ -7282,16 +7666,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7282
7666
|
err("\n");
|
|
7283
7667
|
const initial = prefill;
|
|
7284
7668
|
prefill = void 0;
|
|
7285
|
-
const ctxTok = estimateTranscriptTokens(
|
|
7286
|
-
const ctxCap =
|
|
7669
|
+
const ctxTok = estimateTranscriptTokens(face.transcript);
|
|
7670
|
+
const ctxCap = face.options.maxTokens || 2e5;
|
|
7287
7671
|
const usd = session.meta.costUsd ?? 0;
|
|
7288
7672
|
const computeFooter = () => {
|
|
7289
7673
|
const parts = [];
|
|
7290
7674
|
if (ctxTok > 400) parts.push(`${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)`);
|
|
7291
7675
|
if (usd > 0) parts.push(`${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}`);
|
|
7292
7676
|
if (posture !== "default") parts.push(postureLabel());
|
|
7293
|
-
const r =
|
|
7677
|
+
const r = work.reasoning;
|
|
7294
7678
|
if (r && r !== "off") parts.push(`reasoning:${r}`);
|
|
7679
|
+
if (verboseOutput) parts.push("verbose");
|
|
7680
|
+
if (dx?.tasks.size) {
|
|
7681
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
7682
|
+
parts.push(`tasks: ${[...dx.tasks.values()].map((t) => t.status === "running" ? `${frames[tick++ % frames.length]} ${t.id} working\u2026` : `${t.id}:${t.status}`).join(" ")}`);
|
|
7683
|
+
}
|
|
7295
7684
|
return parts.join(" \xB7 ");
|
|
7296
7685
|
};
|
|
7297
7686
|
const result = await readMultiline((cont) => editor.readLine({
|
|
@@ -7303,15 +7692,14 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7303
7692
|
initial: cont ? void 0 : initial,
|
|
7304
7693
|
status: computeFooter,
|
|
7305
7694
|
vimMode: cfg.editorMode === "vim",
|
|
7695
|
+
statusTickMs: dx ? 1e3 : void 0,
|
|
7696
|
+
// duplex: animate the running-task footer while idle at the prompt
|
|
7306
7697
|
onCyclePosture: cyclePosture,
|
|
7307
7698
|
onToggleThinking: toggleReasoning,
|
|
7699
|
+
onToggleVerbose: toggleVerbose,
|
|
7308
7700
|
onPickModel: async () => {
|
|
7309
|
-
const picked = await pickModel(
|
|
7310
|
-
if (picked)
|
|
7311
|
-
agent.options.model = picked;
|
|
7312
|
-
persistModel(cwd, picked);
|
|
7313
|
-
err(dim(" model \u2192 " + picked + "\n"));
|
|
7314
|
-
}
|
|
7701
|
+
const picked = await pickModel(work.model);
|
|
7702
|
+
if (picked) setModel(picked);
|
|
7315
7703
|
return picked;
|
|
7316
7704
|
}
|
|
7317
7705
|
}));
|
|
@@ -7369,7 +7757,17 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7369
7757
|
}
|
|
7370
7758
|
const task = pendingImages.length ? `${line} ${pendingImages.map((p) => "@" + p).join(" ")}` : line;
|
|
7371
7759
|
pendingImages.length = 0;
|
|
7372
|
-
await
|
|
7760
|
+
await turn(task);
|
|
7761
|
+
}
|
|
7762
|
+
if (dx) {
|
|
7763
|
+
const running = [...dx.tasks.values()].filter((t) => t.status === "running").length;
|
|
7764
|
+
if (running) {
|
|
7765
|
+
err(dim(` \u2026 waiting for ${running} background task(s) (Ctrl-C to force quit)
|
|
7766
|
+
`));
|
|
7767
|
+
await dx.idle();
|
|
7768
|
+
face.options.host?.flushText?.();
|
|
7769
|
+
duplexPersist();
|
|
7770
|
+
}
|
|
7373
7771
|
}
|
|
7374
7772
|
releaseStdin();
|
|
7375
7773
|
await closeMcp(mounted);
|