agent.libx.js 0.89.9 → 0.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/cli.ts +1981 -0
- package/dist/{Agent-B0l9qT_j.d.ts → Agent-BzwprwHr.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +1145 -191
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +200 -6
- package/dist/index.js +584 -14
- package/dist/index.js.map +1 -1
- package/dist/{mcp-Dg3vA1Uj.d.ts → mcp-Bn5TlRbV.d.ts} +10 -2
- package/dist/mcp.client.d.ts +2 -2
- package/dist/mcp.client.js +19 -7
- package/dist/mcp.client.js.map +1 -1
- package/dist/{tools-Ch-OzOU8.d.ts → tools-CeK5AquG.d.ts} +11 -2
- package/dist/tools.shell.d.ts +1 -1
- package/dist/tools.shell.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -852,6 +852,17 @@ ${out}`.trim() : out || "(command succeeded, no output)";
|
|
|
852
852
|
);
|
|
853
853
|
return `Started background job ${id} \u2014 poll with JobOutput({id:"${id}"}) / JobStatus, stop with JobKill.`;
|
|
854
854
|
}
|
|
855
|
+
function exitSessionTool(onExit) {
|
|
856
|
+
return {
|
|
857
|
+
name: "ExitSession",
|
|
858
|
+
description: `End the current session and exit the CLI. Call this when the user says goodbye, asks to quit, or clearly indicates they want to stop the conversation (e.g. "ok bye", "that's all", "exit", "goodnight").`,
|
|
859
|
+
parameters: { type: "object", properties: {} },
|
|
860
|
+
async run() {
|
|
861
|
+
onExit();
|
|
862
|
+
return "Session ending. Goodbye!";
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
855
866
|
function defaultTools() {
|
|
856
867
|
return [bashTool, readTool, editTool];
|
|
857
868
|
}
|
|
@@ -1363,7 +1374,7 @@ function makeRealShellTool(options) {
|
|
|
1363
1374
|
const id = await options.registry.start(cmd);
|
|
1364
1375
|
return `Started background job ${id}. Poll output with ShellOutput({id:"${id}"}), check ShellStatus({id:"${id}"}), stop with ShellKill({id:"${id}"}).`;
|
|
1365
1376
|
}
|
|
1366
|
-
const
|
|
1377
|
+
const spawn3 = options.spawn ?? await nodeSpawn();
|
|
1367
1378
|
const ctl = new AbortController();
|
|
1368
1379
|
const onAbort = () => ctl.abort();
|
|
1369
1380
|
if (ctx.signal) {
|
|
@@ -1386,7 +1397,7 @@ function makeRealShellTool(options) {
|
|
|
1386
1397
|
};
|
|
1387
1398
|
let proc;
|
|
1388
1399
|
try {
|
|
1389
|
-
proc =
|
|
1400
|
+
proc = spawn3("/bin/sh", ["-c", cmd], { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });
|
|
1390
1401
|
} catch (e) {
|
|
1391
1402
|
return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
|
|
1392
1403
|
}
|
|
@@ -1397,7 +1408,7 @@ function makeRealShellTool(options) {
|
|
|
1397
1408
|
proc.stderr?.on("data", collect);
|
|
1398
1409
|
proc.on("error", (err2) => {
|
|
1399
1410
|
if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
1400
|
-
|
|
1411
|
+
log11.debug("shell spawn error", err2);
|
|
1401
1412
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
|
|
1402
1413
|
});
|
|
1403
1414
|
proc.on("close", (code) => {
|
|
@@ -1457,14 +1468,14 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
1457
1468
|
}
|
|
1458
1469
|
];
|
|
1459
1470
|
}
|
|
1460
|
-
var
|
|
1471
|
+
var log11, clean, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
|
|
1461
1472
|
var init_tools_shell = __esm({
|
|
1462
1473
|
"src/tools.shell.ts"() {
|
|
1463
1474
|
"use strict";
|
|
1464
1475
|
init_tools();
|
|
1465
1476
|
init_redact();
|
|
1466
1477
|
init_logging();
|
|
1467
|
-
|
|
1478
|
+
log11 = forComponent("shell");
|
|
1468
1479
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
1469
1480
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
1470
1481
|
ShellJobRegistry = class {
|
|
@@ -1484,8 +1495,8 @@ var init_tools_shell = __esm({
|
|
|
1484
1495
|
job.buf = (job.buf + s).slice(-max);
|
|
1485
1496
|
};
|
|
1486
1497
|
try {
|
|
1487
|
-
const
|
|
1488
|
-
const proc =
|
|
1498
|
+
const spawn3 = this.cfg.spawn ?? await nodeSpawn();
|
|
1499
|
+
const proc = spawn3("/bin/sh", ["-c", command], { cwd: this.cfg.cwd, env: childEnv(this.cfg) });
|
|
1489
1500
|
job.proc = proc;
|
|
1490
1501
|
proc.stdout?.on("data", append);
|
|
1491
1502
|
proc.stderr?.on("data", append);
|
|
@@ -1542,8 +1553,8 @@ var init_tools_shell = __esm({
|
|
|
1542
1553
|
|
|
1543
1554
|
// cli/cli.ts
|
|
1544
1555
|
import { createInterface } from "readline/promises";
|
|
1545
|
-
import { existsSync as
|
|
1546
|
-
import { homedir as
|
|
1556
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
1557
|
+
import { homedir as homedir5, tmpdir } from "os";
|
|
1547
1558
|
|
|
1548
1559
|
// cli/clipboard.ts
|
|
1549
1560
|
import { execFileSync } from "child_process";
|
|
@@ -1599,7 +1610,7 @@ close access f`;
|
|
|
1599
1610
|
}
|
|
1600
1611
|
|
|
1601
1612
|
// cli/cli.ts
|
|
1602
|
-
import { join as
|
|
1613
|
+
import { join as join9, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
|
|
1603
1614
|
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported } from "ai.libx.js";
|
|
1604
1615
|
|
|
1605
1616
|
// src/llm.ts
|
|
@@ -2839,7 +2850,15 @@ var Agent = class _Agent {
|
|
|
2839
2850
|
toolCallsTotal += toolCalls.length;
|
|
2840
2851
|
if (o.maxToolCalls && toolCallsTotal > o.maxToolCalls) return kill("max_tool_calls");
|
|
2841
2852
|
for (const tc of toolCalls) {
|
|
2842
|
-
const
|
|
2853
|
+
const raw = await this.dispatch(tc);
|
|
2854
|
+
let content;
|
|
2855
|
+
if (typeof raw === "string") {
|
|
2856
|
+
content = raw;
|
|
2857
|
+
} else {
|
|
2858
|
+
const parts = [{ type: "text", text: raw.text }];
|
|
2859
|
+
for (const img of raw.images ?? []) parts.push(imagePart(`data:${img.mimeType};base64,${img.data}`));
|
|
2860
|
+
content = parts;
|
|
2861
|
+
}
|
|
2843
2862
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
2844
2863
|
}
|
|
2845
2864
|
}
|
|
@@ -2896,10 +2915,17 @@ var Agent = class _Agent {
|
|
|
2896
2915
|
return earlyError;
|
|
2897
2916
|
}
|
|
2898
2917
|
let result;
|
|
2918
|
+
let images;
|
|
2899
2919
|
let threw = false;
|
|
2900
2920
|
try {
|
|
2901
2921
|
log3.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
2902
|
-
|
|
2922
|
+
const raw = await tool.run(args, this.ctx);
|
|
2923
|
+
if (typeof raw === "string") {
|
|
2924
|
+
result = raw;
|
|
2925
|
+
} else {
|
|
2926
|
+
result = raw.text;
|
|
2927
|
+
images = raw.images;
|
|
2928
|
+
}
|
|
2903
2929
|
} catch (e) {
|
|
2904
2930
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2905
2931
|
log3.debug(`${tc.function.name} -> error: ${msg}`);
|
|
@@ -2909,7 +2935,12 @@ var Agent = class _Agent {
|
|
|
2909
2935
|
if (!threw) result = await this.maybeAutoTest(tc.function.name, result);
|
|
2910
2936
|
await hooks?.postToolUse?.(call, result, meta);
|
|
2911
2937
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: result, isError: threw });
|
|
2912
|
-
|
|
2938
|
+
if (images?.length) {
|
|
2939
|
+
for (const img of images) {
|
|
2940
|
+
this.options.host?.notify?.({ kind: "tool_result_image", id: tc.id ?? "", dataUrl: `data:${img.mimeType};base64,${img.data}` });
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return images?.length ? { text: result, images } : result;
|
|
2913
2944
|
}
|
|
2914
2945
|
static WRITE_CLASS = ["Write", "Edit", "MultiEdit", "ApplyEdits"];
|
|
2915
2946
|
/** Append an autoTest failure section to a write-class tool result, if configured. */
|
|
@@ -3372,8 +3403,11 @@ var DuplexAgentOptions = class {
|
|
|
3372
3403
|
/** Awaited BEFORE a delegated worker spawns — open a per-task checkpoint frame, audit, etc.
|
|
3373
3404
|
* (post-spawn would race the worker's first edits). */
|
|
3374
3405
|
onTaskStart;
|
|
3406
|
+
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
3407
|
+
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
3408
|
+
quickLook;
|
|
3375
3409
|
};
|
|
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.';
|
|
3410
|
+
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.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not delegate, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file \u2014 use `QuickLook` (instant, no task). Anything requiring searching, reasoning, running commands, or editing still goes through `Delegate`.\nNEVER claim to have stored, saved, or remembered something durably \u2014 you cannot. Anything the user wants persisted (their name, preferences, notes) must be Delegated so a worker writes it to memory.\nUser messages may arrive via speech-to-text and can carry transcription artifacts \u2014 odd words, cut-offs, homophones ("for you" vs "folder"). Read for INTENT, not surface text. If a message seems garbled or surprising, briefly confirm what they meant ("did you mean\u2026?") instead of answering the literal words.';
|
|
3377
3411
|
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
3412
|
var DuplexAgent = class {
|
|
3379
3413
|
options;
|
|
@@ -3393,7 +3427,10 @@ var DuplexAgent = class {
|
|
|
3393
3427
|
model: o.voiceModel,
|
|
3394
3428
|
stream: true,
|
|
3395
3429
|
host: o.host,
|
|
3396
|
-
|
|
3430
|
+
// Runtime context line: without it the voice confidently invents "facts" like today's date
|
|
3431
|
+
// (its training cutoff) instead of delegating or admitting it doesn't know.
|
|
3432
|
+
systemPrompt: VOICE_SYSTEM_PROMPT + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
|
|
3433
|
+
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`,
|
|
3397
3434
|
instructionFiles: false,
|
|
3398
3435
|
maxSteps: 8,
|
|
3399
3436
|
// a voice turn should never loop
|
|
@@ -3402,7 +3439,7 @@ var DuplexAgent = class {
|
|
|
3402
3439
|
// no defaultTools() — the voice can only Delegate, never touch files itself. Set AFTER the
|
|
3403
3440
|
// voiceOptions spread (addTools() would be clobbered by the first prepare()); extra voice
|
|
3404
3441
|
// tools come in via voiceOptions.tools and are merged here.
|
|
3405
|
-
tools: [...o.voiceOptions?.tools ?? [], this.delegateTool(), this.taskStatusTool(), this.cancelTaskTool()]
|
|
3442
|
+
tools: [...o.voiceOptions?.tools ?? [], this.delegateTool(), this.taskStatusTool(), this.cancelTaskTool(), this.quickLookTool()]
|
|
3406
3443
|
});
|
|
3407
3444
|
}
|
|
3408
3445
|
/** One user turn: the voice agent streams the reply (and may Delegate). Serialized with re-voice turns. */
|
|
@@ -3527,6 +3564,58 @@ ${recent}` : brief;
|
|
|
3527
3564
|
}
|
|
3528
3565
|
};
|
|
3529
3566
|
}
|
|
3567
|
+
/** Sub-100ms read-only lookups the voice may do itself — everything else stays Delegate-only.
|
|
3568
|
+
* fs-only (no shell; the engine is VFS-abstracted): time, git branch (.git/HEAD read), ls, file
|
|
3569
|
+
* head. Output is hard-capped so a lookup can never bloat the skinny voice context. */
|
|
3570
|
+
quickLookTool() {
|
|
3571
|
+
const CAP2 = 2e3;
|
|
3572
|
+
const kinds = [.../* @__PURE__ */ new Set(["time", "branch", "ls", "file", ...Object.keys(this.options.quickLook ?? {})])];
|
|
3573
|
+
return {
|
|
3574
|
+
name: "QuickLook",
|
|
3575
|
+
description: `Instant read-only lookup \u2014 one of: ${kinds.join(", ")}. For trivial facts only; anything needing search, commands, or reasoning goes through Delegate.`,
|
|
3576
|
+
parameters: {
|
|
3577
|
+
type: "object",
|
|
3578
|
+
required: ["what"],
|
|
3579
|
+
properties: {
|
|
3580
|
+
what: { type: "string", enum: kinds, description: "what to look up" },
|
|
3581
|
+
path: { type: "string", description: "for ls/file: the path to look at" }
|
|
3582
|
+
}
|
|
3583
|
+
},
|
|
3584
|
+
run: async ({ what, path }) => {
|
|
3585
|
+
const fs = this.options.fs;
|
|
3586
|
+
try {
|
|
3587
|
+
const over = this.options.quickLook?.[String(what)];
|
|
3588
|
+
if (over) return await over(path ? String(path) : void 0);
|
|
3589
|
+
switch (String(what)) {
|
|
3590
|
+
case "time":
|
|
3591
|
+
return (/* @__PURE__ */ new Date()).toString();
|
|
3592
|
+
case "branch": {
|
|
3593
|
+
if (!fs) return "unavailable (no filesystem)";
|
|
3594
|
+
const head = (await fs.readFile(".git/HEAD")).trim();
|
|
3595
|
+
return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
|
|
3596
|
+
}
|
|
3597
|
+
case "ls": {
|
|
3598
|
+
if (!fs) return "unavailable (no filesystem)";
|
|
3599
|
+
const names = await fs.readDir(String(path ?? "."));
|
|
3600
|
+
return names.slice(0, 50).join("\n") + (names.length > 50 ? `
|
|
3601
|
+
\u2026 (+${names.length - 50} more)` : "");
|
|
3602
|
+
}
|
|
3603
|
+
case "file": {
|
|
3604
|
+
if (!fs) return "unavailable (no filesystem)";
|
|
3605
|
+
if (!path) return "file lookup needs a path";
|
|
3606
|
+
const text = await fs.readFile(String(path));
|
|
3607
|
+
return text.length > CAP2 ? text.slice(0, CAP2) + `
|
|
3608
|
+
\u2026 (truncated \u2014 ${text.length} chars total; Delegate for the full file)` : text;
|
|
3609
|
+
}
|
|
3610
|
+
default:
|
|
3611
|
+
return `unknown lookup '${what}'`;
|
|
3612
|
+
}
|
|
3613
|
+
} catch (e) {
|
|
3614
|
+
return `lookup failed: ${e?.message ?? e}`;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3530
3619
|
cancelTaskTool() {
|
|
3531
3620
|
return {
|
|
3532
3621
|
name: "CancelTask",
|
|
@@ -3545,15 +3634,26 @@ ${recent}` : brief;
|
|
|
3545
3634
|
};
|
|
3546
3635
|
|
|
3547
3636
|
// src/mcp.ts
|
|
3548
|
-
function
|
|
3549
|
-
if (result == null) return "";
|
|
3550
|
-
if (typeof result === "string") return result;
|
|
3637
|
+
function toResult(result) {
|
|
3638
|
+
if (result == null) return { text: "" };
|
|
3639
|
+
if (typeof result === "string") return { text: result };
|
|
3551
3640
|
const content = result.content;
|
|
3552
3641
|
if (Array.isArray(content)) {
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3642
|
+
const texts = [];
|
|
3643
|
+
const images = [];
|
|
3644
|
+
for (const c of content) {
|
|
3645
|
+
if (c?.type === "image" && typeof c.data === "string" && c.mimeType) {
|
|
3646
|
+
images.push({ mimeType: c.mimeType, data: c.data });
|
|
3647
|
+
} else if (typeof c?.text === "string") {
|
|
3648
|
+
texts.push(c.text);
|
|
3649
|
+
} else {
|
|
3650
|
+
texts.push(JSON.stringify(c));
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
const text = texts.join("\n");
|
|
3654
|
+
if (text || images.length) return { text, ...images.length ? { images } : {} };
|
|
3555
3655
|
}
|
|
3556
|
-
return JSON.stringify(result);
|
|
3656
|
+
return { text: JSON.stringify(result) };
|
|
3557
3657
|
}
|
|
3558
3658
|
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
|
|
3559
3659
|
return {
|
|
@@ -3561,7 +3661,8 @@ function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
|
|
|
3561
3661
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
3562
3662
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
3563
3663
|
async run(args, _ctx) {
|
|
3564
|
-
|
|
3664
|
+
const r = toResult(await callTool(spec.name, args ?? {}));
|
|
3665
|
+
return r.images?.length ? r : r.text;
|
|
3565
3666
|
}
|
|
3566
3667
|
};
|
|
3567
3668
|
}
|
|
@@ -3571,12 +3672,470 @@ function mcpToolsToAgentTools(specs, callTool, prefix = "mcp__", filter) {
|
|
|
3571
3672
|
|
|
3572
3673
|
// src/index.ts
|
|
3573
3674
|
init_logging();
|
|
3675
|
+
|
|
3676
|
+
// src/voice/engine.ts
|
|
3677
|
+
init_logging();
|
|
3678
|
+
var log7 = forComponent("VoiceEngine");
|
|
3679
|
+
var now = () => performance.now();
|
|
3680
|
+
var VoiceEngineOptions = class {
|
|
3681
|
+
stt;
|
|
3682
|
+
tts;
|
|
3683
|
+
player;
|
|
3684
|
+
/** a final utterance arrived (endpoint) — host dispatches it as a turn */
|
|
3685
|
+
onUtterance = () => {
|
|
3686
|
+
};
|
|
3687
|
+
/** live partial transcript while listening (host renders the 🎤 line) */
|
|
3688
|
+
onPartial = () => {
|
|
3689
|
+
};
|
|
3690
|
+
onState = () => {
|
|
3691
|
+
};
|
|
3692
|
+
/** user spoke/acted over playback — host aborts the in-flight turn (called AFTER audio is killed).
|
|
3693
|
+
* phase: 'speaking' = cut mid-speech (real interruption); 'drain' = in the final audio tail
|
|
3694
|
+
* (normal turn-taking — hosts shouldn't alarm). */
|
|
3695
|
+
onBargeIn = () => {
|
|
3696
|
+
};
|
|
3697
|
+
/** spoken micro-ack on utterance endpoint (masks LLM TTFT); '' disables */
|
|
3698
|
+
ackPhrase = "";
|
|
3699
|
+
/** Endpoint merge window (ms): hold an endpointed utterance briefly — if speech resumes (spelled
|
|
3700
|
+
* letters, mid-thought pauses), the next utterance MERGES instead of dispatching a truncated one
|
|
3701
|
+
* ("E-L-Y." / "A."). Costs this much latency per turn; 0 disables. */
|
|
3702
|
+
utteranceMergeMs = 350;
|
|
3703
|
+
/** heuristic (non-AEC) energy barge-in tuning */
|
|
3704
|
+
bargeRmsMult = 2;
|
|
3705
|
+
bargeRmsFloor = 500;
|
|
3706
|
+
};
|
|
3707
|
+
var VoiceEngine = class {
|
|
3708
|
+
options;
|
|
3709
|
+
state = "idle";
|
|
3710
|
+
stt;
|
|
3711
|
+
tts;
|
|
3712
|
+
player;
|
|
3713
|
+
speaking = false;
|
|
3714
|
+
// audible (deltas flowing OR audio draining)
|
|
3715
|
+
ctxOpen = false;
|
|
3716
|
+
// the current TTS context still accepts deltas (false once end-frame sent)
|
|
3717
|
+
interrupted = false;
|
|
3718
|
+
// barge-in latch: drop in-flight deltas until the next legitimate turn
|
|
3719
|
+
spokeDeltas = false;
|
|
3720
|
+
// a TTS context is open for the current spoken turn
|
|
3721
|
+
drainTimer = null;
|
|
3722
|
+
// heuristic tier state (inert under AEC) — frozen as validated in the experiment
|
|
3723
|
+
echoWords = /* @__PURE__ */ new Set();
|
|
3724
|
+
prevReply = "";
|
|
3725
|
+
reply = "";
|
|
3726
|
+
echoUntil = 0;
|
|
3727
|
+
baseline = 0;
|
|
3728
|
+
hot = 0;
|
|
3729
|
+
suspectUntil = 0;
|
|
3730
|
+
ackAt = 0;
|
|
3731
|
+
// when the micro-ack was spoken — its echo can leak before the AEC filter converges
|
|
3732
|
+
pendingUtt = "";
|
|
3733
|
+
// endpointed text held for the merge window
|
|
3734
|
+
pendingTimer = null;
|
|
3735
|
+
lastInterrupted = null;
|
|
3736
|
+
constructor(options) {
|
|
3737
|
+
this.options = { ...new VoiceEngineOptions(), ...options };
|
|
3738
|
+
const o = this.options;
|
|
3739
|
+
if (!o.stt || !o.tts || !o.player) throw new Error("VoiceEngine needs stt, tts and player (see cli/voice.ts VoiceIO for platform defaults)");
|
|
3740
|
+
this.stt = o.stt;
|
|
3741
|
+
this.tts = o.tts;
|
|
3742
|
+
this.player = o.player;
|
|
3743
|
+
}
|
|
3744
|
+
async start() {
|
|
3745
|
+
this.tts.onAudio = (c) => {
|
|
3746
|
+
if (this.speaking) this.player.write(c);
|
|
3747
|
+
};
|
|
3748
|
+
this.stt.onPartial = (text) => this.handlePartial(text);
|
|
3749
|
+
this.stt.onUtterance = (text) => this.handleUtterance(text);
|
|
3750
|
+
this.stt.onLevel = (rms) => this.handleLevel(rms);
|
|
3751
|
+
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
3752
|
+
this.setState("listening");
|
|
3753
|
+
log7.info(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
3754
|
+
}
|
|
3755
|
+
get usingAec() {
|
|
3756
|
+
return this.stt.usingAec;
|
|
3757
|
+
}
|
|
3758
|
+
idleWaiters = [];
|
|
3759
|
+
setState(s) {
|
|
3760
|
+
if (this.state === s) return;
|
|
3761
|
+
this.state = s;
|
|
3762
|
+
this.options.onState(s);
|
|
3763
|
+
if (s !== "speaking" && s !== "thinking") {
|
|
3764
|
+
for (const r of this.idleWaiters.splice(0)) r();
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
/** Resolve when the engine is no longer speaking (immediate if already idle). */
|
|
3768
|
+
awaitIdle() {
|
|
3769
|
+
if (this.state !== "speaking" && this.state !== "thinking") return Promise.resolve();
|
|
3770
|
+
return new Promise((r) => this.idleWaiters.push(r));
|
|
3771
|
+
}
|
|
3772
|
+
// --- speaking side (host-driven) ---
|
|
3773
|
+
/** open a spoken turn (idempotent — safe from both onUtterance and first-delta paths).
|
|
3774
|
+
* `ack` speaks the configured micro-ack as the context opener (utterance path only —
|
|
3775
|
+
* masks LLM TTFT; re-voice turns begun by their first delta skip it). */
|
|
3776
|
+
beginSpeech(ack = false) {
|
|
3777
|
+
if (this.speaking && this.ctxOpen) return;
|
|
3778
|
+
if (this.drainTimer) {
|
|
3779
|
+
clearTimeout(this.drainTimer);
|
|
3780
|
+
this.drainTimer = null;
|
|
3781
|
+
}
|
|
3782
|
+
this.interrupted = false;
|
|
3783
|
+
if (!this.speaking) this.player.markTurn();
|
|
3784
|
+
this.speaking = true;
|
|
3785
|
+
this.ctxOpen = true;
|
|
3786
|
+
this.spokeDeltas = false;
|
|
3787
|
+
this.reply = "";
|
|
3788
|
+
this.echoWords = new Set(this.words(this.prevReply));
|
|
3789
|
+
this.tts.newContext();
|
|
3790
|
+
if (ack && this.options.ackPhrase) {
|
|
3791
|
+
this.tts.speak(this.options.ackPhrase + " ", true);
|
|
3792
|
+
this.spokeDeltas = true;
|
|
3793
|
+
this.ackAt = now();
|
|
3794
|
+
}
|
|
3795
|
+
this.setState("thinking");
|
|
3796
|
+
}
|
|
3797
|
+
speakDelta(text) {
|
|
3798
|
+
if (this.interrupted) return;
|
|
3799
|
+
if (!this.speaking || !this.ctxOpen) this.beginSpeech();
|
|
3800
|
+
this.reply += text;
|
|
3801
|
+
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
3802
|
+
this.tts.speak(text, true);
|
|
3803
|
+
this.spokeDeltas = true;
|
|
3804
|
+
this.setState("speaking");
|
|
3805
|
+
}
|
|
3806
|
+
/** close the spoken turn (idempotent); stays audible until ALL audio arrived AND playback drains */
|
|
3807
|
+
endSpeech() {
|
|
3808
|
+
this.interrupted = false;
|
|
3809
|
+
if (!this.speaking) return;
|
|
3810
|
+
this.ctxOpen = false;
|
|
3811
|
+
if (this.reply) this.prevReply = this.reply;
|
|
3812
|
+
const settle = () => {
|
|
3813
|
+
if (this.ctxOpen) {
|
|
3814
|
+
this.drainTimer = null;
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
this.drainTimer = null;
|
|
3818
|
+
this.speaking = false;
|
|
3819
|
+
this.echoUntil = now() + 2500;
|
|
3820
|
+
if (!this.usingAec) this.stt.reset();
|
|
3821
|
+
this.setState("listening");
|
|
3822
|
+
};
|
|
3823
|
+
const drainThenSettle = () => {
|
|
3824
|
+
if (this.drainTimer) clearTimeout(this.drainTimer);
|
|
3825
|
+
this.drainTimer = setTimeout(settle, this.player.drainMs() + 300);
|
|
3826
|
+
};
|
|
3827
|
+
if (this.spokeDeltas) {
|
|
3828
|
+
this.tts.onDone = drainThenSettle;
|
|
3829
|
+
this.tts.end();
|
|
3830
|
+
if (this.drainTimer) clearTimeout(this.drainTimer);
|
|
3831
|
+
this.drainTimer = setTimeout(drainThenSettle, 15e3);
|
|
3832
|
+
} else drainThenSettle();
|
|
3833
|
+
}
|
|
3834
|
+
/** text of the reply cut by the last barge-in — consumed by the host to tell the model what
|
|
3835
|
+
* the user did NOT hear. Cleared on read. */
|
|
3836
|
+
takeInterruptedReply() {
|
|
3837
|
+
const r = this.lastInterrupted;
|
|
3838
|
+
this.lastInterrupted = null;
|
|
3839
|
+
return r;
|
|
3840
|
+
}
|
|
3841
|
+
/** barge-in: stop audio NOW, cancel generation, reset for the user's utterance */
|
|
3842
|
+
interrupt() {
|
|
3843
|
+
if (!this.speaking && !this.drainTimer) return;
|
|
3844
|
+
if (this.drainTimer) {
|
|
3845
|
+
clearTimeout(this.drainTimer);
|
|
3846
|
+
this.drainTimer = null;
|
|
3847
|
+
}
|
|
3848
|
+
const heardChars = Math.round(Math.max(0, this.player.playedMs()) / 1e3 * 15);
|
|
3849
|
+
if (this.reply) this.lastInterrupted = { full: this.reply, heard: this.reply.slice(0, heardChars) };
|
|
3850
|
+
this.speaking = false;
|
|
3851
|
+
this.ctxOpen = false;
|
|
3852
|
+
this.interrupted = true;
|
|
3853
|
+
this.suspectUntil = 0;
|
|
3854
|
+
this.echoUntil = now() + 2500;
|
|
3855
|
+
this.tts.cancel();
|
|
3856
|
+
this.player.kill();
|
|
3857
|
+
if (!this.usingAec) this.stt.reset();
|
|
3858
|
+
if (this.reply) this.prevReply = this.reply;
|
|
3859
|
+
this.setState("listening");
|
|
3860
|
+
}
|
|
3861
|
+
stop() {
|
|
3862
|
+
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
3863
|
+
if (this.drainTimer) clearTimeout(this.drainTimer);
|
|
3864
|
+
this.stt.stop();
|
|
3865
|
+
this.player.kill();
|
|
3866
|
+
this.tts.close();
|
|
3867
|
+
this.setState("idle");
|
|
3868
|
+
}
|
|
3869
|
+
// --- listening side (STT-driven) ---
|
|
3870
|
+
words(s) {
|
|
3871
|
+
return s.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length >= 2);
|
|
3872
|
+
}
|
|
3873
|
+
novelWords(text) {
|
|
3874
|
+
return this.words(text).filter((w) => !this.echoWords.has(w));
|
|
3875
|
+
}
|
|
3876
|
+
echoActive() {
|
|
3877
|
+
return this.speaking || now() < this.echoUntil;
|
|
3878
|
+
}
|
|
3879
|
+
handlePartial(text) {
|
|
3880
|
+
if (this.speaking) {
|
|
3881
|
+
const barge = this.novelWords(text).length >= (this.usingAec ? 1 : this.suspectUntil ? 1 : 2);
|
|
3882
|
+
if (barge) {
|
|
3883
|
+
const phase = this.ctxOpen ? "speaking" : "drain";
|
|
3884
|
+
this.interrupt();
|
|
3885
|
+
this.options.onBargeIn(phase);
|
|
3886
|
+
}
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
if (this.pendingUtt && text.trim()) {
|
|
3890
|
+
if (this.pendingTimer) {
|
|
3891
|
+
clearTimeout(this.pendingTimer);
|
|
3892
|
+
this.pendingTimer = null;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
if (!this.echoActive() || this.novelWords(text).length >= 1) this.options.onPartial(text);
|
|
3896
|
+
}
|
|
3897
|
+
handleUtterance(text) {
|
|
3898
|
+
if (this.echoActive() && this.novelWords(text).length < (this.usingAec ? 1 : 2)) {
|
|
3899
|
+
this.stt.reset();
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
const squash = (t) => t.toLowerCase().replace(/[^a-z]/g, "").replace(/(.)\1+/g, "$1");
|
|
3903
|
+
if (this.ackAt && now() - this.ackAt < 6e3 && squash(text) === squash(this.options.ackPhrase)) {
|
|
3904
|
+
this.ackAt = 0;
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
this.pendingUtt = this.pendingUtt ? `${this.pendingUtt} ${text}` : text;
|
|
3908
|
+
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
3909
|
+
if (!this.options.utteranceMergeMs) return this.flushUtterance();
|
|
3910
|
+
this.pendingTimer = setTimeout(() => this.flushUtterance(), this.options.utteranceMergeMs);
|
|
3911
|
+
}
|
|
3912
|
+
flushUtterance() {
|
|
3913
|
+
if (this.pendingTimer) {
|
|
3914
|
+
clearTimeout(this.pendingTimer);
|
|
3915
|
+
this.pendingTimer = null;
|
|
3916
|
+
}
|
|
3917
|
+
const text = this.pendingUtt;
|
|
3918
|
+
this.pendingUtt = "";
|
|
3919
|
+
if (text) this.options.onUtterance(text);
|
|
3920
|
+
}
|
|
3921
|
+
/** energy two-stage barge-in (heuristic tier only): spike over echo baseline → pause + confirm via STT */
|
|
3922
|
+
handleLevel(rms) {
|
|
3923
|
+
if (this.usingAec) return;
|
|
3924
|
+
if (!this.speaking) {
|
|
3925
|
+
this.baseline = 0;
|
|
3926
|
+
this.hot = 0;
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
if (!this.baseline) {
|
|
3930
|
+
this.baseline = rms;
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
this.baseline = this.baseline * 0.9 + rms * 0.1;
|
|
3934
|
+
if (rms > Math.max(this.baseline * this.options.bargeRmsMult, this.options.bargeRmsFloor)) this.hot++;
|
|
3935
|
+
else this.hot = 0;
|
|
3936
|
+
if (this.hot >= 2 && !this.suspectUntil) {
|
|
3937
|
+
this.suspectUntil = now() + 1300;
|
|
3938
|
+
setTimeout(() => {
|
|
3939
|
+
this.suspectUntil = 0;
|
|
3940
|
+
}, 1350);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
|
|
3945
|
+
// src/voice/soniox.ts
|
|
3946
|
+
init_logging();
|
|
3947
|
+
|
|
3948
|
+
// src/voice/types.ts
|
|
3949
|
+
var STT_SAMPLE_RATE = 16e3;
|
|
3950
|
+
var TTS_SAMPLE_RATE = 44100;
|
|
3951
|
+
async function resolveAuth(auth) {
|
|
3952
|
+
return typeof auth === "function" ? await auth() : auth;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
// src/voice/soniox.ts
|
|
3956
|
+
var log8 = forComponent("SonioxSTT");
|
|
3957
|
+
var now2 = () => performance.now();
|
|
3958
|
+
var SonioxSTTOptions = class {
|
|
3959
|
+
auth = "";
|
|
3960
|
+
source;
|
|
3961
|
+
model = "stt-rt-preview";
|
|
3962
|
+
languageHints = ["en"];
|
|
3963
|
+
};
|
|
3964
|
+
var SonioxSTT = class {
|
|
3965
|
+
options;
|
|
3966
|
+
ws;
|
|
3967
|
+
stopped = false;
|
|
3968
|
+
sourceStarted = false;
|
|
3969
|
+
onPartial = () => {
|
|
3970
|
+
};
|
|
3971
|
+
onUtterance = () => {
|
|
3972
|
+
};
|
|
3973
|
+
/** mic energy (RMS) per chunk — drives the energy-based heuristic barge-in tier */
|
|
3974
|
+
onLevel = () => {
|
|
3975
|
+
};
|
|
3976
|
+
finalText = "";
|
|
3977
|
+
partialText = "";
|
|
3978
|
+
constructor(options) {
|
|
3979
|
+
this.options = { ...new SonioxSTTOptions(), ...options };
|
|
3980
|
+
}
|
|
3981
|
+
get usingAec() {
|
|
3982
|
+
return this.options.source?.aec ?? false;
|
|
3983
|
+
}
|
|
3984
|
+
async connectWs() {
|
|
3985
|
+
const apiKey = await resolveAuth(this.options.auth);
|
|
3986
|
+
this.ws = new WebSocket("wss://stt-rt.soniox.com/transcribe-websocket");
|
|
3987
|
+
await new Promise((res, rej) => {
|
|
3988
|
+
this.ws.onopen = () => res();
|
|
3989
|
+
this.ws.onerror = (e) => rej(new Error(`soniox ws: ${e.message || "connect failed"}`));
|
|
3990
|
+
});
|
|
3991
|
+
this.ws.send(
|
|
3992
|
+
JSON.stringify({
|
|
3993
|
+
api_key: apiKey,
|
|
3994
|
+
model: this.options.model,
|
|
3995
|
+
audio_format: "pcm_s16le",
|
|
3996
|
+
sample_rate: STT_SAMPLE_RATE,
|
|
3997
|
+
num_channels: 1,
|
|
3998
|
+
language_hints: this.options.languageHints,
|
|
3999
|
+
enable_endpoint_detection: true
|
|
4000
|
+
})
|
|
4001
|
+
);
|
|
4002
|
+
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
4003
|
+
this.ws.onclose = (ev) => {
|
|
4004
|
+
if (this.stopped) return;
|
|
4005
|
+
log8.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
4006
|
+
this.reset();
|
|
4007
|
+
this.connectWs().catch((e) => log8.error(`soniox reconnect failed: ${e.message}`));
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
async start() {
|
|
4011
|
+
await this.connectWs();
|
|
4012
|
+
if (this.sourceStarted) return;
|
|
4013
|
+
this.sourceStarted = true;
|
|
4014
|
+
await this.options.source.start((chunk) => {
|
|
4015
|
+
let sum = 0;
|
|
4016
|
+
const view = new DataView(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
4017
|
+
for (let i = 0; i + 1 < chunk.byteLength; i += 2) {
|
|
4018
|
+
const v = view.getInt16(i, true);
|
|
4019
|
+
sum += v * v;
|
|
4020
|
+
}
|
|
4021
|
+
this.onLevel(Math.sqrt(sum / (chunk.byteLength / 2)));
|
|
4022
|
+
if (this.ws.readyState === WebSocket.OPEN) this.ws.send(chunk);
|
|
4023
|
+
});
|
|
4024
|
+
}
|
|
4025
|
+
handle(m) {
|
|
4026
|
+
if (m.error_message) return log8.error(`soniox: ${m.error_message}`);
|
|
4027
|
+
let endpoint = false;
|
|
4028
|
+
for (const t of m.tokens ?? []) {
|
|
4029
|
+
if (t.text === "<end>") endpoint = true;
|
|
4030
|
+
else if (t.is_final) this.finalText += t.text;
|
|
4031
|
+
}
|
|
4032
|
+
this.partialText = (m.tokens ?? []).filter((t) => !t.is_final && t.text !== "<end>").map((t) => t.text).join("");
|
|
4033
|
+
this.onPartial(this.finalText + this.partialText);
|
|
4034
|
+
if (endpoint && this.finalText.trim()) {
|
|
4035
|
+
const utterance = this.finalText.trim();
|
|
4036
|
+
this.reset();
|
|
4037
|
+
this.onUtterance(utterance, now2());
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
reset() {
|
|
4041
|
+
this.finalText = "";
|
|
4042
|
+
this.partialText = "";
|
|
4043
|
+
}
|
|
4044
|
+
stop() {
|
|
4045
|
+
this.stopped = true;
|
|
4046
|
+
this.options.source?.stop();
|
|
4047
|
+
if (this.ws) this.ws.onclose = null;
|
|
4048
|
+
this.ws?.close();
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
|
|
4052
|
+
// src/voice/cartesia.ts
|
|
4053
|
+
init_logging();
|
|
4054
|
+
var log9 = forComponent("CartesiaTTS");
|
|
4055
|
+
var now3 = () => performance.now();
|
|
4056
|
+
var CartesiaTTSOptions = class {
|
|
4057
|
+
auth = "";
|
|
4058
|
+
voiceId = "";
|
|
4059
|
+
model = "sonic-3.5";
|
|
4060
|
+
/** 'apiKey' (server/CLI) → `api_key=` URL param; 'token' (browser, BE-minted) → `access_token=`. */
|
|
4061
|
+
authMode = "apiKey";
|
|
4062
|
+
};
|
|
4063
|
+
var CartesiaTTS = class {
|
|
4064
|
+
options;
|
|
4065
|
+
ws;
|
|
4066
|
+
ctxSeq = 0;
|
|
4067
|
+
ctxId = "";
|
|
4068
|
+
onAudio = () => {
|
|
4069
|
+
};
|
|
4070
|
+
onDone = () => {
|
|
4071
|
+
};
|
|
4072
|
+
firstAudioAt = 0;
|
|
4073
|
+
constructor(options) {
|
|
4074
|
+
this.options = { ...new CartesiaTTSOptions(), ...options };
|
|
4075
|
+
}
|
|
4076
|
+
async connect() {
|
|
4077
|
+
const key = await resolveAuth(this.options.auth);
|
|
4078
|
+
const param = this.options.authMode === "token" ? "access_token" : "api_key";
|
|
4079
|
+
this.ws = new WebSocket(`wss://api.cartesia.ai/tts/websocket?cartesia_version=2026-03-01&${param}=${key}`);
|
|
4080
|
+
await new Promise((res, rej) => {
|
|
4081
|
+
this.ws.onopen = () => res();
|
|
4082
|
+
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
4083
|
+
});
|
|
4084
|
+
this.ws.onclose = (ev) => log9.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
4085
|
+
this.ws.onmessage = (ev) => {
|
|
4086
|
+
const m = JSON.parse(String(ev.data));
|
|
4087
|
+
if (m.context_id && m.context_id !== this.ctxId) return;
|
|
4088
|
+
if (m.type === "chunk" && m.data) {
|
|
4089
|
+
if (!this.firstAudioAt) this.firstAudioAt = now3();
|
|
4090
|
+
this.onAudio(base64ToBytes(m.data));
|
|
4091
|
+
} else if (m.type === "done") this.onDone();
|
|
4092
|
+
else if (m.type === "error" && !/already been cancelled|does not exist/.test(m.message || "")) log9.warn(`cartesia: ${JSON.stringify(m)}`);
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
newContext() {
|
|
4096
|
+
this.ctxId = `ctx-${++this.ctxSeq}`;
|
|
4097
|
+
this.firstAudioAt = 0;
|
|
4098
|
+
return this.ctxId;
|
|
4099
|
+
}
|
|
4100
|
+
frame(transcript, cont) {
|
|
4101
|
+
return JSON.stringify({
|
|
4102
|
+
model_id: this.options.model,
|
|
4103
|
+
transcript,
|
|
4104
|
+
voice: { mode: "id", id: this.options.voiceId },
|
|
4105
|
+
output_format: { container: "raw", encoding: "pcm_s16le", sample_rate: TTS_SAMPLE_RATE },
|
|
4106
|
+
context_id: this.ctxId,
|
|
4107
|
+
continue: cont
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
speak(text, cont) {
|
|
4111
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(this.frame(text, cont));
|
|
4112
|
+
}
|
|
4113
|
+
end() {
|
|
4114
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(this.frame("", false));
|
|
4115
|
+
}
|
|
4116
|
+
cancel() {
|
|
4117
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify({ context_id: this.ctxId, cancel: true }));
|
|
4118
|
+
}
|
|
4119
|
+
close() {
|
|
4120
|
+
if (this.ws) this.ws.onclose = null;
|
|
4121
|
+
this.ws?.close();
|
|
4122
|
+
}
|
|
4123
|
+
};
|
|
4124
|
+
function base64ToBytes(b64) {
|
|
4125
|
+
if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64");
|
|
4126
|
+
const bin = atob(b64);
|
|
4127
|
+
const out = new Uint8Array(bin.length);
|
|
4128
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
4129
|
+
return out;
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
// src/index.ts
|
|
3574
4133
|
import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
|
|
3575
4134
|
|
|
3576
4135
|
// src/mcp.client.ts
|
|
3577
4136
|
init_logging();
|
|
3578
4137
|
import { spawn } from "child_process";
|
|
3579
|
-
var
|
|
4138
|
+
var log10 = forComponent("mcp");
|
|
3580
4139
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
3581
4140
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
3582
4141
|
var StdioTransport = class {
|
|
@@ -3595,7 +4154,7 @@ var StdioTransport = class {
|
|
|
3595
4154
|
proc.stdout.setEncoding("utf8");
|
|
3596
4155
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
3597
4156
|
proc.stderr.setEncoding("utf8");
|
|
3598
|
-
proc.stderr.on("data", (chunk) =>
|
|
4157
|
+
proc.stderr.on("data", (chunk) => log10.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
3599
4158
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
3600
4159
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
3601
4160
|
}
|
|
@@ -3609,7 +4168,7 @@ var StdioTransport = class {
|
|
|
3609
4168
|
try {
|
|
3610
4169
|
this.dispatch(JSON.parse(line));
|
|
3611
4170
|
} catch (e) {
|
|
3612
|
-
|
|
4171
|
+
log10.debug("dropping non-JSON line from MCP server:", line, e);
|
|
3613
4172
|
}
|
|
3614
4173
|
}
|
|
3615
4174
|
}
|
|
@@ -3658,7 +4217,7 @@ var StdioTransport = class {
|
|
|
3658
4217
|
try {
|
|
3659
4218
|
this.proc?.stdin?.end();
|
|
3660
4219
|
} catch (e) {
|
|
3661
|
-
|
|
4220
|
+
log10.debug("stdin end failed", e);
|
|
3662
4221
|
}
|
|
3663
4222
|
this.proc?.kill();
|
|
3664
4223
|
}
|
|
@@ -3727,7 +4286,7 @@ function parseSseResponse(body) {
|
|
|
3727
4286
|
const obj = JSON.parse(trimmed.slice(5).trim());
|
|
3728
4287
|
if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
|
|
3729
4288
|
} catch (e) {
|
|
3730
|
-
|
|
4289
|
+
log10.debug("skipping unparseable SSE data line", e);
|
|
3731
4290
|
}
|
|
3732
4291
|
}
|
|
3733
4292
|
return {};
|
|
@@ -3781,16 +4340,16 @@ async function mountMcpServers(servers = {}) {
|
|
|
3781
4340
|
for (const [name, cfg] of Object.entries(servers)) {
|
|
3782
4341
|
if (!cfg || cfg.disabled) continue;
|
|
3783
4342
|
if (!cfg.command && !cfg.url) {
|
|
3784
|
-
|
|
4343
|
+
log10.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
3785
4344
|
continue;
|
|
3786
4345
|
}
|
|
3787
4346
|
try {
|
|
3788
4347
|
const m = await mountMcpServer(name, cfg);
|
|
3789
4348
|
out.push(m);
|
|
3790
|
-
|
|
4349
|
+
log10.info(`MCP "${name}" mounted \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}`);
|
|
3791
4350
|
} catch (e) {
|
|
3792
|
-
if (e instanceof McpAuthError)
|
|
3793
|
-
else
|
|
4351
|
+
if (e instanceof McpAuthError) log10.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
4352
|
+
else log10.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
3794
4353
|
}
|
|
3795
4354
|
}
|
|
3796
4355
|
return out;
|
|
@@ -3986,7 +4545,7 @@ function b64url(buf) {
|
|
|
3986
4545
|
function defaultOpenBrowser(url) {
|
|
3987
4546
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
3988
4547
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
3989
|
-
import("child_process").then(({ spawn:
|
|
4548
|
+
import("child_process").then(({ spawn: spawn3 }) => spawn3(cmd, args, { stdio: "ignore", detached: true }).unref());
|
|
3990
4549
|
}
|
|
3991
4550
|
|
|
3992
4551
|
// cli/core.ts
|
|
@@ -4121,9 +4680,9 @@ Reference files in them by their mount path (the left side).`;
|
|
|
4121
4680
|
// would corrupt those calls.
|
|
4122
4681
|
...isCursor ? { providerOptions: { cwd, ...toCursorMcp(o.mcpServers) ?? {} } } : {},
|
|
4123
4682
|
...(() => {
|
|
4124
|
-
const
|
|
4683
|
+
const now5 = /* @__PURE__ */ new Date();
|
|
4125
4684
|
const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
|
|
4126
|
-
const envNote = `Current date: ${
|
|
4685
|
+
const envNote = `Current date: ${now5.toLocaleDateString("en-CA")} ${now5.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })} (${Intl.DateTimeFormat().resolvedOptions().timeZone})
|
|
4127
4686
|
Platform: ${platformNames[platform()] ?? platform()} ${arch()} (${release()})
|
|
4128
4687
|
User: ${userInfo().username}
|
|
4129
4688
|
Shell: ${process.env.SHELL ?? "unknown"}`;
|
|
@@ -4188,16 +4747,162 @@ function summarizeCall(name, args) {
|
|
|
4188
4747
|
}
|
|
4189
4748
|
var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0, n) + "\u2026" : String(s)).replace(/\n/g, "\u23CE");
|
|
4190
4749
|
|
|
4191
|
-
// cli/
|
|
4750
|
+
// cli/voice.ts
|
|
4751
|
+
init_logging();
|
|
4752
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
4753
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
|
|
4192
4754
|
import { homedir as homedir2 } from "os";
|
|
4193
|
-
import {
|
|
4194
|
-
import {
|
|
4755
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
4756
|
+
import { fileURLToPath } from "url";
|
|
4757
|
+
var log12 = forComponent("VoiceIO");
|
|
4758
|
+
var now4 = () => performance.now();
|
|
4759
|
+
var Player = class {
|
|
4760
|
+
proc = null;
|
|
4761
|
+
bytesWritten = 0;
|
|
4762
|
+
startedAt = 0;
|
|
4763
|
+
/** start a new spoken turn: kill any previous player, spawn a fresh one */
|
|
4764
|
+
markTurn() {
|
|
4765
|
+
this.kill();
|
|
4766
|
+
this.proc = spawn2(
|
|
4767
|
+
"ffplay",
|
|
4768
|
+
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
4769
|
+
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
4770
|
+
);
|
|
4771
|
+
this.proc.on("error", (e) => log12.warn(`ffplay error: ${e.message}`));
|
|
4772
|
+
this.proc.stdin.on("error", () => {
|
|
4773
|
+
});
|
|
4774
|
+
this.bytesWritten = 0;
|
|
4775
|
+
this.startedAt = 0;
|
|
4776
|
+
}
|
|
4777
|
+
write(chunk) {
|
|
4778
|
+
if (!this.proc) this.markTurn();
|
|
4779
|
+
if (!this.startedAt) this.startedAt = now4();
|
|
4780
|
+
this.bytesWritten += chunk.length;
|
|
4781
|
+
this.proc.stdin.write(chunk);
|
|
4782
|
+
}
|
|
4783
|
+
/** ms of audio actually played so far this turn */
|
|
4784
|
+
playedMs() {
|
|
4785
|
+
return this.startedAt ? now4() - this.startedAt : 0;
|
|
4786
|
+
}
|
|
4787
|
+
/** estimated ms until queued audio finishes playing */
|
|
4788
|
+
drainMs() {
|
|
4789
|
+
if (!this.startedAt) return 0;
|
|
4790
|
+
const queuedMs = this.bytesWritten / (TTS_SAMPLE_RATE * 2) * 1e3;
|
|
4791
|
+
return Math.max(0, queuedMs - (now4() - this.startedAt));
|
|
4792
|
+
}
|
|
4793
|
+
kill() {
|
|
4794
|
+
this.proc?.kill("SIGKILL");
|
|
4795
|
+
this.proc = null;
|
|
4796
|
+
}
|
|
4797
|
+
};
|
|
4798
|
+
var nativeDir = () => join4(dirname3(fileURLToPath(import.meta.url)), "native");
|
|
4799
|
+
function detectFfmpegMic() {
|
|
4800
|
+
if (process.env.MIC_DEVICE) return process.env.MIC_DEVICE;
|
|
4801
|
+
const out = spawnSync("ffmpeg", ["-f", "avfoundation", "-list_devices", "true", "-i", ""], { encoding: "utf8" }).stderr;
|
|
4802
|
+
const audio = out.slice(out.indexOf("audio devices"));
|
|
4803
|
+
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
4804
|
+
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
4805
|
+
if (!mic) throw new Error("no audio input device found");
|
|
4806
|
+
log12.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
4807
|
+
return `:${mic.idx}`;
|
|
4808
|
+
}
|
|
4809
|
+
function resolveAecBinary() {
|
|
4810
|
+
if (process.env.MIC_AEC === "0" || process.platform !== "darwin") return null;
|
|
4811
|
+
const src = join4(nativeDir(), "mic-aec.swift");
|
|
4812
|
+
const plist = join4(nativeDir(), "Info.plist");
|
|
4813
|
+
if (!existsSync3(src)) return null;
|
|
4814
|
+
const cacheDir = join4(homedir2(), ".agent", "cache");
|
|
4815
|
+
const bin = join4(cacheDir, "mic-aec");
|
|
4816
|
+
if (existsSync3(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
|
|
4817
|
+
if (spawnSync("which", ["swiftc"]).status !== 0) return null;
|
|
4818
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
4819
|
+
log12.info("compiling AEC mic helper (first run)\u2026");
|
|
4820
|
+
const build = spawnSync("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
4821
|
+
if (build.status !== 0) {
|
|
4822
|
+
log12.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
4823
|
+
return null;
|
|
4824
|
+
}
|
|
4825
|
+
const sign = spawnSync("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
4826
|
+
if (sign.status !== 0) {
|
|
4827
|
+
log12.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
4828
|
+
return null;
|
|
4829
|
+
}
|
|
4830
|
+
return bin;
|
|
4831
|
+
}
|
|
4832
|
+
var NodeMicSource = class {
|
|
4833
|
+
aec;
|
|
4834
|
+
bin;
|
|
4835
|
+
proc = null;
|
|
4836
|
+
stopped = false;
|
|
4837
|
+
constructor() {
|
|
4838
|
+
this.bin = resolveAecBinary();
|
|
4839
|
+
this.aec = !!this.bin;
|
|
4840
|
+
}
|
|
4841
|
+
start(onChunk) {
|
|
4842
|
+
if (this.bin) {
|
|
4843
|
+
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
4844
|
+
} else {
|
|
4845
|
+
if (spawnSync("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
4846
|
+
log12.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
4847
|
+
this.proc = spawn2(
|
|
4848
|
+
"ffmpeg",
|
|
4849
|
+
["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
4850
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
4851
|
+
);
|
|
4852
|
+
this.proc.stderr.on("data", (d) => log12.warn(`ffmpeg: ${String(d).trim()}`));
|
|
4853
|
+
}
|
|
4854
|
+
this.proc.on("exit", (c) => {
|
|
4855
|
+
if (c && !this.stopped) log12.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
4856
|
+
});
|
|
4857
|
+
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
4858
|
+
}
|
|
4859
|
+
stop() {
|
|
4860
|
+
this.stopped = true;
|
|
4861
|
+
const p = this.proc;
|
|
4862
|
+
this.proc = null;
|
|
4863
|
+
if (!p) return;
|
|
4864
|
+
p.kill("SIGTERM");
|
|
4865
|
+
setTimeout(() => {
|
|
4866
|
+
try {
|
|
4867
|
+
p.kill("SIGKILL");
|
|
4868
|
+
} catch {
|
|
4869
|
+
}
|
|
4870
|
+
}, 500).unref?.();
|
|
4871
|
+
}
|
|
4872
|
+
};
|
|
4873
|
+
var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
4874
|
+
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
4875
|
+
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
4876
|
+
cartesiaVoiceId = process.env.CARTESIA_VOICE_ID ?? "";
|
|
4877
|
+
};
|
|
4878
|
+
var VoiceIO = class extends VoiceEngine {
|
|
4879
|
+
constructor(options) {
|
|
4880
|
+
const o = { ...new VoiceIOOptions(), ...options };
|
|
4881
|
+
super({
|
|
4882
|
+
...o,
|
|
4883
|
+
stt: o.stt ?? new SonioxSTT({ auth: o.sonioxApiKey, source: new NodeMicSource() }),
|
|
4884
|
+
tts: o.tts ?? new CartesiaTTS({ auth: o.cartesiaApiKey, voiceId: o.cartesiaVoiceId }),
|
|
4885
|
+
player: o.player ?? new Player(),
|
|
4886
|
+
bargeRmsMult: Number(process.env.BARGE_RMS_MULT || o.bargeRmsMult),
|
|
4887
|
+
bargeRmsFloor: Number(process.env.BARGE_RMS_FLOOR || o.bargeRmsFloor)
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
/** ready = keys present (AEC vs heuristic is decided at start()) */
|
|
4891
|
+
static available(env = process.env) {
|
|
4892
|
+
return !!(env.SONIOX_API_KEY && env.CARTESIA_API_KEY && env.CARTESIA_VOICE_ID);
|
|
4893
|
+
}
|
|
4894
|
+
};
|
|
4895
|
+
|
|
4896
|
+
// cli/config.ts
|
|
4897
|
+
import { homedir as homedir3 } from "os";
|
|
4898
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
4899
|
+
import { join as join5 } from "path";
|
|
4195
4900
|
import { pathToFileURL } from "url";
|
|
4196
4901
|
var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
|
|
4197
4902
|
async function loadFrom(dir) {
|
|
4198
4903
|
for (const f of FILES) {
|
|
4199
|
-
const p =
|
|
4200
|
-
if (!
|
|
4904
|
+
const p = join5(dir, ".agent", f);
|
|
4905
|
+
if (!existsSync4(p)) continue;
|
|
4201
4906
|
try {
|
|
4202
4907
|
const mod = await import(pathToFileURL(p).href, f.endsWith(".json") ? { with: { type: "json" } } : void 0);
|
|
4203
4908
|
return mod.default ?? mod.config ?? mod;
|
|
@@ -4209,8 +4914,8 @@ async function loadFrom(dir) {
|
|
|
4209
4914
|
return {};
|
|
4210
4915
|
}
|
|
4211
4916
|
function loadSettings(dir) {
|
|
4212
|
-
const p =
|
|
4213
|
-
if (!
|
|
4917
|
+
const p = join5(dir, ".agent", "settings.json");
|
|
4918
|
+
if (!existsSync4(p)) return {};
|
|
4214
4919
|
try {
|
|
4215
4920
|
const raw = JSON.parse(readFileSync2(p, "utf8"));
|
|
4216
4921
|
const cfg = {};
|
|
@@ -4233,8 +4938,8 @@ function loadSettings(dir) {
|
|
|
4233
4938
|
}
|
|
4234
4939
|
}
|
|
4235
4940
|
async function loadConfig(cwd) {
|
|
4236
|
-
const userSettings = loadSettings(
|
|
4237
|
-
const user = await loadFrom(
|
|
4941
|
+
const userSettings = loadSettings(homedir3());
|
|
4942
|
+
const user = await loadFrom(homedir3());
|
|
4238
4943
|
const projectSettings = loadSettings(cwd);
|
|
4239
4944
|
const project = await loadFrom(cwd);
|
|
4240
4945
|
const merged = { ...userSettings, ...user, ...projectSettings, ...project };
|
|
@@ -4245,8 +4950,8 @@ async function loadConfig(cwd) {
|
|
|
4245
4950
|
}
|
|
4246
4951
|
|
|
4247
4952
|
// cli/hooks-config.ts
|
|
4248
|
-
import { spawnSync } from "child_process";
|
|
4249
|
-
var
|
|
4953
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
4954
|
+
var log13 = forComponent("hooks");
|
|
4250
4955
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
4251
4956
|
function ruleMatches(rule, toolName) {
|
|
4252
4957
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -4255,7 +4960,7 @@ function ruleMatches(rule, toolName) {
|
|
|
4255
4960
|
}
|
|
4256
4961
|
function runCmd(rule, env) {
|
|
4257
4962
|
try {
|
|
4258
|
-
const r =
|
|
4963
|
+
const r = spawnSync2(rule.command, {
|
|
4259
4964
|
shell: true,
|
|
4260
4965
|
encoding: "utf8",
|
|
4261
4966
|
timeout: rule.timeoutMs ?? 1e4,
|
|
@@ -4263,7 +4968,7 @@ function runCmd(rule, env) {
|
|
|
4263
4968
|
});
|
|
4264
4969
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
4265
4970
|
} catch (e) {
|
|
4266
|
-
|
|
4971
|
+
log13.debug(`hook command failed: ${rule.command}`, e);
|
|
4267
4972
|
return { code: 1, out: String(e?.message ?? e) };
|
|
4268
4973
|
}
|
|
4269
4974
|
}
|
|
@@ -4367,17 +5072,17 @@ function formatDiff(ops, opts = {}) {
|
|
|
4367
5072
|
}
|
|
4368
5073
|
|
|
4369
5074
|
// cli/session.ts
|
|
4370
|
-
import { existsSync as
|
|
4371
|
-
import { join as
|
|
4372
|
-
var
|
|
5075
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync } from "fs";
|
|
5076
|
+
import { join as join6 } from "path";
|
|
5077
|
+
var log14 = forComponent("session");
|
|
4373
5078
|
var SessionStore = class {
|
|
4374
5079
|
dir;
|
|
4375
5080
|
constructor(cwd) {
|
|
4376
|
-
this.dir =
|
|
5081
|
+
this.dir = join6(cwd, ".agent", "sessions");
|
|
4377
5082
|
}
|
|
4378
5083
|
/** Sortable, human-readable id: `YYYYMMDD-HHMMSS-mmm`. */
|
|
4379
|
-
newId(
|
|
4380
|
-
const d = new Date(
|
|
5084
|
+
newId(now5 = Date.now()) {
|
|
5085
|
+
const d = new Date(now5);
|
|
4381
5086
|
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
4382
5087
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${p(d.getMilliseconds(), 3)}`;
|
|
4383
5088
|
}
|
|
@@ -4387,36 +5092,36 @@ var SessionStore = class {
|
|
|
4387
5092
|
}
|
|
4388
5093
|
save(data) {
|
|
4389
5094
|
if (!this.safeId(data.meta.id)) throw new Error(`unsafe session id: ${data.meta.id}`);
|
|
4390
|
-
if (!
|
|
4391
|
-
const path =
|
|
5095
|
+
if (!existsSync5(this.dir)) mkdirSync4(this.dir, { recursive: true });
|
|
5096
|
+
const path = join6(this.dir, `${data.meta.id}.json`);
|
|
4392
5097
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
4393
5098
|
writeFileSync3(tmp, JSON.stringify(data));
|
|
4394
5099
|
renameSync(tmp, path);
|
|
4395
5100
|
}
|
|
4396
5101
|
load(id) {
|
|
4397
5102
|
if (!this.safeId(id)) {
|
|
4398
|
-
|
|
5103
|
+
log14.debug(`rejecting unsafe session id: ${id}`);
|
|
4399
5104
|
return void 0;
|
|
4400
5105
|
}
|
|
4401
|
-
const path =
|
|
4402
|
-
if (!
|
|
5106
|
+
const path = join6(this.dir, `${id}.json`);
|
|
5107
|
+
if (!existsSync5(path)) return void 0;
|
|
4403
5108
|
try {
|
|
4404
5109
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4405
5110
|
} catch (e) {
|
|
4406
|
-
|
|
5111
|
+
log14.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
4407
5112
|
return void 0;
|
|
4408
5113
|
}
|
|
4409
5114
|
}
|
|
4410
5115
|
/** All sessions' metadata, most-recently-updated first. */
|
|
4411
5116
|
list() {
|
|
4412
|
-
if (!
|
|
5117
|
+
if (!existsSync5(this.dir)) return [];
|
|
4413
5118
|
const metas = [];
|
|
4414
5119
|
for (const f of readdirSync(this.dir)) {
|
|
4415
5120
|
if (!f.endsWith(".json")) continue;
|
|
4416
5121
|
try {
|
|
4417
|
-
metas.push(JSON.parse(readFileSync3(
|
|
5122
|
+
metas.push(JSON.parse(readFileSync3(join6(this.dir, f), "utf8")).meta);
|
|
4418
5123
|
} catch (e) {
|
|
4419
|
-
|
|
5124
|
+
log14.debug(`skipping unreadable session file ${f}`, e);
|
|
4420
5125
|
}
|
|
4421
5126
|
}
|
|
4422
5127
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -4508,9 +5213,9 @@ var CheckpointStack = class {
|
|
|
4508
5213
|
// cli/gitCheckpoints.ts
|
|
4509
5214
|
import { execFile } from "child_process";
|
|
4510
5215
|
import { promisify } from "util";
|
|
4511
|
-
import { writeFileSync as writeFileSync4, mkdirSync as
|
|
4512
|
-
import { join as
|
|
4513
|
-
var
|
|
5216
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
5217
|
+
import { join as join7, resolve as resolve2, sep as sep2 } from "path";
|
|
5218
|
+
var log15 = forComponent("checkpoints");
|
|
4514
5219
|
var exec = promisify(execFile);
|
|
4515
5220
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
4516
5221
|
var ShadowRepo = class {
|
|
@@ -4537,14 +5242,14 @@ var ShadowRepo = class {
|
|
|
4537
5242
|
if (this.ready !== void 0) return this.ready;
|
|
4538
5243
|
try {
|
|
4539
5244
|
await exec(this.git, ["--version"]);
|
|
4540
|
-
if (!
|
|
4541
|
-
|
|
5245
|
+
if (!existsSync6(this.gitDir)) {
|
|
5246
|
+
mkdirSync5(this.gitDir, { recursive: true });
|
|
4542
5247
|
await this.run("init", "-q");
|
|
4543
5248
|
}
|
|
4544
|
-
writeFileSync4(
|
|
5249
|
+
writeFileSync4(join7(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
4545
5250
|
this.ready = true;
|
|
4546
5251
|
} catch (e) {
|
|
4547
|
-
|
|
5252
|
+
log15.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
4548
5253
|
this.ready = false;
|
|
4549
5254
|
}
|
|
4550
5255
|
return this.ready;
|
|
@@ -4607,7 +5312,7 @@ var ShadowRepo = class {
|
|
|
4607
5312
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
4608
5313
|
});
|
|
4609
5314
|
} catch (e) {
|
|
4610
|
-
|
|
5315
|
+
log15.debug("checkpoint prune failed", e);
|
|
4611
5316
|
}
|
|
4612
5317
|
}
|
|
4613
5318
|
};
|
|
@@ -4637,7 +5342,7 @@ var GitCheckpoints = class {
|
|
|
4637
5342
|
const abs = resolve2(d);
|
|
4638
5343
|
if (abs === cwd || abs.startsWith(cwd + sep2)) continue;
|
|
4639
5344
|
if (cwd.startsWith(abs + sep2)) continue;
|
|
4640
|
-
out.push({ workTree: abs, gitDir:
|
|
5345
|
+
out.push({ workTree: abs, gitDir: join7(abs, ".agent", "checkpoints.git") });
|
|
4641
5346
|
}
|
|
4642
5347
|
return out;
|
|
4643
5348
|
}
|
|
@@ -4664,7 +5369,7 @@ var GitCheckpoints = class {
|
|
|
4664
5369
|
use(sessionId) {
|
|
4665
5370
|
if (sessionId === this.session) return;
|
|
4666
5371
|
this.session = sessionId;
|
|
4667
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
5372
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log15.debug("re-point failed", e));
|
|
4668
5373
|
}
|
|
4669
5374
|
async begin(label) {
|
|
4670
5375
|
if (!await this.start()) return;
|
|
@@ -4675,7 +5380,7 @@ var GitCheckpoints = class {
|
|
|
4675
5380
|
try {
|
|
4676
5381
|
await r.commit(msg);
|
|
4677
5382
|
} catch (e) {
|
|
4678
|
-
|
|
5383
|
+
log15.debug("checkpoint commit failed", e);
|
|
4679
5384
|
}
|
|
4680
5385
|
}
|
|
4681
5386
|
if (slow) clearTimeout(slow);
|
|
@@ -4718,7 +5423,7 @@ var GitCheckpointsOptions = class {
|
|
|
4718
5423
|
/** Real working tree to snapshot (the launch cwd). */
|
|
4719
5424
|
workTree = process.cwd();
|
|
4720
5425
|
/** Isolated git dir for the cwd shadow repo (kept out of the user's real .git). */
|
|
4721
|
-
gitDir =
|
|
5426
|
+
gitDir = join7(process.cwd(), ".agent", "checkpoints.git");
|
|
4722
5427
|
/** Extra mounted dirs (`--add-dir`); those outside cwd each get their own shadow repo. */
|
|
4723
5428
|
addDirs = [];
|
|
4724
5429
|
/** Conversation id → per-session restore-point ref. */
|
|
@@ -4732,9 +5437,9 @@ var GitCheckpointsOptions = class {
|
|
|
4732
5437
|
};
|
|
4733
5438
|
|
|
4734
5439
|
// cli/permissions.ts
|
|
4735
|
-
import { existsSync as
|
|
4736
|
-
import { homedir as
|
|
4737
|
-
import { join as
|
|
5440
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
5441
|
+
import { homedir as homedir4 } from "os";
|
|
5442
|
+
import { join as join8 } from "path";
|
|
4738
5443
|
var RULE_RE = /^(\w+)(?:\((.+)\))?$/;
|
|
4739
5444
|
function parseOne(raw, decision) {
|
|
4740
5445
|
const m = RULE_RE.exec(raw.trim());
|
|
@@ -4756,10 +5461,10 @@ function parsePermRules(perms) {
|
|
|
4756
5461
|
function describeRule(r) {
|
|
4757
5462
|
return `${r.decision.padEnd(5)} ${r.tool ?? "*"}${r.pathGlob ? `(${r.pathGlob})` : ""}`;
|
|
4758
5463
|
}
|
|
4759
|
-
var PERM_FILE = (cwd) =>
|
|
5464
|
+
var PERM_FILE = (cwd) => join8(cwd, ".agent", "permissions.json");
|
|
4760
5465
|
function loadPersistedRules(cwd) {
|
|
4761
5466
|
const p = PERM_FILE(cwd);
|
|
4762
|
-
if (!
|
|
5467
|
+
if (!existsSync7(p)) return {};
|
|
4763
5468
|
try {
|
|
4764
5469
|
const j = JSON.parse(readFileSync4(p, "utf8"));
|
|
4765
5470
|
return { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] };
|
|
@@ -4767,11 +5472,11 @@ function loadPersistedRules(cwd) {
|
|
|
4767
5472
|
return {};
|
|
4768
5473
|
}
|
|
4769
5474
|
}
|
|
4770
|
-
function loadClaudeSettings(cwd, home =
|
|
4771
|
-
const files = [
|
|
5475
|
+
function loadClaudeSettings(cwd, home = homedir4()) {
|
|
5476
|
+
const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
|
|
4772
5477
|
let out = {};
|
|
4773
5478
|
for (const p of files) {
|
|
4774
|
-
if (!
|
|
5479
|
+
if (!existsSync7(p)) continue;
|
|
4775
5480
|
try {
|
|
4776
5481
|
const perms = JSON.parse(readFileSync4(p, "utf8"))?.permissions;
|
|
4777
5482
|
if (perms) out = mergePerms(out, { allow: perms.allow, ask: perms.ask, deny: perms.deny }) ?? out;
|
|
@@ -4785,7 +5490,7 @@ function persistRule(cwd, decision, ruleStr) {
|
|
|
4785
5490
|
const list = cur[decision] ??= [];
|
|
4786
5491
|
if (!list.includes(ruleStr)) list.push(ruleStr);
|
|
4787
5492
|
try {
|
|
4788
|
-
|
|
5493
|
+
mkdirSync6(join8(cwd, ".agent"), { recursive: true });
|
|
4789
5494
|
writeFileSync5(PERM_FILE(cwd), JSON.stringify(cur, null, 2) + "\n");
|
|
4790
5495
|
} catch {
|
|
4791
5496
|
}
|
|
@@ -4799,10 +5504,10 @@ function mergePerms(a, b) {
|
|
|
4799
5504
|
}
|
|
4800
5505
|
return Object.keys(out).length ? out : void 0;
|
|
4801
5506
|
}
|
|
4802
|
-
var TRUST_FILE =
|
|
5507
|
+
var TRUST_FILE = join8(homedir4(), ".agent", "trusted.json");
|
|
4803
5508
|
function isTrusted(cwd, file = TRUST_FILE) {
|
|
4804
5509
|
try {
|
|
4805
|
-
return
|
|
5510
|
+
return existsSync7(file) && JSON.parse(readFileSync4(file, "utf8")).includes(cwd);
|
|
4806
5511
|
} catch {
|
|
4807
5512
|
return false;
|
|
4808
5513
|
}
|
|
@@ -4810,12 +5515,12 @@ function isTrusted(cwd, file = TRUST_FILE) {
|
|
|
4810
5515
|
function trustDir(cwd, file = TRUST_FILE) {
|
|
4811
5516
|
let list = [];
|
|
4812
5517
|
try {
|
|
4813
|
-
if (
|
|
5518
|
+
if (existsSync7(file)) list = JSON.parse(readFileSync4(file, "utf8"));
|
|
4814
5519
|
} catch {
|
|
4815
5520
|
}
|
|
4816
5521
|
if (!list.includes(cwd)) list.push(cwd);
|
|
4817
5522
|
try {
|
|
4818
|
-
|
|
5523
|
+
mkdirSync6(join8(file, ".."), { recursive: true });
|
|
4819
5524
|
writeFileSync5(file, JSON.stringify(list, null, 2) + "\n");
|
|
4820
5525
|
} catch {
|
|
4821
5526
|
}
|
|
@@ -5564,8 +6269,11 @@ function createLineEditor(out) {
|
|
|
5564
6269
|
// cyan
|
|
5565
6270
|
};
|
|
5566
6271
|
let curRow = 0;
|
|
6272
|
+
let suspended = false;
|
|
5567
6273
|
let activeRedraw;
|
|
6274
|
+
let activeAbort;
|
|
5568
6275
|
function render(s, promptArg, maxVisible, status) {
|
|
6276
|
+
if (suspended) return;
|
|
5569
6277
|
const cols = out.columns ?? 80;
|
|
5570
6278
|
const mode = !s.searching ? inputMode(s.buf) : void 0;
|
|
5571
6279
|
const vimTag = s.vim === "normal" && !s.searching && !mode ? inverse(" N ") + " " : "";
|
|
@@ -5630,9 +6338,18 @@ function createLineEditor(out) {
|
|
|
5630
6338
|
};
|
|
5631
6339
|
process.on("SIGWINCH", onResize);
|
|
5632
6340
|
return new Promise((resolve4) => {
|
|
6341
|
+
activeAbort = () => {
|
|
6342
|
+
finish();
|
|
6343
|
+
resolve4(null);
|
|
6344
|
+
};
|
|
5633
6345
|
const redraw = () => render(s, opts.prompt, maxVisible, opts.status);
|
|
6346
|
+
let lastStatus = opts.status?.() ?? "";
|
|
5634
6347
|
const ticker = opts.statusTickMs && opts.status ? setInterval(() => {
|
|
5635
|
-
if (
|
|
6348
|
+
if (s.pasting) return;
|
|
6349
|
+
const cur = opts.status();
|
|
6350
|
+
if (cur === lastStatus) return;
|
|
6351
|
+
lastStatus = cur;
|
|
6352
|
+
redraw();
|
|
5636
6353
|
}, opts.statusTickMs) : void 0;
|
|
5637
6354
|
const onKey = (str, key) => {
|
|
5638
6355
|
if (key?.ctrl && key.name === "l") {
|
|
@@ -5656,6 +6373,21 @@ function createLineEditor(out) {
|
|
|
5656
6373
|
redraw();
|
|
5657
6374
|
return;
|
|
5658
6375
|
}
|
|
6376
|
+
if (key?.ctrl && key.name === "s" || key?.meta && key.name === "s") {
|
|
6377
|
+
if (s.buf.trim() && opts.onStash) {
|
|
6378
|
+
opts.onStash(s.expand());
|
|
6379
|
+
s.reset();
|
|
6380
|
+
s.refresh();
|
|
6381
|
+
redraw();
|
|
6382
|
+
} else if (!s.buf.length && opts.onUnstash) {
|
|
6383
|
+
const text = opts.onUnstash();
|
|
6384
|
+
if (text) {
|
|
6385
|
+
s.insert(text);
|
|
6386
|
+
redraw();
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
5659
6391
|
if (key?.meta && key.name === "p" && opts.onPickModel) {
|
|
5660
6392
|
process.stdin.off("keypress", onKey);
|
|
5661
6393
|
void opts.onPickModel().finally(() => {
|
|
@@ -5693,6 +6425,7 @@ function createLineEditor(out) {
|
|
|
5693
6425
|
};
|
|
5694
6426
|
const finish = () => {
|
|
5695
6427
|
activeRedraw = void 0;
|
|
6428
|
+
activeAbort = void 0;
|
|
5696
6429
|
if (ticker) clearInterval(ticker);
|
|
5697
6430
|
process.stdin.off("keypress", onKey);
|
|
5698
6431
|
process.removeListener("SIGWINCH", onResize);
|
|
@@ -5705,7 +6438,24 @@ function createLineEditor(out) {
|
|
|
5705
6438
|
process.stdin.on("keypress", onKey);
|
|
5706
6439
|
});
|
|
5707
6440
|
}
|
|
5708
|
-
return {
|
|
6441
|
+
return {
|
|
6442
|
+
readLine,
|
|
6443
|
+
redrawNow: () => activeRedraw?.(),
|
|
6444
|
+
suspend: () => {
|
|
6445
|
+
if (suspended) return;
|
|
6446
|
+
suspended = true;
|
|
6447
|
+
if (curRow > 0) out.write(`\x1B[${curRow}A`);
|
|
6448
|
+
out.write("\r\x1B[J");
|
|
6449
|
+
curRow = 0;
|
|
6450
|
+
},
|
|
6451
|
+
resume: () => {
|
|
6452
|
+
if (!suspended) return;
|
|
6453
|
+
suspended = false;
|
|
6454
|
+
curRow = 0;
|
|
6455
|
+
activeRedraw?.();
|
|
6456
|
+
},
|
|
6457
|
+
abort: () => activeAbort?.()
|
|
6458
|
+
};
|
|
5709
6459
|
}
|
|
5710
6460
|
function selectMenu(out, opts) {
|
|
5711
6461
|
if (!out.isTTY || !process.stdin.isTTY || !opts.items.length) return Promise.resolve(null);
|
|
@@ -6006,7 +6756,7 @@ var red = C("31");
|
|
|
6006
6756
|
var bold = C("1");
|
|
6007
6757
|
var yellow = C("33");
|
|
6008
6758
|
var err = (s) => process.stderr.write(s);
|
|
6009
|
-
var
|
|
6759
|
+
var log16 = forComponent("cli");
|
|
6010
6760
|
var VERSION = (() => {
|
|
6011
6761
|
try {
|
|
6012
6762
|
return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -6033,6 +6783,9 @@ var spinner = /* @__PURE__ */ (() => {
|
|
|
6033
6783
|
};
|
|
6034
6784
|
})();
|
|
6035
6785
|
var activeTurn = null;
|
|
6786
|
+
var exitRequested = false;
|
|
6787
|
+
var inputStash = [];
|
|
6788
|
+
var stashBuf = "";
|
|
6036
6789
|
function numFlag(raw, flag) {
|
|
6037
6790
|
const n = Number(raw);
|
|
6038
6791
|
if (!Number.isFinite(n) || n < 0) throw new Error(`invalid ${flag}: ${raw ?? "(missing value)"}`);
|
|
@@ -6137,6 +6890,8 @@ Flags:
|
|
|
6137
6890
|
to a background worker agent (-m model); results are re-voiced when ready
|
|
6138
6891
|
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
6139
6892
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
6893
|
+
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
6894
|
+
spoken replies out (echo-cancelled; speak over it to interrupt)
|
|
6140
6895
|
--voice-model <id> with --duplex: the fast voice model (default anthropic/claude-haiku-4-5)
|
|
6141
6896
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
6142
6897
|
--subagents allow the Task tool (spawn child agents)
|
|
@@ -6170,7 +6925,8 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
|
|
|
6170
6925
|
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
|
|
6171
6926
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
|
|
6172
6927
|
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.
|
|
6173
|
-
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.
|
|
6928
|
+
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 \xB7 Alt+S/Ctrl+S stash/unstash.
|
|
6929
|
+
REPL stash: type while a turn is running \u2192 Enter queues it (auto-submits when the turn finishes). Alt+S (or Ctrl+S) with text stashes it; on an empty prompt pops the next entry for editing.
|
|
6174
6930
|
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.
|
|
6175
6931
|
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).`;
|
|
6176
6932
|
function newestModel() {
|
|
@@ -6188,11 +6944,11 @@ function resolveModelOrNewest(model) {
|
|
|
6188
6944
|
}
|
|
6189
6945
|
var ENV_KEY_ALIASES = { google: ["GEMINI_API_KEY"] };
|
|
6190
6946
|
function loadInstallEnv() {
|
|
6191
|
-
let dir =
|
|
6192
|
-
for (let i = 0; i < 5 && !
|
|
6947
|
+
let dir = dirname4(import.meta.path);
|
|
6948
|
+
for (let i = 0; i < 5 && !existsSync8(join9(dir, "package.json")); i++) dir = dirname4(dir);
|
|
6193
6949
|
for (const name of [".env", ".env.local"]) {
|
|
6194
|
-
const file =
|
|
6195
|
-
if (!
|
|
6950
|
+
const file = join9(dir, name);
|
|
6951
|
+
if (!existsSync8(file)) continue;
|
|
6196
6952
|
for (const line of readFileSync5(file, "utf8").split("\n")) {
|
|
6197
6953
|
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
6198
6954
|
if (!m || m[1] in process.env) continue;
|
|
@@ -6572,7 +7328,7 @@ async function mountMcp(cfg, oauth) {
|
|
|
6572
7328
|
return mounted;
|
|
6573
7329
|
}
|
|
6574
7330
|
async function closeMcp(mounted) {
|
|
6575
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
7331
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log16.debug("mcp close failed", e))));
|
|
6576
7332
|
}
|
|
6577
7333
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
6578
7334
|
function readImageParts(cwd, line) {
|
|
@@ -6595,9 +7351,9 @@ function pastePathClassifier(cwd) {
|
|
|
6595
7351
|
t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
|
|
6596
7352
|
if (/\s/.test(t)) return null;
|
|
6597
7353
|
if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
|
|
6598
|
-
const abs = t.startsWith("~/") ?
|
|
7354
|
+
const abs = t.startsWith("~/") ? join9(homedir5(), t.slice(2)) : resolve3(cwd, t);
|
|
6599
7355
|
try {
|
|
6600
|
-
if (!
|
|
7356
|
+
if (!statSync3(abs).isFile()) return null;
|
|
6601
7357
|
} catch {
|
|
6602
7358
|
return null;
|
|
6603
7359
|
}
|
|
@@ -6700,7 +7456,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
6700
7456
|
const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length;
|
|
6701
7457
|
const ok = res.finishReason === "stop";
|
|
6702
7458
|
const shortId = session.meta.id.slice(-10);
|
|
6703
|
-
err("\n" + (ok ? green(" \u2713 done") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
|
|
7459
|
+
err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
|
|
6704
7460
|
`));
|
|
6705
7461
|
if (res.finishReason === "error" && res.error) {
|
|
6706
7462
|
const e = res.error;
|
|
@@ -6731,8 +7487,8 @@ function startSession(args, store, agent, cwd) {
|
|
|
6731
7487
|
if (data) {
|
|
6732
7488
|
agent.transcript = data.messages;
|
|
6733
7489
|
if (args.fork) {
|
|
6734
|
-
const
|
|
6735
|
-
const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(
|
|
7490
|
+
const now6 = Date.now();
|
|
7491
|
+
const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(now6), created: now6, updated: now6, turns: data.meta.turns }, messages: data.messages };
|
|
6736
7492
|
err(dim(` forked ${data.meta.id} \u2192 ${forked.meta.id} (${data.meta.turns} turns)
|
|
6737
7493
|
`));
|
|
6738
7494
|
if (!args.task) printHistory(data.messages);
|
|
@@ -6746,11 +7502,11 @@ function startSession(args, store, agent, cwd) {
|
|
|
6746
7502
|
err(yellow(` no session to resume \u2014 starting fresh
|
|
6747
7503
|
`));
|
|
6748
7504
|
}
|
|
6749
|
-
const
|
|
6750
|
-
const id = args.sessionId ?? store.newId(
|
|
7505
|
+
const now5 = Date.now();
|
|
7506
|
+
const id = args.sessionId ?? store.newId(now5);
|
|
6751
7507
|
if (!args.task) err(dim(` session ${id}
|
|
6752
7508
|
`));
|
|
6753
|
-
return { meta: { id, created:
|
|
7509
|
+
return { meta: { id, created: now5, updated: now5, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
|
|
6754
7510
|
}
|
|
6755
7511
|
var AGENTS_MD_TEMPLATE = `# ${"${name}"}
|
|
6756
7512
|
|
|
@@ -6770,24 +7526,24 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
|
|
|
6770
7526
|
`;
|
|
6771
7527
|
function initInstructions(cwd) {
|
|
6772
7528
|
for (const f of ["AGENTS.md", "CLAUDE.md"]) {
|
|
6773
|
-
if (
|
|
7529
|
+
if (existsSync8(join9(cwd, f))) {
|
|
6774
7530
|
err(yellow(` ${f} already exists \u2014 leaving it as-is
|
|
6775
7531
|
`));
|
|
6776
7532
|
return;
|
|
6777
7533
|
}
|
|
6778
7534
|
}
|
|
6779
|
-
const path =
|
|
7535
|
+
const path = join9(cwd, "AGENTS.md");
|
|
6780
7536
|
writeFileSync6(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
|
|
6781
7537
|
err(green(` created ${path}
|
|
6782
7538
|
`) + dim(" edit it, then it auto-loads into every run.\n"));
|
|
6783
7539
|
}
|
|
6784
7540
|
function persistSetting(cwd, key, value) {
|
|
6785
|
-
const path =
|
|
7541
|
+
const path = join9(cwd, ".agent", "settings.json");
|
|
6786
7542
|
try {
|
|
6787
|
-
const obj =
|
|
7543
|
+
const obj = existsSync8(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
|
|
6788
7544
|
if (obj[key] === value) return;
|
|
6789
7545
|
obj[key] = value;
|
|
6790
|
-
|
|
7546
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
6791
7547
|
writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
|
|
6792
7548
|
} catch (e) {
|
|
6793
7549
|
err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
|
|
@@ -6804,14 +7560,14 @@ var isCancelTeardown = (e) => {
|
|
|
6804
7560
|
function installCancelGuards(mounted) {
|
|
6805
7561
|
process.on("unhandledRejection", (e) => {
|
|
6806
7562
|
if (isCancelTeardown(e)) {
|
|
6807
|
-
|
|
7563
|
+
log16.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
6808
7564
|
return;
|
|
6809
7565
|
}
|
|
6810
|
-
|
|
7566
|
+
log16.error("unhandledRejection", e);
|
|
6811
7567
|
});
|
|
6812
7568
|
process.on("uncaughtException", (e) => {
|
|
6813
7569
|
if (isCancelTeardown(e)) {
|
|
6814
|
-
|
|
7570
|
+
log16.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
6815
7571
|
return;
|
|
6816
7572
|
}
|
|
6817
7573
|
console.error(e);
|
|
@@ -6820,11 +7576,15 @@ function installCancelGuards(mounted) {
|
|
|
6820
7576
|
});
|
|
6821
7577
|
}
|
|
6822
7578
|
async function repl(args, ai, cfg, cwd) {
|
|
6823
|
-
const oauth = new McpOAuth({ storePath:
|
|
7579
|
+
const oauth = new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") });
|
|
6824
7580
|
const mounted = await mountMcp(cfg, oauth);
|
|
6825
7581
|
const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
|
|
7582
|
+
if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
|
|
7583
|
+
exitRequested = true;
|
|
7584
|
+
})];
|
|
6826
7585
|
const duplex = args.duplex;
|
|
6827
7586
|
let dx;
|
|
7587
|
+
let voiceIO;
|
|
6828
7588
|
let editorRef;
|
|
6829
7589
|
let workerOptions;
|
|
6830
7590
|
let duplexPersist = () => {
|
|
@@ -6846,17 +7606,23 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6846
7606
|
const host = {
|
|
6847
7607
|
...base,
|
|
6848
7608
|
notify(e) {
|
|
7609
|
+
if (e.kind === "text_delta" && voiceIO) {
|
|
7610
|
+
voiceIO.speakDelta(e.message);
|
|
7611
|
+
editorRef?.suspend();
|
|
7612
|
+
}
|
|
6849
7613
|
if (e.kind === "revoice_done") {
|
|
6850
7614
|
base.flushText();
|
|
6851
7615
|
process.stdout.write("\n");
|
|
7616
|
+
voiceIO?.endSpeech();
|
|
6852
7617
|
duplexPersist();
|
|
7618
|
+
editorRef?.resume();
|
|
6853
7619
|
editorRef?.redrawNow();
|
|
6854
7620
|
return;
|
|
6855
7621
|
}
|
|
6856
7622
|
if (e.kind === "task_done" && e.data?.text) {
|
|
6857
7623
|
const lines = String(e.data.text).split("\n");
|
|
6858
7624
|
const shown = lines.slice(0, previewLines());
|
|
6859
|
-
err("\r\x1B[
|
|
7625
|
+
err("\r\x1B[0J\n" + dim(` \u29BF ${e.message}
|
|
6860
7626
|
`) + shown.map((l) => dim(` ${l}
|
|
6861
7627
|
`)).join(""));
|
|
6862
7628
|
if (lines.length > shown.length) err(dim(` \u2026 (+${lines.length - shown.length} more lines)
|
|
@@ -6884,7 +7650,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6884
7650
|
dx = new DuplexAgent({
|
|
6885
7651
|
ai,
|
|
6886
7652
|
fs: agent.options.fs,
|
|
6887
|
-
...args.voiceModel ? { voiceModel: resolveModelOrNewest(args.voiceModel) } : {},
|
|
7653
|
+
...args.voiceModel ?? cfg.voiceModel ? { voiceModel: resolveModelOrNewest(args.voiceModel ?? cfg.voiceModel) } : {},
|
|
6888
7654
|
workerModel: agent.options.model,
|
|
6889
7655
|
workerOptions,
|
|
6890
7656
|
host,
|
|
@@ -6894,9 +7660,34 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6894
7660
|
onTaskStart: async (_id, label) => {
|
|
6895
7661
|
await checkpoints.begin(label);
|
|
6896
7662
|
},
|
|
7663
|
+
// The jail deny-lists .git/** (VCS internals can carry credentials), so the engine's fs-based
|
|
7664
|
+
// 'branch' lookup can't see it — supply it host-side (one safe read-only file).
|
|
7665
|
+
quickLook: {
|
|
7666
|
+
branch: () => {
|
|
7667
|
+
try {
|
|
7668
|
+
const head = readFileSync5(join9(cwd, ".git", "HEAD"), "utf8").trim();
|
|
7669
|
+
return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
|
|
7670
|
+
} catch {
|
|
7671
|
+
return "not a git repository";
|
|
7672
|
+
}
|
|
7673
|
+
},
|
|
7674
|
+
// Memory READS are QuickLook material (instant, capped); memory WRITES stay delegated —
|
|
7675
|
+
// a worker creates/updates the files under .agent/memory/.
|
|
7676
|
+
memory: async () => {
|
|
7677
|
+
const dir = agent.options.memoryDir || adot("memory");
|
|
7678
|
+
try {
|
|
7679
|
+
const idx = await fs.readFile(`${dir}/MEMORY.md`);
|
|
7680
|
+
return idx.slice(0, 2e3) || "(memory index is empty)";
|
|
7681
|
+
} catch {
|
|
7682
|
+
return "no memory yet \u2014 to save something, Delegate it (a worker writes .agent/memory/)";
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
},
|
|
6897
7686
|
// The voice runs on the REAL fs (it has no fs tools — harmless) so @mentions, !cmd and #note
|
|
6898
7687
|
// resolve against the project; + CC-parity chrome for its own tool calls (⚙ Delegate …).
|
|
6899
|
-
voiceOptions: { fs: agent.options.fs, hooks: displayHooks(agent.options.fs), tools: [rewindFilesTool
|
|
7688
|
+
voiceOptions: { fs: agent.options.fs, hooks: displayHooks(agent.options.fs), tools: [rewindFilesTool, exitSessionTool(() => {
|
|
7689
|
+
exitRequested = true;
|
|
7690
|
+
})] }
|
|
6900
7691
|
});
|
|
6901
7692
|
}
|
|
6902
7693
|
const face = dx ? dx.voice : agent;
|
|
@@ -6952,9 +7743,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6952
7743
|
};
|
|
6953
7744
|
const pendingImages = [];
|
|
6954
7745
|
const grabClipboardAttachment = () => {
|
|
6955
|
-
const dir =
|
|
7746
|
+
const dir = join9(tmpdir(), "agentx-pasted");
|
|
6956
7747
|
try {
|
|
6957
|
-
|
|
7748
|
+
mkdirSync7(dir, { recursive: true });
|
|
6958
7749
|
} catch {
|
|
6959
7750
|
}
|
|
6960
7751
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
@@ -6963,15 +7754,17 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6963
7754
|
process.on("SIGINT", () => {
|
|
6964
7755
|
if (activeTurn) {
|
|
6965
7756
|
activeTurn.abort();
|
|
7757
|
+
voiceIO?.interrupt();
|
|
6966
7758
|
return;
|
|
6967
7759
|
}
|
|
7760
|
+
voiceIO?.stop();
|
|
6968
7761
|
void closeMcp(mounted);
|
|
6969
7762
|
process.exit(130);
|
|
6970
7763
|
});
|
|
6971
7764
|
installCancelGuards(mounted);
|
|
6972
7765
|
const store = new SessionStore(cwd);
|
|
6973
7766
|
let session = startSession(args, store, face, cwd);
|
|
6974
|
-
const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir:
|
|
7767
|
+
const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join9(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
|
|
6975
7768
|
const cpHooks = checkpoints.hooks?.();
|
|
6976
7769
|
if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
|
|
6977
7770
|
duplexPersist = () => {
|
|
@@ -6998,17 +7791,17 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
6998
7791
|
const fs = agent.options.fs;
|
|
6999
7792
|
const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
|
|
7000
7793
|
const adot = (sub) => `${fsBase}/.agent/${sub}`;
|
|
7001
|
-
const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${
|
|
7794
|
+
const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir5()}/.agent/${sub}`, `${homedir5()}/.claude/${sub}`];
|
|
7002
7795
|
const cmds = (await loadCommands(fs, adots("commands"))).commands;
|
|
7003
7796
|
const skills = (await loadSkills(fs, adots("skills"))).skills;
|
|
7004
|
-
const histPath =
|
|
7005
|
-
const history =
|
|
7797
|
+
const histPath = join9(cwd, ".agent", "history");
|
|
7798
|
+
const history = existsSync8(histPath) ? readFileSync5(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
|
|
7006
7799
|
const remember = (line) => {
|
|
7007
7800
|
try {
|
|
7008
|
-
|
|
7801
|
+
mkdirSync7(join9(cwd, ".agent"), { recursive: true });
|
|
7009
7802
|
appendFileSync(histPath, line + "\n");
|
|
7010
7803
|
} catch (e) {
|
|
7011
|
-
|
|
7804
|
+
log16.debug("history write failed", e);
|
|
7012
7805
|
}
|
|
7013
7806
|
};
|
|
7014
7807
|
const ago = (t) => {
|
|
@@ -7066,7 +7859,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7066
7859
|
try {
|
|
7067
7860
|
store.save(session);
|
|
7068
7861
|
} catch (e) {
|
|
7069
|
-
|
|
7862
|
+
log16.debug("session save after rewind failed", e);
|
|
7070
7863
|
}
|
|
7071
7864
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
7072
7865
|
`));
|
|
@@ -7090,10 +7883,15 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7090
7883
|
const announcedTasks = /* @__PURE__ */ new Set();
|
|
7091
7884
|
const turn = async (task) => {
|
|
7092
7885
|
const r = await runTurn(face, store, session, task, duplex ? void 0 : checkpoints, cwd, sendVia);
|
|
7886
|
+
if (voiceIO) {
|
|
7887
|
+
process.stdout.write("\n");
|
|
7888
|
+
editorRef?.resume();
|
|
7889
|
+
}
|
|
7890
|
+
voiceIO?.endSpeech();
|
|
7093
7891
|
if (dx) {
|
|
7094
7892
|
const fresh = [...dx.tasks.values()].filter((t) => t.status === "running" && !announcedTasks.has(t.id));
|
|
7095
7893
|
fresh.forEach((t) => announcedTasks.add(t.id));
|
|
7096
|
-
if (fresh.length) err(cyan(` \u25D4 ${fresh.length === 1 ? `task ${fresh[0].id} (${fresh[0].label})` : `${fresh.length} tasks`} working in the background`) + dim(" \u2014 the result will appear here; keep chatting meanwhile\n"));
|
|
7894
|
+
if (fresh.length) err("\r\x1B[0J" + cyan(` \u25D4 ${fresh.length === 1 ? `task ${fresh[0].id} (${fresh[0].label})` : `${fresh.length} tasks`} working in the background`) + dim(" \u2014 the result will appear here; keep chatting meanwhile\n"));
|
|
7097
7895
|
}
|
|
7098
7896
|
return r;
|
|
7099
7897
|
};
|
|
@@ -7261,6 +8059,26 @@ ${extra}` : body);
|
|
|
7261
8059
|
else err(dim(" " + (duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model) + "\n"));
|
|
7262
8060
|
}
|
|
7263
8061
|
},
|
|
8062
|
+
...duplex ? { "voice-model": {
|
|
8063
|
+
desc: "switch the duplex voice (fast) model \u2014 /voice-model <id>, or alone for a picker",
|
|
8064
|
+
run: async (a) => {
|
|
8065
|
+
const apply = (id) => {
|
|
8066
|
+
const m = resolveModelOrNewest(id);
|
|
8067
|
+
dx.options.voiceModel = m;
|
|
8068
|
+
dx.voice.options.model = m;
|
|
8069
|
+
err(green(` \u2713 voice model \u2192 ${m}
|
|
8070
|
+
`));
|
|
8071
|
+
};
|
|
8072
|
+
if (a[0]) {
|
|
8073
|
+
apply(a[0]);
|
|
8074
|
+
return;
|
|
8075
|
+
}
|
|
8076
|
+
const picked = await pickModel(dx.options.voiceModel);
|
|
8077
|
+
if (picked) apply(picked);
|
|
8078
|
+
else err(dim(` voice ${dx.options.voiceModel}
|
|
8079
|
+
`));
|
|
8080
|
+
}
|
|
8081
|
+
} } : {},
|
|
7264
8082
|
reasoning: {
|
|
7265
8083
|
desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker (duplex: the workers')",
|
|
7266
8084
|
run: async (a) => {
|
|
@@ -7489,7 +8307,7 @@ ${extra}` : body);
|
|
|
7489
8307
|
}
|
|
7490
8308
|
const m = mounted.splice(idx, 1)[0];
|
|
7491
8309
|
removeWorkTools(m.tools.map((t) => t.name));
|
|
7492
|
-
await m.client.close().catch((e) =>
|
|
8310
|
+
await m.client.close().catch((e) => log16.debug("mcp close failed", e));
|
|
7493
8311
|
err(dim(` removed "${name}"
|
|
7494
8312
|
`));
|
|
7495
8313
|
return;
|
|
@@ -7604,10 +8422,10 @@ ${extra}` : body);
|
|
|
7604
8422
|
return;
|
|
7605
8423
|
}
|
|
7606
8424
|
const md = exportMarkdown(session.meta, shown);
|
|
7607
|
-
const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" :
|
|
8425
|
+
const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join9(".agent", "exports", `${session.meta.id}.md`);
|
|
7608
8426
|
const path = resolve3(cwd, name);
|
|
7609
8427
|
try {
|
|
7610
|
-
|
|
8428
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
7611
8429
|
writeFileSync6(path, md);
|
|
7612
8430
|
err(green(` \u2713 exported \u2192 ${path}
|
|
7613
8431
|
`) + dim(` ${shown.length} message(s) \xB7 ${md.length} chars
|
|
@@ -7628,9 +8446,9 @@ ${extra}` : body);
|
|
|
7628
8446
|
`));
|
|
7629
8447
|
const listDir = (absDir) => {
|
|
7630
8448
|
try {
|
|
7631
|
-
return readdirSync2(
|
|
8449
|
+
return readdirSync2(join9(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
7632
8450
|
} catch (e) {
|
|
7633
|
-
|
|
8451
|
+
log16.debug("completion readdir failed", absDir, e);
|
|
7634
8452
|
return null;
|
|
7635
8453
|
}
|
|
7636
8454
|
};
|
|
@@ -7645,21 +8463,57 @@ ${extra}` : body);
|
|
|
7645
8463
|
let aborting = false;
|
|
7646
8464
|
let pendingRewind = false;
|
|
7647
8465
|
if (process.stdin.isTTY) {
|
|
8466
|
+
const renderStashBuf = () => {
|
|
8467
|
+
if (!stashBuf) return;
|
|
8468
|
+
const q2 = inputStash.length ? dim(` [${inputStash.length} queued]`) : "";
|
|
8469
|
+
err(`\r\x1B[K${dim(" stash \u203A ")}${stashBuf}${q2}`);
|
|
8470
|
+
};
|
|
7648
8471
|
process.stdin.on("keypress", (_s, key) => {
|
|
7649
8472
|
if (!activeTurn) return;
|
|
7650
8473
|
if (key?.ctrl && key?.name === "o") {
|
|
7651
8474
|
toggleVerbose();
|
|
7652
8475
|
return;
|
|
7653
8476
|
}
|
|
7654
|
-
const
|
|
7655
|
-
|
|
7656
|
-
if (
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
8477
|
+
const k = key?.name;
|
|
8478
|
+
const cancel = k === "escape" || key?.ctrl && k === "c";
|
|
8479
|
+
if (cancel) {
|
|
8480
|
+
if (stashBuf) {
|
|
8481
|
+
stashBuf = "";
|
|
8482
|
+
err("\r\x1B[K");
|
|
8483
|
+
return;
|
|
8484
|
+
}
|
|
8485
|
+
if (!aborting) {
|
|
8486
|
+
aborting = true;
|
|
8487
|
+
activeTurn.abort();
|
|
8488
|
+
voiceIO?.interrupt();
|
|
8489
|
+
err(yellow("\n \u238B cancelling\u2026\n"));
|
|
8490
|
+
} else if (k === "escape" && !pendingRewind) {
|
|
8491
|
+
pendingRewind = true;
|
|
8492
|
+
err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
|
|
8493
|
+
}
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
if (k === "return" || k === "enter") {
|
|
8497
|
+
if (stashBuf.trim()) {
|
|
8498
|
+
inputStash.push(stashBuf.trim());
|
|
8499
|
+
err(`\r\x1B[K${green(" \u2713 stashed")} ${dim(`#${inputStash.length}: ${stashBuf.trim().slice(0, 50)}${stashBuf.trim().length > 50 ? "\u2026" : ""}`)}
|
|
8500
|
+
`);
|
|
8501
|
+
}
|
|
8502
|
+
stashBuf = "";
|
|
8503
|
+
return;
|
|
8504
|
+
}
|
|
8505
|
+
if (k === "backspace") {
|
|
8506
|
+
if (stashBuf.length) {
|
|
8507
|
+
stashBuf = stashBuf.slice(0, -1);
|
|
8508
|
+
if (stashBuf) renderStashBuf();
|
|
8509
|
+
else err("\r\x1B[K");
|
|
8510
|
+
}
|
|
8511
|
+
return;
|
|
8512
|
+
}
|
|
8513
|
+
if (!key?.ctrl && !key?.meta && isPrintable(_s)) {
|
|
8514
|
+
stashBuf += _s;
|
|
8515
|
+
renderStashBuf();
|
|
8516
|
+
return;
|
|
7663
8517
|
}
|
|
7664
8518
|
});
|
|
7665
8519
|
}
|
|
@@ -7677,6 +8531,120 @@ ${extra}` : body);
|
|
|
7677
8531
|
};
|
|
7678
8532
|
let prefill;
|
|
7679
8533
|
let tick = 0;
|
|
8534
|
+
const dispatchLine = async (line) => {
|
|
8535
|
+
history.unshift(line.replace(/\n+/g, " \u23CE "));
|
|
8536
|
+
remember(line.replace(/\n+/g, " \u23CE "));
|
|
8537
|
+
if (line.startsWith("!")) {
|
|
8538
|
+
const cmd = line.slice(1).trim();
|
|
8539
|
+
if (cmd) {
|
|
8540
|
+
err(dim(await runShellLine(agent.options.fs, cmd) + "\n"));
|
|
8541
|
+
}
|
|
8542
|
+
return;
|
|
8543
|
+
}
|
|
8544
|
+
if (line.startsWith("#")) {
|
|
8545
|
+
const note = line.slice(1).trim();
|
|
8546
|
+
if (note) {
|
|
8547
|
+
const where = await appendMemoryNote(agent.options.fs, agent.options.memoryDir || adot("memory"), note);
|
|
8548
|
+
err(green(` \u270E remembered \u2192 ${where}
|
|
8549
|
+
`));
|
|
8550
|
+
}
|
|
8551
|
+
return;
|
|
8552
|
+
}
|
|
8553
|
+
if (line.startsWith("/")) {
|
|
8554
|
+
const [name, ...a] = line.slice(1).split(/\s+/);
|
|
8555
|
+
if (!name) {
|
|
8556
|
+
err(red(" / needs a command name\n") + dim(" (try /help)\n"));
|
|
8557
|
+
return;
|
|
8558
|
+
}
|
|
8559
|
+
const b = builtins[name];
|
|
8560
|
+
if (b) {
|
|
8561
|
+
if (await b.run(a)) return "quit";
|
|
8562
|
+
return;
|
|
8563
|
+
}
|
|
8564
|
+
const c = cmds.find((x) => x.name === name);
|
|
8565
|
+
if (c) {
|
|
8566
|
+
await runCommand(c, a.join(" "));
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8569
|
+
const sk = skills.find((x) => x.name === name);
|
|
8570
|
+
if (sk) {
|
|
8571
|
+
await runSkill(sk, a.join(" "));
|
|
8572
|
+
return;
|
|
8573
|
+
}
|
|
8574
|
+
const known = Object.keys(builtins).filter((k) => k !== "quit").map((k) => "/" + k);
|
|
8575
|
+
const custom = [...cmds.map((x) => x.name), ...skills.map((x) => x.name)].map((n) => "/" + n);
|
|
8576
|
+
err(red(` unknown command /${name}
|
|
8577
|
+
`) + dim(" builtins: " + known.join(" ") + "\n") + (custom.length ? dim(" custom: " + custom.join(" ") + "\n") : "") + dim(" (or /help)\n"));
|
|
8578
|
+
return;
|
|
8579
|
+
}
|
|
8580
|
+
const task = pendingImages.length ? `${line} ${pendingImages.map((p) => "@" + p).join(" ")}` : line;
|
|
8581
|
+
pendingImages.length = 0;
|
|
8582
|
+
await turn(task);
|
|
8583
|
+
if (exitRequested) return "quit";
|
|
8584
|
+
};
|
|
8585
|
+
let voicePartial = "";
|
|
8586
|
+
let partialRedraw = null;
|
|
8587
|
+
if (args.voice && duplex && process.stdin.isTTY) {
|
|
8588
|
+
if (!VoiceIO.available()) {
|
|
8589
|
+
err(dim(" (voice I/O off \u2014 set SONIOX_API_KEY, CARTESIA_API_KEY, CARTESIA_VOICE_ID to talk)\n"));
|
|
8590
|
+
} else {
|
|
8591
|
+
voiceIO = new VoiceIO({
|
|
8592
|
+
// No ack phrase by default: a fixed "Mm-hm," every turn reads robotic, Haiku's TTFT doesn't
|
|
8593
|
+
// need masking (~0.7-1.2s full turns), and the conversational register already opens with a
|
|
8594
|
+
// natural reaction. The mechanism (+ echo-leak guard) stays for slower voice models.
|
|
8595
|
+
onState: () => editorRef?.redrawNow(),
|
|
8596
|
+
// Throttled: each redraw clears the screen below the prompt — a partial-per-token storm
|
|
8597
|
+
// (fast speech, or echo bleed if AEC degrades) would continuously erase streamed text.
|
|
8598
|
+
onPartial: (text) => {
|
|
8599
|
+
if (text === voicePartial) return;
|
|
8600
|
+
voicePartial = text;
|
|
8601
|
+
if (!partialRedraw) partialRedraw = setTimeout(() => {
|
|
8602
|
+
partialRedraw = null;
|
|
8603
|
+
editorRef?.redrawNow();
|
|
8604
|
+
}, 250);
|
|
8605
|
+
},
|
|
8606
|
+
onBargeIn: (phase) => {
|
|
8607
|
+
activeTurn?.abort();
|
|
8608
|
+
if (phase === "speaking") err(yellow("\n \u270B interrupted\n"));
|
|
8609
|
+
},
|
|
8610
|
+
onUtterance: (text) => {
|
|
8611
|
+
voicePartial = "";
|
|
8612
|
+
if (!text.trim()) return;
|
|
8613
|
+
const cut = voiceIO.takeInterruptedReply();
|
|
8614
|
+
const note = cut && cut.full.length - cut.heard.length > 40 ? `
|
|
8615
|
+
[the user interrupted you mid-speech \u2014 they only heard up to: "\u2026${cut.heard.slice(-80)}". Work any unheard essentials into your reply naturally, only if still relevant.]` : "";
|
|
8616
|
+
if (!/^[!#/]/.test(text.trim())) voiceIO.beginSpeech(true);
|
|
8617
|
+
err(`\r\x1B[K ${bold(cyan("\u{1F3A4} \u203A"))} ${text}
|
|
8618
|
+
`);
|
|
8619
|
+
void dispatchLine(text + note).then(async (r) => {
|
|
8620
|
+
if (r === "quit") {
|
|
8621
|
+
await voiceIO?.awaitIdle();
|
|
8622
|
+
editorRef?.abort();
|
|
8623
|
+
}
|
|
8624
|
+
}).finally(() => editorRef?.redrawNow());
|
|
8625
|
+
}
|
|
8626
|
+
});
|
|
8627
|
+
try {
|
|
8628
|
+
await voiceIO.start();
|
|
8629
|
+
process.on("exit", () => voiceIO?.stop());
|
|
8630
|
+
for (const sig of ["SIGHUP", "SIGTERM"]) process.on(sig, () => {
|
|
8631
|
+
voiceIO?.stop();
|
|
8632
|
+
process.exit(0);
|
|
8633
|
+
});
|
|
8634
|
+
err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}) \u2014 just talk; speak over it to interrupt
|
|
8635
|
+
`));
|
|
8636
|
+
const where = cwd.split("/").pop();
|
|
8637
|
+
const resumed = session.messages.length > 0;
|
|
8638
|
+
void turn(
|
|
8639
|
+
`[session started] First call QuickLook with what:"memory" \u2014 if it knows the user's name or preferences, use them. Then greet the user warmly in one or two short sentences, as the opener of a live voice conversation. Context: working directory "${where}"${resumed ? "; this resumes an earlier conversation \u2014 glance at it and pick up naturally" : ""}. Personalize from whatever you learned (memory, prior conversation). Then ask what they'd like to do.`
|
|
8640
|
+
).finally(() => editorRef?.redrawNow());
|
|
8641
|
+
} catch (e) {
|
|
8642
|
+
err(yellow(` \u26A0 voice I/O failed to start: ${e?.message ?? e} \u2014 continuing text-only
|
|
8643
|
+
`));
|
|
8644
|
+
voiceIO = void 0;
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
}
|
|
7680
8648
|
while (true) {
|
|
7681
8649
|
if (pendingRewind) {
|
|
7682
8650
|
pendingRewind = false;
|
|
@@ -7684,6 +8652,7 @@ ${extra}` : body);
|
|
|
7684
8652
|
if (t !== void 0) prefill = t;
|
|
7685
8653
|
}
|
|
7686
8654
|
aborting = false;
|
|
8655
|
+
stashBuf = "";
|
|
7687
8656
|
err("\n");
|
|
7688
8657
|
const initial = prefill;
|
|
7689
8658
|
prefill = void 0;
|
|
@@ -7692,12 +8661,17 @@ ${extra}` : body);
|
|
|
7692
8661
|
const usd = session.meta.costUsd ?? 0;
|
|
7693
8662
|
const computeFooter = () => {
|
|
7694
8663
|
const parts = [];
|
|
8664
|
+
if (voiceIO) {
|
|
8665
|
+
const glyph = { listening: "\u{1F3A4}", thinking: "\u{1F4AD}", speaking: "\u{1F50A}", idle: "\xB7" }[voiceIO.state];
|
|
8666
|
+
parts.push(voicePartial && voiceIO.state === "listening" ? `\u{1F3A4} ${voicePartial.slice(-60)}` : `${glyph} ${voiceIO.state}`);
|
|
8667
|
+
}
|
|
7695
8668
|
if (ctxTok > 400) parts.push(`${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)`);
|
|
7696
8669
|
if (usd > 0) parts.push(`${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}`);
|
|
7697
8670
|
if (posture !== "default") parts.push(postureLabel());
|
|
7698
8671
|
const r = work.reasoning;
|
|
7699
8672
|
if (r && r !== "off") parts.push(`reasoning:${r}`);
|
|
7700
8673
|
if (verboseOutput) parts.push("verbose");
|
|
8674
|
+
if (inputStash.length) parts.push(`${inputStash.length} stashed (\u2303S to pop)`);
|
|
7701
8675
|
const taskLines = [];
|
|
7702
8676
|
if (dx) {
|
|
7703
8677
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -7724,6 +8698,18 @@ ${extra}` : body);
|
|
|
7724
8698
|
const picked = await pickModel(work.model);
|
|
7725
8699
|
if (picked) setModel(picked);
|
|
7726
8700
|
return picked;
|
|
8701
|
+
},
|
|
8702
|
+
onStash: (text) => {
|
|
8703
|
+
inputStash.push(text);
|
|
8704
|
+
err(`${green(" \u2713 stashed")} ${dim(`#${inputStash.length}: ${text.slice(0, 50)}${text.length > 50 ? "\u2026" : ""}`)}
|
|
8705
|
+
`);
|
|
8706
|
+
},
|
|
8707
|
+
onUnstash: () => {
|
|
8708
|
+
if (!inputStash.length) return void 0;
|
|
8709
|
+
const t = inputStash.pop();
|
|
8710
|
+
err(dim(` \u2191 unstashed${inputStash.length ? ` (${inputStash.length} left)` : ""}
|
|
8711
|
+
`));
|
|
8712
|
+
return t;
|
|
7727
8713
|
}
|
|
7728
8714
|
}));
|
|
7729
8715
|
if (result === null) break;
|
|
@@ -7733,59 +8719,27 @@ ${extra}` : body);
|
|
|
7733
8719
|
}
|
|
7734
8720
|
const line = result.trim();
|
|
7735
8721
|
if (!line) continue;
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
if (cmd) {
|
|
7741
|
-
err(dim(await runShellLine(agent.options.fs, cmd) + "\n"));
|
|
7742
|
-
}
|
|
7743
|
-
continue;
|
|
7744
|
-
}
|
|
7745
|
-
if (line.startsWith("#")) {
|
|
7746
|
-
const note = line.slice(1).trim();
|
|
7747
|
-
if (note) {
|
|
7748
|
-
const where = await appendMemoryNote(agent.options.fs, agent.options.memoryDir || adot("memory"), note);
|
|
7749
|
-
err(green(` \u270E remembered \u2192 ${where}
|
|
8722
|
+
let quit = await dispatchLine(line) === "quit";
|
|
8723
|
+
while (!quit && inputStash.length) {
|
|
8724
|
+
const next = inputStash.shift();
|
|
8725
|
+
err(dim(` \u23CE stashed \u203A ${next.slice(0, 60)}${next.length > 60 ? "\u2026" : ""}
|
|
7750
8726
|
`));
|
|
7751
|
-
|
|
7752
|
-
continue;
|
|
8727
|
+
quit = await dispatchLine(next) === "quit";
|
|
7753
8728
|
}
|
|
7754
|
-
if (
|
|
7755
|
-
const [name, ...a] = line.slice(1).split(/\s+/);
|
|
7756
|
-
if (!name) {
|
|
7757
|
-
err(red(" / needs a command name\n") + dim(" (try /help)\n"));
|
|
7758
|
-
continue;
|
|
7759
|
-
}
|
|
7760
|
-
const b = builtins[name];
|
|
7761
|
-
if (b) {
|
|
7762
|
-
if (await b.run(a)) break;
|
|
7763
|
-
continue;
|
|
7764
|
-
}
|
|
7765
|
-
const c = cmds.find((x) => x.name === name);
|
|
7766
|
-
if (c) {
|
|
7767
|
-
await runCommand(c, a.join(" "));
|
|
7768
|
-
continue;
|
|
7769
|
-
}
|
|
7770
|
-
const sk = skills.find((x) => x.name === name);
|
|
7771
|
-
if (sk) {
|
|
7772
|
-
await runSkill(sk, a.join(" "));
|
|
7773
|
-
continue;
|
|
7774
|
-
}
|
|
7775
|
-
const known = Object.keys(builtins).filter((k) => k !== "quit").map((k) => "/" + k);
|
|
7776
|
-
const custom = [...cmds.map((x) => x.name), ...skills.map((x) => x.name)].map((n) => "/" + n);
|
|
7777
|
-
err(red(` unknown command /${name}
|
|
7778
|
-
`) + dim(" builtins: " + known.join(" ") + "\n") + (custom.length ? dim(" custom: " + custom.join(" ") + "\n") : "") + dim(" (or /help)\n"));
|
|
7779
|
-
continue;
|
|
7780
|
-
}
|
|
7781
|
-
const task = pendingImages.length ? `${line} ${pendingImages.map((p) => "@" + p).join(" ")}` : line;
|
|
7782
|
-
pendingImages.length = 0;
|
|
7783
|
-
await turn(task);
|
|
8729
|
+
if (quit) break;
|
|
7784
8730
|
}
|
|
8731
|
+
voiceIO?.stop();
|
|
7785
8732
|
if (dx) {
|
|
7786
|
-
const running = [...dx.tasks.values()].filter((t) => t.status === "running")
|
|
7787
|
-
if (running) {
|
|
7788
|
-
|
|
8733
|
+
const running = [...dx.tasks.values()].filter((t) => t.status === "running");
|
|
8734
|
+
if (exitRequested && running.length) {
|
|
8735
|
+
for (const t of running) {
|
|
8736
|
+
t.status = "cancelled";
|
|
8737
|
+
t.controller.abort();
|
|
8738
|
+
}
|
|
8739
|
+
err(dim(` \u2026 cancelled ${running.length} background task(s)
|
|
8740
|
+
`));
|
|
8741
|
+
} else if (running.length) {
|
|
8742
|
+
err(dim(` \u2026 waiting for ${running.length} background task(s) (Ctrl-C to force quit)
|
|
7789
8743
|
`));
|
|
7790
8744
|
await dx.idle();
|
|
7791
8745
|
face.options.host?.flushText?.();
|
|
@@ -7861,7 +8815,7 @@ async function main() {
|
|
|
7861
8815
|
}
|
|
7862
8816
|
});
|
|
7863
8817
|
if (args.task) {
|
|
7864
|
-
const mounted = await mountMcp(cfg, new McpOAuth({ storePath:
|
|
8818
|
+
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") }));
|
|
7865
8819
|
const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
|
|
7866
8820
|
const store = new SessionStore(cwd);
|
|
7867
8821
|
const session = startSession(args, store, agent, cwd);
|