agent.libx.js 0.89.8 → 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/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 spawn2 = options.spawn ?? await nodeSpawn();
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 = spawn2("/bin/sh", ["-c", cmd], { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });
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
- log8.debug("shell spawn error", err2);
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 log8, clean, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
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
- log8 = forComponent("shell");
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 spawn2 = this.cfg.spawn ?? await nodeSpawn();
1488
- const proc = spawn2("/bin/sh", ["-c", command], { cwd: this.cfg.cwd, env: childEnv(this.cfg) });
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 existsSync7, readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1546
- import { homedir as homedir4, tmpdir } from "os";
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 join8, resolve as resolve3, basename as basename2, extname, dirname as dirname3 } from "path";
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 content = await this.dispatch(tc);
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
- result = await tool.run(args, this.ctx);
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
- return result;
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
- systemPrompt: VOICE_SYSTEM_PROMPT + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : ""),
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 toText(result) {
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 text = content.map((c) => typeof c?.text === "string" ? c.text : JSON.stringify(c)).join("\n");
3554
- if (text) return text;
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
- return toText(await callTool(spec.name, args ?? {}));
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 log7 = forComponent("mcp");
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) => log7.debug(`[${command}] stderr:`, chunk.trimEnd()));
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
- log7.debug("dropping non-JSON line from MCP server:", line, e);
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
- log7.debug("stdin end failed", e);
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
- log7.debug("skipping unparseable SSE data line", e);
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
- log7.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
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
- log7.info(`MCP "${name}" mounted \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}`);
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) log7.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
3793
- else log7.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
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: spawn2 }) => spawn2(cmd, args, { stdio: "ignore", detached: true }).unref());
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 now = /* @__PURE__ */ new Date();
4683
+ const now5 = /* @__PURE__ */ new Date();
4125
4684
  const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
