agent.libx.js 0.89.6 → 0.89.8
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 +529 -136
- 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(
|
|
@@ -3845,7 +4056,7 @@ async function buildAgent(o) {
|
|
|
3845
4056
|
);
|
|
3846
4057
|
let fs = jailedDisk;
|
|
3847
4058
|
if (o.sandbox) {
|
|
3848
|
-
const mem = new
|
|
4059
|
+
const mem = new MemFilesystem3();
|
|
3849
4060
|
await mkdirp(mem, cwd);
|
|
3850
4061
|
await hydrate(jailedDisk, mem, cwd);
|
|
3851
4062
|
mem.setCwd(cwd);
|
|
@@ -3904,10 +4115,11 @@ Reference files in them by their mount path (the left side).`;
|
|
|
3904
4115
|
ai: o.ai,
|
|
3905
4116
|
fs,
|
|
3906
4117
|
model: o.model ?? "anthropic/claude-sonnet-4-6",
|
|
3907
|
-
// Anchor cursor to the launch dir (its adapter defaults to TMPDIR otherwise)
|
|
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:
|
|
3908
4120
|
// openai/google adapters Object.assign providerOptions into the request body, so a blanket cwd
|
|
3909
4121
|
// would corrupt those calls.
|
|
3910
|
-
...isCursor ? { providerOptions: { cwd } } : {},
|
|
4122
|
+
...isCursor ? { providerOptions: { cwd, ...toCursorMcp(o.mcpServers) ?? {} } } : {},
|
|
3911
4123
|
...(() => {
|
|
3912
4124
|
const now = /* @__PURE__ */ new Date();
|
|
3913
4125
|
const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
|
|
@@ -3971,6 +4183,7 @@ function summarizeCall(name, args) {
|
|
|
3971
4183
|
if (args.command) return String(args.command);
|
|
3972
4184
|
if (args.pattern) return `/${args.pattern}/${args.glob ? " in " + args.glob : ""}`;
|
|
3973
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);
|
|
3974
4187
|
return trunc(JSON.stringify(args), 60);
|
|
3975
4188
|
}
|
|
3976
4189
|
var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0, n) + "\u2026" : String(s)).replace(/\n/g, "\u23CE");
|
|
@@ -4033,7 +4246,7 @@ async function loadConfig(cwd) {
|
|
|
4033
4246
|
|
|
4034
4247
|
// cli/hooks-config.ts
|
|
4035
4248
|
import { spawnSync } from "child_process";
|
|
4036
|
-
var
|
|
4249
|
+
var log9 = forComponent("hooks");
|
|
4037
4250
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
4038
4251
|
function ruleMatches(rule, toolName) {
|
|
4039
4252
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -4050,7 +4263,7 @@ function runCmd(rule, env) {
|
|
|
4050
4263
|
});
|
|
4051
4264
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
4052
4265
|
} catch (e) {
|
|
4053
|
-
|
|
4266
|
+
log9.debug(`hook command failed: ${rule.command}`, e);
|
|
4054
4267
|
return { code: 1, out: String(e?.message ?? e) };
|
|
4055
4268
|
}
|
|
4056
4269
|
}
|
|
@@ -4156,7 +4369,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
4156
4369
|
// cli/session.ts
|
|
4157
4370
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync } from "fs";
|
|
4158
4371
|
import { join as join5 } from "path";
|
|
4159
|
-
var
|
|
4372
|
+
var log10 = forComponent("session");
|
|
4160
4373
|
var SessionStore = class {
|
|
4161
4374
|
dir;
|
|
4162
4375
|
constructor(cwd) {
|
|
@@ -4182,7 +4395,7 @@ var SessionStore = class {
|
|
|
4182
4395
|
}
|
|
4183
4396
|
load(id) {
|
|
4184
4397
|
if (!this.safeId(id)) {
|
|
4185
|
-
|
|
4398
|
+
log10.debug(`rejecting unsafe session id: ${id}`);
|
|
4186
4399
|
return void 0;
|
|
4187
4400
|
}
|
|
4188
4401
|
const path = join5(this.dir, `${id}.json`);
|
|
@@ -4190,7 +4403,7 @@ var SessionStore = class {
|
|
|
4190
4403
|
try {
|
|
4191
4404
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4192
4405
|
} catch (e) {
|
|
4193
|
-
|
|
4406
|
+
log10.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
4194
4407
|
return void 0;
|
|
4195
4408
|
}
|
|
4196
4409
|
}
|
|
@@ -4203,7 +4416,7 @@ var SessionStore = class {
|
|
|
4203
4416
|
try {
|
|
4204
4417
|
metas.push(JSON.parse(readFileSync3(join5(this.dir, f), "utf8")).meta);
|
|
4205
4418
|
} catch (e) {
|
|
4206
|
-
|
|
4419
|
+
log10.debug(`skipping unreadable session file ${f}`, e);
|
|
4207
4420
|
}
|
|
4208
4421
|
}
|
|
4209
4422
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -4297,7 +4510,7 @@ import { execFile } from "child_process";
|
|
|
4297
4510
|
import { promisify } from "util";
|
|
4298
4511
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
4299
4512
|
import { join as join6, resolve as resolve2, sep as sep2 } from "path";
|
|
4300
|
-
var
|
|
4513
|
+
var log11 = forComponent("checkpoints");
|
|
4301
4514
|
var exec = promisify(execFile);
|
|
4302
4515
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
4303
4516
|
var ShadowRepo = class {
|
|
@@ -4331,7 +4544,7 @@ var ShadowRepo = class {
|
|
|
4331
4544
|
writeFileSync4(join6(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
4332
4545
|
this.ready = true;
|
|
4333
4546
|
} catch (e) {
|
|
4334
|
-
|
|
4547
|
+
log11.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
4335
4548
|
this.ready = false;
|
|
4336
4549
|
}
|
|
4337
4550
|
return this.ready;
|
|
@@ -4394,7 +4607,7 @@ var ShadowRepo = class {
|
|
|
4394
4607
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
4395
4608
|
});
|
|
4396
4609
|
} catch (e) {
|
|
4397
|
-
|
|
4610
|
+
log11.debug("checkpoint prune failed", e);
|
|
4398
4611
|
}
|
|
4399
4612
|
}
|
|
4400
4613
|
};
|
|
@@ -4451,7 +4664,7 @@ var GitCheckpoints = class {
|
|
|
4451
4664
|
use(sessionId) {
|
|
4452
4665
|
if (sessionId === this.session) return;
|
|
4453
4666
|
this.session = sessionId;
|
|
4454
|
-
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));
|
|
4455
4668
|
}
|
|
4456
4669
|
async begin(label) {
|
|
4457
4670
|
if (!await this.start()) return;
|
|
@@ -4462,7 +4675,7 @@ var GitCheckpoints = class {
|
|
|
4462
4675
|
try {
|
|
4463
4676
|
await r.commit(msg);
|
|
4464
4677
|
} catch (e) {
|
|
4465
|
-
|
|
4678
|
+
log11.debug("checkpoint commit failed", e);
|
|
4466
4679
|
}
|
|
4467
4680
|
}
|
|
4468
4681
|
if (slow) clearTimeout(slow);
|
|
@@ -5411,6 +5624,9 @@ function createLineEditor(out) {
|
|
|
5411
5624
|
process.on("SIGWINCH", onResize);
|
|
5412
5625
|
return new Promise((resolve4) => {
|
|
5413
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;
|
|
5414
5630
|
const onKey = (str, key) => {
|
|
5415
5631
|
if (key?.ctrl && key.name === "l") {
|
|
5416
5632
|
out.write("\x1B[2J\x1B[3J\x1B[H");
|
|
@@ -5428,6 +5644,11 @@ function createLineEditor(out) {
|
|
|
5428
5644
|
redraw();
|
|
5429
5645
|
return;
|
|
5430
5646
|
}
|
|
5647
|
+
if (key?.ctrl && key.name === "o" && opts.onToggleVerbose) {
|
|
5648
|
+
opts.onToggleVerbose();
|
|
5649
|
+
redraw();
|
|
5650
|
+
return;
|
|
5651
|
+
}
|
|
5431
5652
|
if (key?.meta && key.name === "p" && opts.onPickModel) {
|
|
5432
5653
|
process.stdin.off("keypress", onKey);
|
|
5433
5654
|
void opts.onPickModel().finally(() => {
|
|
@@ -5464,6 +5685,7 @@ function createLineEditor(out) {
|
|
|
5464
5685
|
render(s, opts.prompt, maxVisible, opts.status);
|
|
5465
5686
|
};
|
|
5466
5687
|
const finish = () => {
|
|
5688
|
+
if (ticker) clearInterval(ticker);
|
|
5467
5689
|
process.stdin.off("keypress", onKey);
|
|
5468
5690
|
process.removeListener("SIGWINCH", onResize);
|
|
5469
5691
|
out.write("\x1B[?2004l");
|
|
@@ -5776,7 +5998,7 @@ var red = C("31");
|
|
|
5776
5998
|
var bold = C("1");
|
|
5777
5999
|
var yellow = C("33");
|
|
5778
6000
|
var err = (s) => process.stderr.write(s);
|
|
5779
|
-
var
|
|
6001
|
+
var log12 = forComponent("cli");
|
|
5780
6002
|
var VERSION = (() => {
|
|
5781
6003
|
try {
|
|
5782
6004
|
return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -5815,7 +6037,7 @@ function parseReasoning(raw) {
|
|
|
5815
6037
|
throw new Error(`invalid --reasoning: ${raw} (use off|low|medium|high or a token budget)`);
|
|
5816
6038
|
}
|
|
5817
6039
|
function parseArgs(argv) {
|
|
5818
|
-
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 };
|
|
5819
6041
|
const rest = [];
|
|
5820
6042
|
const val = (i, flag) => {
|
|
5821
6043
|
const v = argv[i];
|
|
@@ -5847,6 +6069,11 @@ function parseArgs(argv) {
|
|
|
5847
6069
|
else if (x === "--shell") a.shell = true;
|
|
5848
6070
|
else if (x === "--no-shell") a.shell = false;
|
|
5849
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);
|
|
5850
6077
|
else if (x === "--allowedTools" || x === "--allowed-tools") a.allowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
|
|
5851
6078
|
else if (x === "--disallowedTools" || x === "--disallowed-tools") a.disallowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
|
|
5852
6079
|
else if (x === "--append-system-prompt") a.appendSystemPrompt = val(++i, x);
|
|
@@ -5866,6 +6093,9 @@ function parseArgs(argv) {
|
|
|
5866
6093
|
if (!a.task && rest.length) a.task = rest.join(" ");
|
|
5867
6094
|
if (a.boddb && a.vfs) throw new Error("--boddb and --sandbox are mutually exclusive (both are non-disk filesystems; pick one)");
|
|
5868
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");
|
|
5869
6099
|
return a;
|
|
5870
6100
|
}
|
|
5871
6101
|
var HELP = `agentx \u2014 agent.libx.js CLI
|
|
@@ -5895,6 +6125,11 @@ Flags:
|
|
|
5895
6125
|
--allowedTools <l> comma-list of tools to allow w/o asking, e.g. "Edit,Shell(git *)"
|
|
5896
6126
|
--disallowedTools <l> comma-list of tools to deny outright (wins over allow), e.g. "Shell(rm *)"
|
|
5897
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)
|
|
5898
6133
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
5899
6134
|
--subagents allow the Task tool (spawn child agents)
|
|
5900
6135
|
--reasoning <e> extended thinking: off|low|medium|high or a token budget (anthropic/openai)
|
|
@@ -5927,7 +6162,7 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
|
|
|
5927
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
|
|
5928
6163
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
|
|
5929
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.
|
|
5930
|
-
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.
|
|
5931
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.
|
|
5932
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).`;
|
|
5933
6168
|
function newestModel() {
|
|
@@ -6066,7 +6301,7 @@ function displayHooks(fs) {
|
|
|
6066
6301
|
const text = String(result).replace(/\s+$/, "");
|
|
6067
6302
|
if (text && !/^Edited|^Wrote|^Applied/.test(text)) {
|
|
6068
6303
|
const lines = text.split("\n");
|
|
6069
|
-
const shown = lines.slice(0,
|
|
6304
|
+
const shown = lines.slice(0, previewLines());
|
|
6070
6305
|
for (const ln of shown) err(dim(` ${ln.length > 200 ? ln.slice(0, 200) + "\u2026" : ln}
|
|
6071
6306
|
`));
|
|
6072
6307
|
const more = lines.length - shown.length;
|
|
@@ -6194,6 +6429,14 @@ async function appendMemoryNote(fs, dir, text) {
|
|
|
6194
6429
|
}
|
|
6195
6430
|
var ASK_MUTATING = ["bash", "Shell", "Write", "Edit", "MultiEdit", "deleteFile"];
|
|
6196
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
|
+
};
|
|
6197
6440
|
var canPrompt = !!(process.stdin.isTTY && process.stderr.isTTY);
|
|
6198
6441
|
function resolvePermMode(args, interactiveCapable) {
|
|
6199
6442
|
if (args.yes) return { gate: "allow" };
|
|
@@ -6274,7 +6517,10 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
|
|
|
6274
6517
|
maxToolCalls: cfg.maxToolCalls,
|
|
6275
6518
|
keepToolOutputs: cfg.keepToolOutputs,
|
|
6276
6519
|
maxContextTokens: cfg.maxContextTokens,
|
|
6277
|
-
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
|
|
6278
6524
|
};
|
|
6279
6525
|
}
|
|
6280
6526
|
async function makeAgent(args, ai, cfg, extraTools = []) {
|
|
@@ -6318,7 +6564,7 @@ async function mountMcp(cfg, oauth) {
|
|
|
6318
6564
|
return mounted;
|
|
6319
6565
|
}
|
|
6320
6566
|
async function closeMcp(mounted) {
|
|
6321
|
-
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))));
|
|
6322
6568
|
}
|
|
6323
6569
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
6324
6570
|
function readImageParts(cwd, line) {
|
|
@@ -6398,7 +6644,7 @@ async function readMultiline(readLine) {
|
|
|
6398
6644
|
return parts.join("\n");
|
|
6399
6645
|
}
|
|
6400
6646
|
}
|
|
6401
|
-
async function runTurn(agent, store, session, task, cp, cwd = process.cwd()) {
|
|
6647
|
+
async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sendFn) {
|
|
6402
6648
|
const t0 = Date.now();
|
|
6403
6649
|
await cp?.begin(task);
|
|
6404
6650
|
const { text, loaded, missing } = await expandMentions(agent.options.fs, task);
|
|
@@ -6418,9 +6664,9 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd()) {
|
|
|
6418
6664
|
agent.options.signal = ctrl.signal;
|
|
6419
6665
|
const content = images.length ? [{ type: "text", text }, ...images] : text;
|
|
6420
6666
|
let res;
|
|
6421
|
-
spinner.start();
|
|
6667
|
+
spinner.start(sendFn ? "voice\u2026" : void 0);
|
|
6422
6668
|
try {
|
|
6423
|
-
res = await agent.send(content);
|
|
6669
|
+
res = await (sendFn ? sendFn(content) : agent.send(content));
|
|
6424
6670
|
} catch (e) {
|
|
6425
6671
|
spinner.stop();
|
|
6426
6672
|
err(red(` error: ${e?.message ?? e}
|
|
@@ -6541,25 +6787,127 @@ function persistSetting(cwd, key, value) {
|
|
|
6541
6787
|
}
|
|
6542
6788
|
}
|
|
6543
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
|
+
}
|
|
6544
6814
|
async function repl(args, ai, cfg, cwd) {
|
|
6545
6815
|
const oauth = new McpOAuth({ storePath: join8(cwd, ".agent", "mcp-auth.json") });
|
|
6546
6816
|
const mounted = await mountMcp(cfg, oauth);
|
|
6547
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;
|
|
6548
6894
|
const baseRules = () => [
|
|
6549
6895
|
...parsePermRules({ deny: args.disallowedTools }),
|
|
6550
6896
|
...parsePermRules({ allow: args.allowedTools }),
|
|
6551
6897
|
...parsePermRules(mergePerms(loadPersistedRules(cwd), cfg.permissions))
|
|
6552
6898
|
];
|
|
6553
6899
|
const askFor = (tools) => tools.map((t) => ({ tool: t, decision: "ask" }));
|
|
6554
|
-
const POSTURES = ["default", "acceptEdits", "plan"];
|
|
6555
|
-
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";
|
|
6556
6902
|
const postureLabel = () => posture === "default" ? "ask (default)" : posture === "acceptEdits" ? "accept edits" : "plan mode";
|
|
6557
6903
|
const applyPosture = (p) => {
|
|
6558
6904
|
posture = p;
|
|
6559
6905
|
const ask = p === "acceptEdits" ? askFor(["bash", "Shell"]) : askFor(ASK_MUTATING);
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
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
|
+
}
|
|
6563
6911
|
};
|
|
6564
6912
|
const cyclePosture = () => {
|
|
6565
6913
|
applyPosture(POSTURES[(POSTURES.indexOf(posture) + 1) % POSTURES.length]);
|
|
@@ -6570,13 +6918,27 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6570
6918
|
if (!args.yes) applyPosture(posture);
|
|
6571
6919
|
const REASONING_CYCLE = ["off", "low", "medium", "high"];
|
|
6572
6920
|
const toggleReasoning = () => {
|
|
6573
|
-
const cur = String(
|
|
6921
|
+
const cur = String(work.reasoning ?? "off");
|
|
6574
6922
|
const next = REASONING_CYCLE[(Math.max(0, REASONING_CYCLE.indexOf(cur)) + 1) % REASONING_CYCLE.length];
|
|
6575
|
-
|
|
6923
|
+
work.reasoning = next;
|
|
6576
6924
|
err(dim(` ~ reasoning \u2192 ${next}
|
|
6577
6925
|
`));
|
|
6578
6926
|
return next;
|
|
6579
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
|
+
};
|
|
6580
6942
|
const pendingImages = [];
|
|
6581
6943
|
const grabClipboardAttachment = () => {
|
|
6582
6944
|
const dir = join8(tmpdir(), "agentx-pasted");
|
|
@@ -6595,33 +6957,33 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6595
6957
|
void closeMcp(mounted);
|
|
6596
6958
|
process.exit(130);
|
|
6597
6959
|
});
|
|
6598
|
-
|
|
6599
|
-
if (!e) return false;
|
|
6600
|
-
if (e.code === 20 || e.cause?.code === 20) return true;
|
|
6601
|
-
const blob = `${e.code ?? ""} ${e.name ?? ""} ${e.cause?.name ?? ""} ${e.rawMessage ?? ""} ${e.message ?? ""}`;
|
|
6602
|
-
return /NGHTTP2_FRAME_SIZE_ERROR|ERR_HTTP2_STREAM_ERROR|operation was aborted|\bAbortError\b/i.test(blob);
|
|
6603
|
-
};
|
|
6604
|
-
process.on("unhandledRejection", (e) => {
|
|
6605
|
-
if (isCancelTeardown(e)) {
|
|
6606
|
-
log11.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
6607
|
-
return;
|
|
6608
|
-
}
|
|
6609
|
-
log11.error("unhandledRejection", e);
|
|
6610
|
-
});
|
|
6611
|
-
process.on("uncaughtException", (e) => {
|
|
6612
|
-
if (isCancelTeardown(e)) {
|
|
6613
|
-
log11.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
6614
|
-
return;
|
|
6615
|
-
}
|
|
6616
|
-
console.error(e);
|
|
6617
|
-
void closeMcp(mounted);
|
|
6618
|
-
process.exit(1);
|
|
6619
|
-
});
|
|
6960
|
+
installCancelGuards(mounted);
|
|
6620
6961
|
const store = new SessionStore(cwd);
|
|
6621
|
-
let session = startSession(args, store,
|
|
6962
|
+
let session = startSession(args, store, face, cwd);
|
|
6622
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 });
|
|
6623
6964
|
const cpHooks = checkpoints.hooks?.();
|
|
6624
|
-
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
|
+
};
|
|
6625
6987
|
const fs = agent.options.fs;
|
|
6626
6988
|
const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
|
|
6627
6989
|
const adot = (sub) => `${fsBase}/.agent/${sub}`;
|
|
@@ -6635,7 +6997,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6635
6997
|
mkdirSync6(join8(cwd, ".agent"), { recursive: true });
|
|
6636
6998
|
appendFileSync(histPath, line + "\n");
|
|
6637
6999
|
} catch (e) {
|
|
6638
|
-
|
|
7000
|
+
log12.debug("history write failed", e);
|
|
6639
7001
|
}
|
|
6640
7002
|
};
|
|
6641
7003
|
const ago = (t) => {
|
|
@@ -6643,7 +7005,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6643
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`;
|
|
6644
7006
|
};
|
|
6645
7007
|
const resumeInto = (data) => {
|
|
6646
|
-
|
|
7008
|
+
face.transcript = data.messages;
|
|
6647
7009
|
session = data;
|
|
6648
7010
|
checkpoints.use?.(data.meta.id);
|
|
6649
7011
|
err(dim(` resumed ${data.meta.id} (${data.meta.turns} turns)${data.meta.title ? " \u2014 " + data.meta.title : ""}
|
|
@@ -6651,7 +7013,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6651
7013
|
printHistory(data.messages);
|
|
6652
7014
|
};
|
|
6653
7015
|
const rewindToMessage = async () => {
|
|
6654
|
-
const users =
|
|
7016
|
+
const users = face.transcript.map((m, i) => ({ m, i })).filter((x) => x.m.role === "user");
|
|
6655
7017
|
if (!users.length) {
|
|
6656
7018
|
err(dim(" (no earlier messages to jump back to)\n"));
|
|
6657
7019
|
return void 0;
|
|
@@ -6667,7 +7029,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6667
7029
|
const idx = users[p].i;
|
|
6668
7030
|
const frame = p - (users.length - checkpoints.size);
|
|
6669
7031
|
let mode = "convo";
|
|
6670
|
-
if (frame >= 0 && frame < checkpoints.size) {
|
|
7032
|
+
if (!duplex && frame >= 0 && frame < checkpoints.size) {
|
|
6671
7033
|
const m = await selectMenu(process.stderr, { title: "Restore\u2026", items: [
|
|
6672
7034
|
{ label: "Conversation and code", value: "both" },
|
|
6673
7035
|
{ label: "Conversation only", value: "convo" },
|
|
@@ -6676,7 +7038,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6676
7038
|
if (m == null) return void 0;
|
|
6677
7039
|
mode = m;
|
|
6678
7040
|
}
|
|
6679
|
-
const text = contentText(
|
|
7041
|
+
const text = contentText(face.transcript[idx].content).split("\n\n--- @")[0].trim();
|
|
6680
7042
|
if (mode === "code" || mode === "both") {
|
|
6681
7043
|
try {
|
|
6682
7044
|
const { restored, deleted } = await checkpoints.rewindTo(frame);
|
|
@@ -6688,14 +7050,14 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6688
7050
|
}
|
|
6689
7051
|
}
|
|
6690
7052
|
if (mode === "convo" || mode === "both") {
|
|
6691
|
-
|
|
6692
|
-
session.messages =
|
|
7053
|
+
face.transcript = face.transcript.slice(0, idx);
|
|
7054
|
+
session.messages = face.transcript;
|
|
6693
7055
|
try {
|
|
6694
7056
|
store.save(session);
|
|
6695
7057
|
} catch (e) {
|
|
6696
|
-
|
|
7058
|
+
log12.debug("session save after rewind failed", e);
|
|
6697
7059
|
}
|
|
6698
|
-
err(green(" \u27F2 jumped back") + dim(` \u2014 ${
|
|
7060
|
+
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
6699
7061
|
`));
|
|
6700
7062
|
return text;
|
|
6701
7063
|
}
|
|
@@ -6714,19 +7076,27 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6714
7076
|
if (data) resumeInto(data);
|
|
6715
7077
|
else err(red(" no such session\n"));
|
|
6716
7078
|
};
|
|
7079
|
+
const turn = async (task) => {
|
|
7080
|
+
const r = await runTurn(face, store, session, task, duplex ? void 0 : checkpoints, cwd, sendVia);
|
|
7081
|
+
if (dx) {
|
|
7082
|
+
const running = [...dx.tasks.values()].filter((t) => t.status === "running");
|
|
7083
|
+
if (running.length) err(cyan(` \u25D4 ${running.length === 1 ? `task ${running[0].id} (${running[0].label})` : `${running.length} tasks`} still working in the background`) + dim(" \u2014 the result will appear here; keep chatting meanwhile\n"));
|
|
7084
|
+
}
|
|
7085
|
+
return r;
|
|
7086
|
+
};
|
|
6717
7087
|
const runSkill = async (sk, extra = "") => {
|
|
6718
7088
|
try {
|
|
6719
7089
|
const body = await fs.readFile(sk.path);
|
|
6720
|
-
await
|
|
7090
|
+
await turn(extra ? `${body}
|
|
6721
7091
|
|
|
6722
|
-
${extra}` : body
|
|
7092
|
+
${extra}` : body);
|
|
6723
7093
|
} catch (e) {
|
|
6724
7094
|
err(red(` couldn't load skill ${sk.name}: ${e?.message ?? e}
|
|
6725
7095
|
`));
|
|
6726
7096
|
}
|
|
6727
7097
|
};
|
|
6728
7098
|
const runCommand = async (c, extra = "") => {
|
|
6729
|
-
await
|
|
7099
|
+
await turn(await expandCommand(fs, c, extra));
|
|
6730
7100
|
};
|
|
6731
7101
|
const pickAndRun = async (kind) => {
|
|
6732
7102
|
const pool = kind === "skill" ? skills : cmds;
|
|
@@ -6783,15 +7153,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6783
7153
|
desc: "show CLI version + runtime",
|
|
6784
7154
|
run: () => {
|
|
6785
7155
|
const rt = process.versions.bun ? `bun ${process.versions.bun}` : `node ${process.versions.node}`;
|
|
6786
|
-
err(` ${bold("agent.libx.js")} ${cyan("v" + VERSION)}${dim(` \xB7 ${
|
|
7156
|
+
err(` ${bold("agent.libx.js")} ${cyan("v" + VERSION)}${dim(` \xB7 ${duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model} \xB7 ${rt}`)}
|
|
6787
7157
|
`);
|
|
6788
7158
|
}
|
|
6789
7159
|
},
|
|
6790
|
-
tools: {
|
|
7160
|
+
tools: {
|
|
7161
|
+
desc: "list available tools",
|
|
7162
|
+
run: () => {
|
|
7163
|
+
if (duplex) err(dim(" voice: " + face.options.tools.map((t) => t.name).join(", ") + "\n worker: " + (work.tools ?? []).map((t) => t.name).join(", ") + "\n"));
|
|
7164
|
+
else err(dim(" " + agent.options.tools.map((t) => t.name).join(", ") + "\n"));
|
|
7165
|
+
}
|
|
7166
|
+
},
|
|
6791
7167
|
permissions: {
|
|
6792
7168
|
desc: "show the active permission rules + default posture",
|
|
6793
7169
|
run: () => {
|
|
6794
|
-
const pol =
|
|
7170
|
+
const pol = work.permissions;
|
|
6795
7171
|
const rules = pol?.options.rules ?? [];
|
|
6796
7172
|
if (!rules.length) err(dim(" (no rules \u2014 default: " + (pol?.options.default ?? "allow") + ")\n"));
|
|
6797
7173
|
else {
|
|
@@ -6805,19 +7181,22 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6805
7181
|
desc: "session status \u2014 model, dir, fs-mode, permissions, tools, usage",
|
|
6806
7182
|
run: () => {
|
|
6807
7183
|
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)";
|
|
6808
|
-
const pol =
|
|
7184
|
+
const pol = work.permissions;
|
|
6809
7185
|
const perm = !pol ? "allow all (unattended)" : `${pol.options.rules.length} rule(s), default ${pol.options.default}`;
|
|
6810
|
-
|
|
7186
|
+
const model = duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model;
|
|
7187
|
+
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 }));
|
|
7188
|
+
if (duplex && dx.tasks.size) err(dim(` tasks: ${[...dx.tasks.values()].map((t) => `${t.id}:${t.status}`).join(" ")}
|
|
7189
|
+
`));
|
|
6811
7190
|
}
|
|
6812
7191
|
},
|
|
6813
7192
|
cost: {
|
|
6814
7193
|
desc: "cumulative cost + token usage for this session",
|
|
6815
7194
|
run: () => {
|
|
6816
7195
|
const t = session.meta.tokens ?? 0, usd = session.meta.costUsd ?? 0;
|
|
6817
|
-
const priced = getModelInfo(
|
|
7196
|
+
const priced = getModelInfo(work.model)?.pricing;
|
|
6818
7197
|
const est = session.meta.costEstimated ?? false;
|
|
6819
7198
|
const m = est ? "~" : "";
|
|
6820
|
-
const note = priced ? est ? " (estimated \u2014 some turns streamed without usage)" : " (exact \u2014 provider-reported usage)" : ` (no pricing for ${
|
|
7199
|
+
const note = priced ? est ? " (estimated \u2014 some turns streamed without usage)" : " (exact \u2014 provider-reported usage)" : ` (no pricing for ${work.model})`;
|
|
6821
7200
|
err(dim(` ${usd > 0 ? m + fmtUsd(usd) + " \xB7 " : ""}${m}${(t / 1e3).toFixed(1)}k tokens across ${session.meta.turns} turn(s)${note}
|
|
6822
7201
|
`));
|
|
6823
7202
|
}
|
|
@@ -6825,9 +7204,9 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6825
7204
|
context: {
|
|
6826
7205
|
desc: "context-window usage (messages + estimated tokens)",
|
|
6827
7206
|
run: () => {
|
|
6828
|
-
const est = estimateTranscriptTokens(
|
|
6829
|
-
const cap =
|
|
6830
|
-
err(dim(` ${
|
|
7207
|
+
const est = estimateTranscriptTokens(face.transcript);
|
|
7208
|
+
const cap = face.options.maxTokens || 2e5;
|
|
7209
|
+
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)
|
|
6831
7210
|
`));
|
|
6832
7211
|
}
|
|
6833
7212
|
},
|
|
@@ -6858,26 +7237,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6858
7237
|
}
|
|
6859
7238
|
},
|
|
6860
7239
|
model: {
|
|
6861
|
-
desc: "switch model \u2014 /model <id>, or /model alone for an interactive picker",
|
|
7240
|
+
desc: "switch model \u2014 /model <id>, or /model alone for an interactive picker (duplex: the worker model)",
|
|
6862
7241
|
run: async (a) => {
|
|
6863
7242
|
if (a[0]) {
|
|
6864
|
-
|
|
6865
|
-
persistModel(cwd, a[0]);
|
|
6866
|
-
err(dim(" model \u2192 " + a[0] + "\n"));
|
|
7243
|
+
setModel(a[0]);
|
|
6867
7244
|
return;
|
|
6868
7245
|
}
|
|
6869
|
-
const picked = await pickModel(
|
|
6870
|
-
if (picked)
|
|
6871
|
-
|
|
6872
|
-
persistModel(cwd, picked);
|
|
6873
|
-
err(dim(" model \u2192 " + picked + "\n"));
|
|
6874
|
-
} else err(dim(" " + agent.options.model + "\n"));
|
|
7246
|
+
const picked = await pickModel(work.model);
|
|
7247
|
+
if (picked) setModel(picked);
|
|
7248
|
+
else err(dim(" " + (duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model) + "\n"));
|
|
6875
7249
|
}
|
|
6876
7250
|
},
|
|
6877
7251
|
reasoning: {
|
|
6878
|
-
desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker",
|
|
7252
|
+
desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker (duplex: the workers')",
|
|
6879
7253
|
run: async (a) => {
|
|
6880
|
-
const current = String(
|
|
7254
|
+
const current = String(work.reasoning ?? "off");
|
|
6881
7255
|
let next;
|
|
6882
7256
|
if (a[0]) {
|
|
6883
7257
|
try {
|
|
@@ -6900,9 +7274,9 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6900
7274
|
}
|
|
6901
7275
|
next = picked;
|
|
6902
7276
|
}
|
|
6903
|
-
|
|
6904
|
-
if (next !== "off" && getModelInfo(
|
|
6905
|
-
err(yellow(` note: ${
|
|
7277
|
+
work.reasoning = next;
|
|
7278
|
+
if (next !== "off" && getModelInfo(work.model)?.reasoning === false)
|
|
7279
|
+
err(yellow(` note: ${work.model} has no reasoning capability \u2014 setting may be ignored
|
|
6906
7280
|
`));
|
|
6907
7281
|
err(dim(" reasoning \u2192 " + next + "\n"));
|
|
6908
7282
|
}
|
|
@@ -6912,23 +7286,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6912
7286
|
run: async () => {
|
|
6913
7287
|
for (; ; ) {
|
|
6914
7288
|
const items = [
|
|
6915
|
-
{ label: "model", value: "model", desc:
|
|
6916
|
-
{ label: "reasoning", value: "reasoning", desc: String(
|
|
7289
|
+
{ label: "model", value: "model", desc: work.model },
|
|
7290
|
+
{ label: "reasoning", value: "reasoning", desc: String(work.reasoning ?? "off") },
|
|
6917
7291
|
{ label: "permission posture", value: "posture", desc: postureLabel() + " (Shift+Tab)" },
|
|
6918
|
-
|
|
7292
|
+
// streaming is the voice's lifeblood in duplex (always on) — only a normal-mode knob
|
|
7293
|
+
...duplex ? [] : [{ label: "streaming", value: "stream", desc: agent.options.stream ? "on" : "off" }],
|
|
6919
7294
|
{ label: "editor mode", value: "editor", desc: cfg.editorMode === "vim" ? "vim" : "normal" }
|
|
6920
7295
|
];
|
|
6921
7296
|
const pick = await selectMenu(process.stderr, { title: "Settings \xB7 \u21B5 change \xB7 esc close", items });
|
|
6922
7297
|
if (!pick) return;
|
|
6923
7298
|
if (pick === "model") {
|
|
6924
|
-
const m = await pickModel(
|
|
6925
|
-
if (m)
|
|
6926
|
-
agent.options.model = m;
|
|
6927
|
-
persistModel(cwd, m);
|
|
6928
|
-
}
|
|
7299
|
+
const m = await pickModel(work.model);
|
|
7300
|
+
if (m) setModel(m);
|
|
6929
7301
|
} else if (pick === "reasoning") {
|
|
6930
7302
|
await builtins.reasoning.run([]);
|
|
6931
|
-
persistSetting(cwd, "reasoning",
|
|
7303
|
+
persistSetting(cwd, "reasoning", work.reasoning ?? "off");
|
|
6932
7304
|
} else if (pick === "posture") {
|
|
6933
7305
|
cyclePosture();
|
|
6934
7306
|
persistSetting(cwd, "permissionMode", posture);
|
|
@@ -6963,8 +7335,8 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
6963
7335
|
compact: {
|
|
6964
7336
|
desc: "summarize older context to free up the window",
|
|
6965
7337
|
run: () => {
|
|
6966
|
-
const n =
|
|
6967
|
-
session.messages =
|
|
7338
|
+
const n = face.compactNow();
|
|
7339
|
+
session.messages = face.transcript;
|
|
6968
7340
|
try {
|
|
6969
7341
|
store.save(session);
|
|
6970
7342
|
} catch {
|
|
@@ -7011,8 +7383,8 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7011
7383
|
clear: {
|
|
7012
7384
|
desc: "start a fresh conversation (and clear the screen)",
|
|
7013
7385
|
run: () => {
|
|
7014
|
-
|
|
7015
|
-
session = startSession({ ...args, cont: false, resume: void 0 }, store,
|
|
7386
|
+
face.transcript = [];
|
|
7387
|
+
session = startSession({ ...args, cont: false, resume: void 0 }, store, face, cwd);
|
|
7016
7388
|
err("\x1Bc");
|
|
7017
7389
|
}
|
|
7018
7390
|
},
|
|
@@ -7050,13 +7422,13 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7050
7422
|
err(green(` \u2713 authorized "${name}"`) + dim(" \u2014 remounting with the new token\n"));
|
|
7051
7423
|
const idx = mounted.findIndex((m2) => m2.name === name);
|
|
7052
7424
|
if (idx >= 0) {
|
|
7053
|
-
|
|
7425
|
+
removeWorkTools(mounted[idx].tools.map((t) => t.name));
|
|
7054
7426
|
await mounted.splice(idx, 1)[0].client.close().catch(() => {
|
|
7055
7427
|
});
|
|
7056
7428
|
}
|
|
7057
7429
|
const m = await mountMcpServer(name, { ...target, bearerToken: await oauth.tokenFor(target.url) });
|
|
7058
7430
|
mounted.push(m);
|
|
7059
|
-
|
|
7431
|
+
addWorkTools(m.tools);
|
|
7060
7432
|
err(green(` \u2713 ${m.name}`) + dim(` \u2014 ${m.tools.length} tool(s)
|
|
7061
7433
|
`));
|
|
7062
7434
|
} catch (e) {
|
|
@@ -7081,7 +7453,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7081
7453
|
try {
|
|
7082
7454
|
const m = await mountMcpServer(name, cfg2);
|
|
7083
7455
|
mounted.push(m);
|
|
7084
|
-
|
|
7456
|
+
addWorkTools(m.tools);
|
|
7085
7457
|
err(green(` \u2713 ${m.name}`) + dim(` \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}
|
|
7086
7458
|
`));
|
|
7087
7459
|
} catch (e) {
|
|
@@ -7103,8 +7475,8 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7103
7475
|
return;
|
|
7104
7476
|
}
|
|
7105
7477
|
const m = mounted.splice(idx, 1)[0];
|
|
7106
|
-
|
|
7107
|
-
await m.client.close().catch((e) =>
|
|
7478
|
+
removeWorkTools(m.tools.map((t) => t.name));
|
|
7479
|
+
await m.client.close().catch((e) => log12.debug("mcp close failed", e));
|
|
7108
7480
|
err(dim(` removed "${name}"
|
|
7109
7481
|
`));
|
|
7110
7482
|
return;
|
|
@@ -7203,7 +7575,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7203
7575
|
return;
|
|
7204
7576
|
}
|
|
7205
7577
|
const msg = a.join(" ").trim();
|
|
7206
|
-
if (msg) await
|
|
7578
|
+
if (msg) await turn(`${msg} ${att.ref}`);
|
|
7207
7579
|
else {
|
|
7208
7580
|
pendingImages.push(att.path);
|
|
7209
7581
|
err(green(` \u2713 image attached (#${pendingImages.length})`) + dim(" \u2014 type your message to send it\n"));
|
|
@@ -7213,7 +7585,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7213
7585
|
export: {
|
|
7214
7586
|
desc: "save this conversation to Markdown \u2014 /export [path] (default ./.agent/exports/<id>.md)",
|
|
7215
7587
|
run: (a) => {
|
|
7216
|
-
const shown =
|
|
7588
|
+
const shown = face.transcript.filter((m) => m.role !== "system");
|
|
7217
7589
|
if (!shown.length) {
|
|
7218
7590
|
err(dim(" (nothing to export yet)\n"));
|
|
7219
7591
|
return;
|
|
@@ -7236,14 +7608,16 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7236
7608
|
exit: { desc: "quit", run: () => true },
|
|
7237
7609
|
quit: { desc: "quit", run: () => true }
|
|
7238
7610
|
};
|
|
7239
|
-
err(bold("agent.libx.js") + cyan(" v" + VERSION) + dim(` \u2014 ${
|
|
7611
|
+
err(bold("agent.libx.js") + cyan(" v" + VERSION) + dim(` \u2014 ${work.model} \xB7 ${cwd}
|
|
7240
7612
|
`));
|
|
7241
7613
|
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"));
|
|
7614
|
+
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)
|
|
7615
|
+
`));
|
|
7242
7616
|
const listDir = (absDir) => {
|
|
7243
7617
|
try {
|
|
7244
7618
|
return readdirSync2(join8(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
7245
7619
|
} catch (e) {
|
|
7246
|
-
|
|
7620
|
+
log12.debug("completion readdir failed", absDir, e);
|
|
7247
7621
|
return null;
|
|
7248
7622
|
}
|
|
7249
7623
|
};
|
|
@@ -7259,6 +7633,10 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7259
7633
|
if (process.stdin.isTTY) {
|
|
7260
7634
|
process.stdin.on("keypress", (_s, key) => {
|
|
7261
7635
|
if (!activeTurn) return;
|
|
7636
|
+
if (key?.ctrl && key?.name === "o") {
|
|
7637
|
+
toggleVerbose();
|
|
7638
|
+
return;
|
|
7639
|
+
}
|
|
7262
7640
|
const cancel = key?.name === "escape" || key?.ctrl && key?.name === "c";
|
|
7263
7641
|
if (!cancel) return;
|
|
7264
7642
|
if (!aborting) {
|
|
@@ -7284,6 +7662,7 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7284
7662
|
process.stdin.pause();
|
|
7285
7663
|
};
|
|
7286
7664
|
let prefill;
|
|
7665
|
+
let tick = 0;
|
|
7287
7666
|
while (true) {
|
|
7288
7667
|
if (pendingRewind) {
|
|
7289
7668
|
pendingRewind = false;
|
|
@@ -7294,16 +7673,21 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7294
7673
|
err("\n");
|
|
7295
7674
|
const initial = prefill;
|
|
7296
7675
|
prefill = void 0;
|
|
7297
|
-
const ctxTok = estimateTranscriptTokens(
|
|
7298
|
-
const ctxCap =
|
|
7676
|
+
const ctxTok = estimateTranscriptTokens(face.transcript);
|
|
7677
|
+
const ctxCap = face.options.maxTokens || 2e5;
|
|
7299
7678
|
const usd = session.meta.costUsd ?? 0;
|
|
7300
7679
|
const computeFooter = () => {
|
|
7301
7680
|
const parts = [];
|
|
7302
7681
|
if (ctxTok > 400) parts.push(`${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)`);
|
|
7303
7682
|
if (usd > 0) parts.push(`${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}`);
|
|
7304
7683
|
if (posture !== "default") parts.push(postureLabel());
|
|
7305
|
-
const r =
|
|
7684
|
+
const r = work.reasoning;
|
|
7306
7685
|
if (r && r !== "off") parts.push(`reasoning:${r}`);
|
|
7686
|
+
if (verboseOutput) parts.push("verbose");
|
|
7687
|
+
if (dx?.tasks.size) {
|
|
7688
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
7689
|
+
parts.push(`tasks: ${[...dx.tasks.values()].map((t) => t.status === "running" ? `${frames[tick++ % frames.length]} ${t.id} working\u2026` : `${t.id}:${t.status}`).join(" ")}`);
|
|
7690
|
+
}
|
|
7307
7691
|
return parts.join(" \xB7 ");
|
|
7308
7692
|
};
|
|
7309
7693
|
const result = await readMultiline((cont) => editor.readLine({
|
|
@@ -7315,15 +7699,14 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7315
7699
|
initial: cont ? void 0 : initial,
|
|
7316
7700
|
status: computeFooter,
|
|
7317
7701
|
vimMode: cfg.editorMode === "vim",
|
|
7702
|
+
statusTickMs: dx ? 1e3 : void 0,
|
|
7703
|
+
// duplex: animate the running-task footer while idle at the prompt
|
|
7318
7704
|
onCyclePosture: cyclePosture,
|
|
7319
7705
|
onToggleThinking: toggleReasoning,
|
|
7706
|
+
onToggleVerbose: toggleVerbose,
|
|
7320
7707
|
onPickModel: async () => {
|
|
7321
|
-
const picked = await pickModel(
|
|
7322
|
-
if (picked)
|
|
7323
|
-
agent.options.model = picked;
|
|
7324
|
-
persistModel(cwd, picked);
|
|
7325
|
-
err(dim(" model \u2192 " + picked + "\n"));
|
|
7326
|
-
}
|
|
7708
|
+
const picked = await pickModel(work.model);
|
|
7709
|
+
if (picked) setModel(picked);
|
|
7327
7710
|
return picked;
|
|
7328
7711
|
}
|
|
7329
7712
|
}));
|
|
@@ -7381,7 +7764,17 @@ ${extra}` : body, checkpoints, cwd);
|
|
|
7381
7764
|
}
|
|
7382
7765
|
const task = pendingImages.length ? `${line} ${pendingImages.map((p) => "@" + p).join(" ")}` : line;
|
|
7383
7766
|
pendingImages.length = 0;
|
|
7384
|
-
await
|
|
7767
|
+
await turn(task);
|
|
7768
|
+
}
|
|
7769
|
+
if (dx) {
|
|
7770
|
+
const running = [...dx.tasks.values()].filter((t) => t.status === "running").length;
|
|
7771
|
+
if (running) {
|
|
7772
|
+
err(dim(` \u2026 waiting for ${running} background task(s) (Ctrl-C to force quit)
|
|
7773
|
+
`));
|
|
7774
|
+
await dx.idle();
|
|
7775
|
+
face.options.host?.flushText?.();
|
|
7776
|
+
duplexPersist();
|
|
7777
|
+
}
|
|
7385
7778
|
}
|
|
7386
7779
|
releaseStdin();
|
|
7387
7780
|
await closeMcp(mounted);
|