4126
- const envNote = `Current date: ${now.toLocaleDateString("en-CA")} ${now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })} (${Intl.DateTimeFormat().resolvedOptions().timeZone})
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/config.ts
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 { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
4194
- import { join as join4 } from "path";
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 = join4(dir, ".agent", f);
4200
- if (!existsSync3(p)) continue;
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 = join4(dir, ".agent", "settings.json");
4213
- if (!existsSync3(p)) return {};
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(homedir2());
4237
- const user = await loadFrom(homedir2());
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 log9 = forComponent("hooks");
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 = spawnSync(rule.command, {
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
- log9.debug(`hook command failed: ${rule.command}`, e);
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 existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync } from "fs";
4371
- import { join as join5 } from "path";
4372
- var log10 = forComponent("session");
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 = join5(cwd, ".agent", "sessions");
5081
+ this.dir = join6(cwd, ".agent", "sessions");
4377
5082
  }
4378
5083
  /** Sortable, human-readable id: `YYYYMMDD-HHMMSS-mmm`. */
4379
- newId(now = Date.now()) {
4380
- const d = new Date(now);
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 (!existsSync4(this.dir)) mkdirSync3(this.dir, { recursive: true });
4391
- const path = join5(this.dir, `${data.meta.id}.json`);
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
- log10.debug(`rejecting unsafe session id: ${id}`);
5103
+ log14.debug(`rejecting unsafe session id: ${id}`);
4399
5104
  return void 0;
4400
5105
  }
4401
- const path = join5(this.dir, `${id}.json`);
4402
- if (!existsSync4(path)) return void 0;
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
- log10.debug(`unreadable session ${id} \u2014 ignoring`, e);
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 (!existsSync4(this.dir)) return [];
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(join5(this.dir, f), "utf8")).meta);
5122
+ metas.push(JSON.parse(readFileSync3(join6(this.dir, f), "utf8")).meta);
4418
5123
  } catch (e) {
4419
- log10.debug(`skipping unreadable session file ${f}`, e);
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 mkdirSync4, existsSync as existsSync5 } from "fs";
4512
- import { join as join6, resolve as resolve2, sep as sep2 } from "path";
4513
- var log11 = forComponent("checkpoints");
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 (!existsSync5(this.gitDir)) {
4541
- mkdirSync4(this.gitDir, { recursive: true });
5245
+ if (!existsSync6(this.gitDir)) {
5246
+ mkdirSync5(this.gitDir, { recursive: true });
4542
5247
  await this.run("init", "-q");
4543
5248
  }
4544
- writeFileSync4(join6(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
5249
+ writeFileSync4(join7(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
4545
5250
  this.ready = true;
4546
5251
  } catch (e) {
4547
- log11.debug(`git checkpoints unavailable for ${this.workTree}`, e);
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
- log11.debug("checkpoint prune failed", e);
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: join6(abs, ".agent", "checkpoints.git") });
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) => log11.debug("re-point failed", 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
- log11.debug("checkpoint commit failed", e);
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 = join6(process.cwd(), ".agent", "checkpoints.git");
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 existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
4736
- import { homedir as homedir3 } from "os";
4737
- import { join as join7 } from "path";
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) => join7(cwd, ".agent", "permissions.json");
5464
+ var PERM_FILE = (cwd) => join8(cwd, ".agent", "permissions.json");
4760
5465
  function loadPersistedRules(cwd) {
4761
5466
  const p = PERM_FILE(cwd);
4762
- if (!existsSync6(p)) return {};
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 = homedir3()) {
4771
- const files = [join7(home, ".claude", "settings.json"), join7(cwd, ".claude", "settings.json"), join7(cwd, ".claude", "settings.local.json")];
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 (!existsSync6(p)) continue;
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
- mkdirSync5(join7(cwd, ".agent"), { recursive: true });
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 = join7(homedir3(), ".agent", "trusted.json");
5507
+ var TRUST_FILE = join8(homedir4(), ".agent", "trusted.json");
4803
5508
  function isTrusted(cwd, file = TRUST_FILE) {
4804
5509
  try {
4805
- return existsSync6(file) && JSON.parse(readFileSync4(file, "utf8")).includes(cwd);
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 (existsSync6(file)) list = JSON.parse(readFileSync4(file, "utf8"));
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
- mkdirSync5(join7(file, ".."), { recursive: true });
5523
+ mkdirSync6(join8(file, ".."), { recursive: true });
4819
5524
  writeFileSync5(file, JSON.stringify(list, null, 2) + "\n");
4820
5525
  } catch {
4821
5526
  }
@@ -5564,7 +6269,11 @@ function createLineEditor(out) {
5564
6269
  // cyan
5565
6270
  };
5566
6271
  let curRow = 0;
6272
+ let suspended = false;
6273
+ let activeRedraw;
6274
+ let activeAbort;
5567
6275
  function render(s, promptArg, maxVisible, status) {
6276
+ if (suspended) return;
5568
6277
  const cols = out.columns ?? 80;
5569
6278
  const mode = !s.searching ? inputMode(s.buf) : void 0;
5570
6279
  const vimTag = s.vim === "normal" && !s.searching && !mode ? inverse(" N ") + " " : "";
@@ -5595,9 +6304,11 @@ function createLineEditor(out) {
5595
6304
  let footerRows = 0;
5596
6305
  const footer = status?.();
5597
6306
  if (footer) {
5598
- const f = visibleWidth(footer) > cols - 1 ? footer.slice(0, cols - 2) + "\u2026" : footer;
5599
- out.write("\r\n" + dim2(f));
5600
- footerRows = 1;
6307
+ for (const line of footer.split("\n")) {
6308
+ const f = visibleWidth(line) > cols - 1 ? line.slice(0, cols - 2) + "\u2026" : line;
6309
+ out.write("\r\n" + dim2(f));
6310
+ footerRows++;
6311
+ }
5601
6312
  }
5602
6313
  const up = inputRows + menuRows + footerRows - cursorRow;
5603
6314
  if (up > 0) out.write(`\x1B[${up}A`);
@@ -5621,11 +6332,24 @@ function createLineEditor(out) {
5621
6332
  curRow = 0;
5622
6333
  render(s, opts.prompt, maxVisible, opts.status);
5623
6334
  };
6335
+ activeRedraw = () => {
6336
+ curRow = 0;
6337
+ render(s, opts.prompt, maxVisible, opts.status);
6338
+ };
5624
6339
  process.on("SIGWINCH", onResize);
5625
6340
  return new Promise((resolve4) => {
6341
+ activeAbort = () => {
6342
+ finish();
6343
+ resolve4(null);
6344
+ };
5626
6345
  const redraw = () => render(s, opts.prompt, maxVisible, opts.status);
6346
+ let lastStatus = opts.status?.() ?? "";
5627
6347
  const ticker = opts.statusTickMs && opts.status ? setInterval(() => {
5628
- if (!s.pasting) redraw();
6348
+ if (s.pasting) return;
6349
+ const cur = opts.status();
6350
+ if (cur === lastStatus) return;
6351
+ lastStatus = cur;
6352
+ redraw();
5629
6353
  }, opts.statusTickMs) : void 0;
5630
6354
  const onKey = (str, key) => {
5631
6355
  if (key?.ctrl && key.name === "l") {
@@ -5649,6 +6373,21 @@ function createLineEditor(out) {
5649
6373
  redraw();
5650
6374
  return;
5651
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
+ }
5652
6391
  if (key?.meta && key.name === "p" && opts.onPickModel) {
5653
6392
  process.stdin.off("keypress", onKey);
5654
6393
  void opts.onPickModel().finally(() => {
@@ -5685,6 +6424,8 @@ function createLineEditor(out) {
5685
6424
  render(s, opts.prompt, maxVisible, opts.status);
5686
6425
  };
5687
6426
  const finish = () => {
6427
+ activeRedraw = void 0;
6428
+ activeAbort = void 0;
5688
6429
  if (ticker) clearInterval(ticker);
5689
6430
  process.stdin.off("keypress", onKey);
5690
6431
  process.removeListener("SIGWINCH", onResize);
@@ -5697,7 +6438,24 @@ function createLineEditor(out) {
5697
6438
  process.stdin.on("keypress", onKey);
5698
6439
  });
5699
6440
  }
5700
- return { readLine };
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
+ };
5701
6459
  }
5702
6460
  function selectMenu(out, opts) {
5703
6461
  if (!out.isTTY || !process.stdin.isTTY || !opts.items.length) return Promise.resolve(null);
@@ -5998,7 +6756,7 @@ var red = C("31");
5998
6756
  var bold = C("1");
5999
6757
  var yellow = C("33");
6000
6758
  var err = (s) => process.stderr.write(s);
6001
- var log12 = forComponent("cli");
6759
+ var log16 = forComponent("cli");
6002
6760
  var VERSION = (() => {
6003
6761
  try {
6004
6762
  return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -6025,6 +6783,9 @@ var spinner = /* @__PURE__ */ (() => {
6025
6783
  };
6026
6784
  })();
6027
6785
  var activeTurn = null;
6786
+ var exitRequested = false;
6787
+ var inputStash = [];
6788
+ var stashBuf = "";
6028
6789
  function numFlag(raw, flag) {
6029
6790
  const n = Number(raw);
6030
6791
  if (!Number.isFinite(n) || n < 0) throw new Error(`invalid ${flag}: ${raw ?? "(missing value)"}`);
@@ -6129,6 +6890,8 @@ Flags:
6129
6890
  to a background worker agent (-m model); results are re-voiced when ready
6130
6891
  --conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
6131
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)
6132
6895
  --voice-model <id> with --duplex: the fast voice model (default anthropic/claude-haiku-4-5)
6133
6896
  --add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
6134
6897
  --subagents allow the Task tool (spawn child agents)
@@ -6162,7 +6925,8 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
6162
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
6163
6926
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
6164
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.
6165
- REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 \u2192 or Tab accepts the dim history ghost-suggestion.
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.
6166
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.
6167
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).`;
6168
6932
  function newestModel() {
@@ -6180,11 +6944,11 @@ function resolveModelOrNewest(model) {
6180
6944
  }
6181
6945
  var ENV_KEY_ALIASES = { google: ["GEMINI_API_KEY"] };
6182
6946
  function loadInstallEnv() {
6183
- let dir = dirname3(import.meta.path);
6184
- for (let i = 0; i < 5 && !existsSync7(join8(dir, "package.json")); i++) dir = dirname3(dir);
6947
+ let dir = dirname4(import.meta.path);
6948
+ for (let i = 0; i < 5 && !existsSync8(join9(dir, "package.json")); i++) dir = dirname4(dir);
6185
6949
  for (const name of [".env", ".env.local"]) {
6186
- const file = join8(dir, name);
6187
- if (!existsSync7(file)) continue;
6950
+ const file = join9(dir, name);
6951
+ if (!existsSync8(file)) continue;
6188
6952
  for (const line of readFileSync5(file, "utf8").split("\n")) {
6189
6953
  const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
6190
6954
  if (!m || m[1] in process.env) continue;
@@ -6564,7 +7328,7 @@ async function mountMcp(cfg, oauth) {
6564
7328
  return mounted;
6565
7329
  }
6566
7330
  async function closeMcp(mounted) {
6567
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log12.debug("mcp close failed", e))));
7331
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log16.debug("mcp close failed", e))));
6568
7332
  }
6569
7333
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
6570
7334
  function readImageParts(cwd, line) {
@@ -6587,9 +7351,9 @@ function pastePathClassifier(cwd) {
6587
7351
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
6588
7352
  if (/\s/.test(t)) return null;
6589
7353
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
6590
- const abs = t.startsWith("~/") ? join8(homedir4(), t.slice(2)) : resolve3(cwd, t);
7354
+ const abs = t.startsWith("~/") ? join9(homedir5(), t.slice(2)) : resolve3(cwd, t);
6591
7355
  try {
6592
- if (!statSync2(abs).isFile()) return null;
7356
+ if (!statSync3(abs).isFile()) return null;
6593
7357
  } catch {
6594
7358
  return null;
6595
7359
  }
@@ -6692,7 +7456,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
6692
7456
  const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length;
6693
7457
  const ok = res.finishReason === "stop";
6694
7458
  const shortId = session.meta.id.slice(-10);
6695
- 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}
6696
7460
  `));
6697
7461
  if (res.finishReason === "error" && res.error) {
6698
7462
  const e = res.error;
@@ -6723,8 +7487,8 @@ function startSession(args, store, agent, cwd) {
6723
7487
  if (data) {
6724
7488
  agent.transcript = data.messages;
6725
7489
  if (args.fork) {
6726
- const now2 = Date.now();
6727
- const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(now2), created: now2, updated: now2, turns: data.meta.turns }, messages: data.messages };
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 };
6728
7492
  err(dim(` forked ${data.meta.id} \u2192 ${forked.meta.id} (${data.meta.turns} turns)
6729
7493
  `));
6730
7494
  if (!args.task) printHistory(data.messages);
@@ -6738,11 +7502,11 @@ function startSession(args, store, agent, cwd) {
6738
7502
  err(yellow(` no session to resume \u2014 starting fresh
6739
7503
  `));
6740
7504
  }
6741
- const now = Date.now();
6742
- const id = args.sessionId ?? store.newId(now);
7505
+ const now5 = Date.now();
7506
+ const id = args.sessionId ?? store.newId(now5);
6743
7507
  if (!args.task) err(dim(` session ${id}
6744
7508
  `));
6745
- return { meta: { id, created: now, updated: now, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
7509
+ return { meta: { id, created: now5, updated: now5, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
6746
7510
  }
6747
7511
  var AGENTS_MD_TEMPLATE = `# ${"${name}"}
6748
7512
 
@@ -6762,24 +7526,24 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
6762
7526
  `;
6763
7527
  function initInstructions(cwd) {
6764
7528
  for (const f of ["AGENTS.md", "CLAUDE.md"]) {
6765
- if (existsSync7(join8(cwd, f))) {
7529
+ if (existsSync8(join9(cwd, f))) {
6766
7530
  err(yellow(` ${f} already exists \u2014 leaving it as-is
6767
7531
  `));
6768
7532
  return;
6769
7533
  }
6770
7534
  }
6771
- const path = join8(cwd, "AGENTS.md");
7535
+ const path = join9(cwd, "AGENTS.md");
6772
7536
  writeFileSync6(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
6773
7537
  err(green(` created ${path}
6774
7538
  `) + dim(" edit it, then it auto-loads into every run.\n"));
6775
7539
  }
6776
7540
  function persistSetting(cwd, key, value) {
6777
- const path = join8(cwd, ".agent", "settings.json");
7541
+ const path = join9(cwd, ".agent", "settings.json");
6778
7542
  try {
6779
- const obj = existsSync7(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
7543
+ const obj = existsSync8(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
6780
7544
  if (obj[key] === value) return;
6781
7545
  obj[key] = value;
6782
- mkdirSync6(dirname3(path), { recursive: true });
7546
+ mkdirSync7(dirname4(path), { recursive: true });
6783
7547
  writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
6784
7548
  } catch (e) {
6785
7549
  err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
@@ -6796,14 +7560,14 @@ var isCancelTeardown = (e) => {
6796
7560
  function installCancelGuards(mounted) {
6797
7561
  process.on("unhandledRejection", (e) => {
6798
7562
  if (isCancelTeardown(e)) {
6799
- log12.debug("suppressed unhandledRejection (cursor stream cancel)", e);
7563
+ log16.debug("suppressed unhandledRejection (cursor stream cancel)", e);
6800
7564
  return;
6801
7565
  }
6802
- log12.error("unhandledRejection", e);
7566
+ log16.error("unhandledRejection", e);
6803
7567
  });
6804
7568
  process.on("uncaughtException", (e) => {
6805
7569
  if (isCancelTeardown(e)) {
6806
- log12.debug("suppressed uncaughtException (cursor stream cancel)", e);
7570
+ log16.debug("suppressed uncaughtException (cursor stream cancel)", e);
6807
7571
  return;
6808
7572
  }
6809
7573
  console.error(e);
@@ -6812,11 +7576,16 @@ function installCancelGuards(mounted) {
6812
7576
  });
6813
7577
  }
6814
7578
  async function repl(args, ai, cfg, cwd) {
6815
- const oauth = new McpOAuth({ storePath: join8(cwd, ".agent", "mcp-auth.json") });
7579
+ const oauth = new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") });
6816
7580
  const mounted = await mountMcp(cfg, oauth);
6817
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
+ })];
6818
7585
  const duplex = args.duplex;
6819
7586
  let dx;
7587
+ let voiceIO;
7588
+ let editorRef;
6820
7589
  let workerOptions;
6821
7590
  let duplexPersist = () => {
6822
7591
  };
@@ -6837,21 +7606,29 @@ async function repl(args, ai, cfg, cwd) {
6837
7606
  const host = {
6838
7607
  ...base,
6839
7608
  notify(e) {
7609
+ if (e.kind === "text_delta" && voiceIO) {
7610
+ voiceIO.speakDelta(e.message);
7611
+ editorRef?.suspend();
7612
+ }
6840
7613
  if (e.kind === "revoice_done") {
6841
7614
  base.flushText();
6842
7615
  process.stdout.write("\n");
7616
+ voiceIO?.endSpeech();
6843
7617
  duplexPersist();
7618
+ editorRef?.resume();
7619
+ editorRef?.redrawNow();
6844
7620
  return;
6845
7621
  }
6846
7622
  if (e.kind === "task_done" && e.data?.text) {
6847
7623
  const lines = String(e.data.text).split("\n");
6848
7624
  const shown = lines.slice(0, previewLines());
6849
- err("\n" + dim(` \u29BF ${e.message}
7625
+ err("\r\x1B[0J\n" + dim(` \u29BF ${e.message}
6850
7626
  `) + shown.map((l) => dim(` ${l}
6851
7627
  `)).join(""));
6852
7628
  if (lines.length > shown.length) err(dim(` \u2026 (+${lines.length - shown.length} more lines)
6853
7629
  `));
6854
7630
  duplexAccount(e.data);
7631
+ editorRef?.redrawNow();
6855
7632
  return;
6856
7633
  }
6857
7634
  base.notify(e);
@@ -6873,7 +7650,7 @@ async function repl(args, ai, cfg, cwd) {
6873
7650
  dx = new DuplexAgent({
6874
7651
  ai,
6875
7652
  fs: agent.options.fs,
6876
- ...args.voiceModel ? { voiceModel: resolveModelOrNewest(args.voiceModel) } : {},
7653
+ ...args.voiceModel ?? cfg.voiceModel ? { voiceModel: resolveModelOrNewest(args.voiceModel ?? cfg.voiceModel) } : {},
6877
7654
  workerModel: agent.options.model,
6878
7655
  workerOptions,
6879
7656
  host,
@@ -6883,9 +7660,34 @@ async function repl(args, ai, cfg, cwd) {
6883
7660
  onTaskStart: async (_id, label) => {
6884
7661
  await checkpoints.begin(label);
6885
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
+ },
6886
7686
  // The voice runs on the REAL fs (it has no fs tools — harmless) so @mentions, !cmd and #note
6887
7687
  // resolve against the project; + CC-parity chrome for its own tool calls (⚙ Delegate …).
6888
- voiceOptions: { fs: agent.options.fs, hooks: displayHooks(agent.options.fs), tools: [rewindFilesTool] }
7688
+ voiceOptions: { fs: agent.options.fs, hooks: displayHooks(agent.options.fs), tools: [rewindFilesTool, exitSessionTool(() => {
7689
+ exitRequested = true;
7690
+ })] }
6889
7691
  });
6890
7692
  }
6891
7693
  const face = dx ? dx.voice : agent;
@@ -6941,9 +7743,9 @@ async function repl(args, ai, cfg, cwd) {
6941
7743
  };
6942
7744
  const pendingImages = [];
6943
7745
  const grabClipboardAttachment = () => {
6944
- const dir = join8(tmpdir(), "agentx-pasted");
7746
+ const dir = join9(tmpdir(), "agentx-pasted");
6945
7747
  try {
6946
- mkdirSync6(dir, { recursive: true });
7748
+ mkdirSync7(dir, { recursive: true });
6947
7749
  } catch {
6948
7750
  }
6949
7751
  const img = grabClipboardImage(dir, String(Date.now()));
@@ -6952,15 +7754,17 @@ async function repl(args, ai, cfg, cwd) {
6952
7754
  process.on("SIGINT", () => {
6953
7755
  if (activeTurn) {
6954
7756
  activeTurn.abort();
7757
+ voiceIO?.interrupt();
6955
7758
  return;
6956
7759
  }
7760
+ voiceIO?.stop();
6957
7761
  void closeMcp(mounted);
6958
7762
  process.exit(130);
6959
7763
  });
6960
7764
  installCancelGuards(mounted);
6961
7765
  const store = new SessionStore(cwd);
6962
7766
  let session = startSession(args, store, face, cwd);
6963
- const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join8(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
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 });
6964
7768
  const cpHooks = checkpoints.hooks?.();
6965
7769
  if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
6966
7770
  duplexPersist = () => {
@@ -6987,17 +7791,17 @@ async function repl(args, ai, cfg, cwd) {
6987
7791
  const fs = agent.options.fs;
6988
7792
  const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
6989
7793
  const adot = (sub) => `${fsBase}/.agent/${sub}`;
6990
- const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir4()}/.agent/${sub}`, `${homedir4()}/.claude/${sub}`];
7794
+ const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir5()}/.agent/${sub}`, `${homedir5()}/.claude/${sub}`];
6991
7795
  const cmds = (await loadCommands(fs, adots("commands"))).commands;
6992
7796
  const skills = (await loadSkills(fs, adots("skills"))).skills;
6993
- const histPath = join8(cwd, ".agent", "history");
6994
- const history = existsSync7(histPath) ? readFileSync5(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
7797
+ const histPath = join9(cwd, ".agent", "history");
7798
+ const history = existsSync8(histPath) ? readFileSync5(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
6995
7799
  const remember = (line) => {
6996
7800
  try {
6997
- mkdirSync6(join8(cwd, ".agent"), { recursive: true });
7801
+ mkdirSync7(join9(cwd, ".agent"), { recursive: true });
6998
7802
  appendFileSync(histPath, line + "\n");
6999
7803
  } catch (e) {
7000
- log12.debug("history write failed", e);
7804
+ log16.debug("history write failed", e);
7001
7805
  }
7002
7806
  };
7003
7807
  const ago = (t) => {
@@ -7055,7 +7859,7 @@ async function repl(args, ai, cfg, cwd) {
7055
7859
  try {
7056
7860
  store.save(session);
7057
7861
  } catch (e) {
7058
- log12.debug("session save after rewind failed", e);
7862
+ log16.debug("session save after rewind failed", e);
7059
7863
  }
7060
7864
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
7061
7865
  `));
@@ -7076,11 +7880,18 @@ async function repl(args, ai, cfg, cwd) {
7076
7880
  if (data) resumeInto(data);
7077
7881
  else err(red(" no such session\n"));
7078
7882
  };
7883
+ const announcedTasks = /* @__PURE__ */ new Set();
7079
7884
  const turn = async (task) => {
7080
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();
7081
7891
  if (dx) {
7082
- const running = [...dx.tasks.values()].filter((t) => t.status === "running");
7083
- if (running.length) err(cyan(` \u25D4 ${running.length === 1 ? `task ${running[0].id} (${running[0].label})` : `${running.length} tasks`} still working in the background`) + dim(" \u2014 the result will appear here; keep chatting meanwhile\n"));
7892
+ const fresh = [...dx.tasks.values()].filter((t) => t.status === "running" && !announcedTasks.has(t.id));
7893
+ fresh.forEach((t) => announcedTasks.add(t.id));
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"));
7084
7895
  }
7085
7896
  return r;
7086
7897
  };
@@ -7248,6 +8059,26 @@ ${extra}` : body);
7248
8059
  else err(dim(" " + (duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model) + "\n"));
7249
8060
  }
7250
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
+ } } : {},
7251
8082
  reasoning: {
7252
8083
  desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker (duplex: the workers')",
7253
8084
  run: async (a) => {
@@ -7476,7 +8307,7 @@ ${extra}` : body);
7476
8307
  }
7477
8308
  const m = mounted.splice(idx, 1)[0];
7478
8309
  removeWorkTools(m.tools.map((t) => t.name));
7479
- await m.client.close().catch((e) => log12.debug("mcp close failed", e));
8310
+ await m.client.close().catch((e) => log16.debug("mcp close failed", e));
7480
8311
  err(dim(` removed "${name}"
7481
8312
  `));
7482
8313
  return;
@@ -7591,10 +8422,10 @@ ${extra}` : body);
7591
8422
  return;
7592
8423
  }
7593
8424
  const md = exportMarkdown(session.meta, shown);
7594
- const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join8(".agent", "exports", `${session.meta.id}.md`);
8425
+ const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join9(".agent", "exports", `${session.meta.id}.md`);
7595
8426
  const path = resolve3(cwd, name);
7596
8427
  try {
7597
- mkdirSync6(dirname3(path), { recursive: true });
8428
+ mkdirSync7(dirname4(path), { recursive: true });
7598
8429
  writeFileSync6(path, md);
7599
8430
  err(green(` \u2713 exported \u2192 ${path}
7600
8431
  `) + dim(` ${shown.length} message(s) \xB7 ${md.length} chars
@@ -7615,9 +8446,9 @@ ${extra}` : body);
7615
8446
  `));
7616
8447
  const listDir = (absDir) => {
7617
8448
  try {
7618
- return readdirSync2(join8(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
8449
+ return readdirSync2(join9(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
7619
8450
  } catch (e) {
7620
- log12.debug("completion readdir failed", absDir, e);
8451
+ log16.debug("completion readdir failed", absDir, e);
7621
8452
  return null;
7622
8453
  }
7623
8454
  };
@@ -7628,24 +8459,61 @@ ${extra}` : body);
7628
8459
  return { hits, token, describe };
7629
8460
  };
7630
8461
  const editor = createLineEditor(process.stderr);
8462
+ editorRef = editor;
7631
8463
  let aborting = false;
7632
8464
  let pendingRewind = false;
7633
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
+ };
7634
8471
  process.stdin.on("keypress", (_s, key) => {
7635
8472
  if (!activeTurn) return;
7636
8473
  if (key?.ctrl && key?.name === "o") {
7637
8474
  toggleVerbose();
7638
8475
  return;
7639
8476
  }
7640
- const cancel = key?.name === "escape" || key?.ctrl && key?.name === "c";
7641
- if (!cancel) return;
7642
- if (!aborting) {
7643
- aborting = true;
7644
- activeTurn.abort();
7645
- err(yellow("\n \u238B cancelling\u2026\n"));
7646
- } else if (key?.name === "escape" && !pendingRewind) {
7647
- pendingRewind = true;
7648
- err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
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;
7649
8517
  }
7650
8518
  });
7651
8519
  }
@@ -7663,6 +8531,120 @@ ${extra}` : body);
7663
8531
  };
7664
8532
  let prefill;
7665
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
+ }
7666
8648
  while (true) {
7667
8649
  if (pendingRewind) {
7668
8650
  pendingRewind = false;
@@ -7670,6 +8652,7 @@ ${extra}` : body);
7670
8652
  if (t !== void 0) prefill = t;
7671
8653
  }
7672
8654
  aborting = false;
8655
+ stashBuf = "";
7673
8656
  err("\n");
7674
8657
  const initial = prefill;
7675
8658
  prefill = void 0;
@@ -7678,17 +8661,24 @@ ${extra}` : body);
7678
8661
  const usd = session.meta.costUsd ?? 0;
7679
8662
  const computeFooter = () => {
7680
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
+ }
7681
8668
  if (ctxTok > 400) parts.push(`${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)`);
7682
8669
  if (usd > 0) parts.push(`${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}`);
7683
8670
  if (posture !== "default") parts.push(postureLabel());
7684
8671
  const r = work.reasoning;
7685
8672
  if (r && r !== "off") parts.push(`reasoning:${r}`);
7686
8673
  if (verboseOutput) parts.push("verbose");
7687
- if (dx?.tasks.size) {
8674
+ if (inputStash.length) parts.push(`${inputStash.length} stashed (\u2303S to pop)`);
8675
+ const taskLines = [];
8676
+ if (dx) {
7688
8677
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7689
- parts.push(`tasks: ${[...dx.tasks.values()].map((t) => t.status === "running" ? `${frames[tick++ % frames.length]} ${t.id} working\u2026` : `${t.id}:${t.status}`).join(" ")}`);
8678
+ for (const t of dx.tasks.values()) if (t.status === "running") taskLines.push(`\u25D4 ${frames[tick % frames.length]} ${t.id} ${t.label} \u2014 working in background\u2026`);
8679
+ if (taskLines.length) tick++;
7690
8680
  }
7691
- return parts.join(" \xB7 ");
8681
+ return [...taskLines, parts.join(" \xB7 ")].filter(Boolean).join("\n");
7692
8682
  };
7693
8683
  const result = await readMultiline((cont) => editor.readLine({
7694
8684
  prompt: cont ? contPrompt : promptStr,
@@ -7708,6 +8698,18 @@ ${extra}` : body);
7708
8698
  const picked = await pickModel(work.model);
7709
8699
  if (picked) setModel(picked);
7710
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;
7711
8713
  }
7712
8714
  }));
7713
8715
  if (result === null) break;
@@ -7717,59 +8719,27 @@ ${extra}` : body);
7717
8719
  }
7718
8720
  const line = result.trim();
7719
8721
  if (!line) continue;
7720
- history.unshift(line.replace(/\n+/g, " \u23CE "));
7721
- remember(line.replace(/\n+/g, " \u23CE "));
7722
- if (line.startsWith("!")) {
7723
- const cmd = line.slice(1).trim();
7724
- if (cmd) {
7725
- err(dim(await runShellLine(agent.options.fs, cmd) + "\n"));
7726
- }
7727
- continue;
7728
- }
7729
- if (line.startsWith("#")) {
7730
- const note = line.slice(1).trim();
7731
- if (note) {
7732
- const where = await appendMemoryNote(agent.options.fs, agent.options.memoryDir || adot("memory"), note);
7733
- 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" : ""}
7734
8726
  `));
7735
- }
7736
- continue;
8727
+ quit = await dispatchLine(next) === "quit";
7737
8728
  }
7738
- if (line.startsWith("/")) {
7739
- const [name, ...a] = line.slice(1).split(/\s+/);
7740
- if (!name) {
7741
- err(red(" / needs a command name\n") + dim(" (try /help)\n"));
7742
- continue;
7743
- }
7744
- const b = builtins[name];
7745
- if (b) {
7746
- if (await b.run(a)) break;
7747
- continue;
7748
- }
7749
- const c = cmds.find((x) => x.name === name);
7750
- if (c) {
7751
- await runCommand(c, a.join(" "));
7752
- continue;
7753
- }
7754
- const sk = skills.find((x) => x.name === name);
7755
- if (sk) {
7756
- await runSkill(sk, a.join(" "));
7757
- continue;
7758
- }
7759
- const known = Object.keys(builtins).filter((k) => k !== "quit").map((k) => "/" + k);
7760
- const custom = [...cmds.map((x) => x.name), ...skills.map((x) => x.name)].map((n) => "/" + n);
7761
- err(red(` unknown command /${name}
7762
- `) + dim(" builtins: " + known.join(" ") + "\n") + (custom.length ? dim(" custom: " + custom.join(" ") + "\n") : "") + dim(" (or /help)\n"));
7763
- continue;
7764
- }
7765
- const task = pendingImages.length ? `${line} ${pendingImages.map((p) => "@" + p).join(" ")}` : line;
7766
- pendingImages.length = 0;
7767
- await turn(task);
8729
+ if (quit) break;
7768
8730
  }
8731
+ voiceIO?.stop();
7769
8732
  if (dx) {
7770
- const running = [...dx.tasks.values()].filter((t) => t.status === "running").length;
7771
- if (running) {
7772
- err(dim(` \u2026 waiting for ${running} background task(s) (Ctrl-C to force quit)
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)
7773
8743
  `));
7774
8744
  await dx.idle();
7775
8745
  face.options.host?.flushText?.();
@@ -7845,7 +8815,7 @@ async function main() {
7845
8815
  }
7846
8816
  });
7847
8817
  if (args.task) {
7848
- const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join8(cwd, ".agent", "mcp-auth.json") }));
8818
+ const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") }));
7849
8819
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
7850
8820
  const store = new SessionStore(cwd);
7851
8821
  const session = startSession(args, store, agent, cwd);