agent.libx.js 0.94.22 → 0.94.24

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
@@ -254,12 +254,14 @@ var init_tools_structured = __esm({
254
254
  const ctxN = Math.max(0, Number(context ?? 0));
255
255
  const out = [];
256
256
  const matched = [];
257
+ let skipped = 0;
257
258
  for (const path of files) {
258
259
  ckAbort(ctx.signal);
259
260
  let content;
260
261
  try {
261
262
  content = await ctx.fs.readFile(path);
262
263
  } catch {
264
+ skipped++;
263
265
  continue;
264
266
  }
265
267
  const lines = content.split("\n");
@@ -274,8 +276,10 @@ var init_tools_structured = __esm({
274
276
  }
275
277
  if (fileHit) matched.push(path);
276
278
  }
277
- if (filesOnly) return matched.length ? matched.join("\n") : "(no matches)";
278
- return out.length ? out.join("\n") : "(no matches)";
279
+ const note = skipped ? `
280
+ [skipped ${skipped} unreadable file${skipped === 1 ? "" : "s"}]` : "";
281
+ if (filesOnly) return (matched.length ? matched.join("\n") : "(no matches)") + note;
282
+ return (out.length ? out.join("\n") : "(no matches)") + note;
279
283
  }
280
284
  };
281
285
  SIG_RE = /^\s*(export\b|(?:export\s+)?(?:async\s+)?function\s+\*?\w|(?:export\s+)?(?:abstract\s+)?class\s+\w|(?:export\s+)?interface\s+\w|(?:export\s+)?type\s+\w|(?:export\s+)?enum\s+\w)/;
@@ -964,7 +968,7 @@ var init_tools = __esm({
964
968
  IMG_MIME = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
965
969
  readTool = {
966
970
  name: "Read",
967
- description: "Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. Always Read a file before Editing it.",
971
+ description: "Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.",
968
972
  parameters: {
969
973
  type: "object",
970
974
  required: ["path"],
@@ -976,6 +980,12 @@ var init_tools = __esm({
976
980
  },
977
981
  async run({ path, offset, limit }, ctx) {
978
982
  const ext = String(path).toLowerCase().split(".").pop() ?? "";
983
+ if (ext === "pdf") {
984
+ if (!ctx.pdfText) return `[${path} is a PDF \u2014 text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;
985
+ if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
986
+ const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();
987
+ return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;
988
+ }
979
989
  if (IMG_MIME[ext]) {
980
990
  const fs = ctx.fs;
981
991
  if (typeof fs.readFileBytes !== "function") {
@@ -1053,7 +1063,7 @@ var NodeDiskFilesystem;
1053
1063
  var init_NodeDiskFilesystem = __esm({
1054
1064
  "src/NodeDiskFilesystem.ts"() {
1055
1065
  "use strict";
1056
- NodeDiskFilesystem = class {
1066
+ NodeDiskFilesystem = class _NodeDiskFilesystem {
1057
1067
  constructor(baseDir, opts = {}) {
1058
1068
  this.baseDir = baseDir;
1059
1069
  this.opts = { denySymlinks: true, ...opts };
@@ -1068,14 +1078,24 @@ var init_NodeDiskFilesystem = __esm({
1068
1078
  real(vpath) {
1069
1079
  return np.join(this.baseDir, "." + vpath);
1070
1080
  }
1081
+ // Verified non-symlink DIRECTORY components, with a short TTL: tree walks (Glob/Grep) hit the same
1082
+ // parents thousands of times; re-lstat'ing each per op is the dominant syscall cost. The 1s window
1083
+ // is an accepted race only against an out-of-band symlink swap mid-walk (this FS can't create
1084
+ // symlinks itself); leaf components are never cached.
1085
+ verified = /* @__PURE__ */ new Map();
1086
+ static VERIFY_TTL_MS = 1e3;
1071
1087
  /** Throw if any existing component of `real` is a symlink (escape vector). */
1072
1088
  async assertNoSymlink(real) {
1073
1089
  if (!this.opts.denySymlinks) return;
1074
1090
  const rel = np.relative(this.baseDir, real);
1075
1091
  if (rel === "" || rel.startsWith("..")) return;
1092
+ const parts = rel.split(np.sep);
1076
1093
  let cur = this.baseDir;
1077
- for (const part of rel.split(np.sep)) {
1078
- cur = np.join(cur, part);
1094
+ const now5 = Date.now();
1095
+ for (let i = 0; i < parts.length; i++) {
1096
+ cur = np.join(cur, parts[i]);
1097
+ const isLeaf = i === parts.length - 1;
1098
+ if (!isLeaf && (this.verified.get(cur) ?? 0) > now5) continue;
1079
1099
  let st;
1080
1100
  try {
1081
1101
  st = await fsp.lstat(cur);
@@ -1083,6 +1103,10 @@ var init_NodeDiskFilesystem = __esm({
1083
1103
  return;
1084
1104
  }
1085
1105
  if (st.isSymbolicLink()) throw new Error("File not found: symlink not permitted");
1106
+ if (!isLeaf) {
1107
+ if (this.verified.size > 1e4) this.verified.clear();
1108
+ this.verified.set(cur, now5 + _NodeDiskFilesystem.VERIFY_TTL_MS);
1109
+ }
1086
1110
  }
1087
1111
  }
1088
1112
  resolvePath(path, cwd) {
@@ -1418,10 +1442,10 @@ function sandboxArgv(command, cwd, opts = {}, platform2 = process.platform, tmpD
1418
1442
  return null;
1419
1443
  }
1420
1444
  async function findSandboxWrapper(platform2 = process.platform) {
1421
- const { existsSync: existsSync9 } = await import("fs");
1422
- if (platform2 === "darwin") return existsSync9("/usr/bin/sandbox-exec") ? "/usr/bin/sandbox-exec" : null;
1445
+ const { existsSync: existsSync10 } = await import("fs");
1446
+ if (platform2 === "darwin") return existsSync10("/usr/bin/sandbox-exec") ? "/usr/bin/sandbox-exec" : null;
1423
1447
  if (platform2 === "linux") {
1424
- for (const dir of (process.env.PATH ?? "/usr/bin:/bin").split(":")) if (dir && existsSync9(`${dir}/bwrap`)) return `${dir}/bwrap`;
1448
+ for (const dir of (process.env.PATH ?? "/usr/bin:/bin").split(":")) if (dir && existsSync10(`${dir}/bwrap`)) return `${dir}/bwrap`;
1425
1449
  return null;
1426
1450
  }
1427
1451
  return null;
@@ -1689,8 +1713,8 @@ var init_tools_shell = __esm({
1689
1713
 
1690
1714
  // cli/cli.ts
1691
1715
  import { createInterface } from "readline/promises";
1692
- import { existsSync as existsSync8, readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
1693
- import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
1716
+ import { existsSync as existsSync9, readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, readdirSync as readdirSync3, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
1717
+ import { homedir as homedir8, tmpdir as tmpdir3 } from "os";
1694
1718
 
1695
1719
  // cli/clipboard.ts
1696
1720
  import { execFileSync } from "child_process";
@@ -1744,9 +1768,20 @@ close access f`;
1744
1768
  }
1745
1769
  return null;
1746
1770
  }
1771
+ function copyTextToClipboard(text, platform2 = process.platform) {
1772
+ const candidates = platform2 === "darwin" ? [["pbcopy", []]] : platform2 === "linux" ? [["wl-copy", []], ["xclip", ["-selection", "clipboard"]]] : [];
1773
+ for (const [cmd, args] of candidates) {
1774
+ try {
1775
+ execFileSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
1776
+ return true;
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ return false;
1781
+ }
1747
1782
 
1748
1783
  // cli/cli.ts
1749
- import { join as join9, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
1784
+ import { join as join11, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
1750
1785
  import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions } from "ai.libx.js";
1751
1786
 
1752
1787
  // src/llm.ts
@@ -2839,6 +2874,8 @@ var AgentOptions = class {
2839
2874
  permissions;
2840
2875
  /** Opt-in syntax guardrail: refuse to persist a syntactically-broken code-file write/edit. Default off. */
2841
2876
  lintOnWrite;
2877
+ /** Optional PDF text extraction for Read on .pdf files (node hosts wire pdftotext); absent => Read explains. */
2878
+ pdfText;
2842
2879
  /** Opt-in: after a write-class tool runs, run `command` over the VFS and append any failure to the tool result.
2843
2880
  * `tools` defaults to ['Write','Edit','MultiEdit','ApplyEdits']. */
2844
2881
  autoTest;
@@ -2889,8 +2926,12 @@ var Agent = class _Agent {
2889
2926
  reprepare() {
2890
2927
  this.prepared = false;
2891
2928
  }
2892
- /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn. */
2929
+ /** Tools injected via addTools(); kept separate from options.tools so prepare() rebuilds don't drop them. */
2930
+ injectedTools = [];
2931
+ /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn
2932
+ * and survives prepare() rebuilds (reprepare(), new conversations). */
2893
2933
  addTools(tools) {
2934
+ this.injectedTools.push(...tools);
2894
2935
  this.activeTools.push(...tools);
2895
2936
  }
2896
2937
  /** Remove tools by name from a running agent. Returns the count removed. */
@@ -2898,6 +2939,7 @@ var Agent = class _Agent {
2898
2939
  const s = names instanceof Set ? names : new Set(names);
2899
2940
  const before = this.activeTools.length;
2900
2941
  this.activeTools = this.activeTools.filter((t) => !s.has(t.name));
2942
+ this.injectedTools = this.injectedTools.filter((t) => !s.has(t.name));
2901
2943
  return before - this.activeTools.length;
2902
2944
  }
2903
2945
  constructor(options) {
@@ -2909,6 +2951,7 @@ var Agent = class _Agent {
2909
2951
  this.ctx = makeContext(this.options.fs, this.options.host);
2910
2952
  this.ctx.signal = this.options.signal;
2911
2953
  if (this.options.lintOnWrite) this.ctx.lint = checkSyntax;
2954
+ if (this.options.pdfText) this.ctx.pdfText = this.options.pdfText;
2912
2955
  this.ctx.ai = this.options.ai;
2913
2956
  this.ctx.model = this.options.model;
2914
2957
  this.ctx.parkHuman = (p) => this.park(p);
@@ -2981,7 +3024,7 @@ var Agent = class _Agent {
2981
3024
  const plan = o.planMode ? planMode({ host: o.host }) : void 0;
2982
3025
  if (plan) tools = [...tools, plan.tool];
2983
3026
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
2984
- this.activeTools = tools;
3027
+ this.activeTools = [...tools, ...this.injectedTools];
2985
3028
  this.systemPromptCache = systemPrompt;
2986
3029
  this.prepared = true;
2987
3030
  return systemPrompt;
@@ -3116,7 +3159,15 @@ var Agent = class _Agent {
3116
3159
  } catch (err2) {
3117
3160
  if (err2?.code === "budget") return kill("budget");
3118
3161
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
3119
- log3.error(`chat() failed: ${err2?.message ?? err2}`, err2);
3162
+ const body = err2?.body ?? err2?.response?.data ?? err2?.error;
3163
+ let bodyStr;
3164
+ try {
3165
+ bodyStr = body && typeof body !== "string" ? JSON.stringify(body).slice(0, 2e3) : body;
3166
+ } catch {
3167
+ bodyStr = void 0;
3168
+ }
3169
+ if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
3170
+ log3.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err2);
3120
3171
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
3121
3172
  }
3122
3173
  if (o.signal?.aborted) return kill("aborted");
@@ -3347,20 +3398,28 @@ function stubOldToolResults(messages, keep) {
3347
3398
  return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
3348
3399
  });
3349
3400
  }
3350
- var hasCallFor = (msgs, id) => msgs.some((m) => m.role === "assistant" && m.tool_calls?.some((tc) => tc.id === id));
3401
+ var callIdSet = (msgs) => {
3402
+ const ids = /* @__PURE__ */ new Set();
3403
+ for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
3404
+ return ids;
3405
+ };
3351
3406
  function dropOrphanToolResults(messages) {
3352
- const ok = (m) => m.role !== "tool" || hasCallFor(messages, m.tool_call_id);
3407
+ const ids = callIdSet(messages);
3408
+ const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
3353
3409
  return messages.every(ok) ? messages : messages.filter(ok);
3354
3410
  }
3355
3411
  function fitTokenBudget(messages, maxTokens) {
3356
- if (estimateTokens(messages) <= maxTokens) return messages;
3412
+ const per = messages.map((x) => estimateTokens([x]));
3413
+ let total = per.reduce((a, b) => a + b, 0);
3414
+ if (total <= maxTokens) return messages;
3357
3415
  const head = messages[0]?.role === "system" ? [messages[0]] : [];
3358
- let body = messages.slice(head.length);
3359
- while (body.length && estimateTokens([...head, ...body]) > maxTokens) body = body.slice(1);
3360
- while (body.length && body[0].role === "tool" && !hasCallFor(body, body[0].tool_call_id)) body = body.slice(1);
3361
- if (estimateTokens([...head, ...body]) > maxTokens)
3362
- log3.warn(`context ~${estimateTokens([...head, ...body])} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3363
- return [...head, ...body];
3416
+ let from = head.length;
3417
+ while (from < messages.length && total > maxTokens) total -= per[from++];
3418
+ const ids = callIdSet(messages.slice(from));
3419
+ while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
3420
+ if (total > maxTokens)
3421
+ log3.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3422
+ return [...head, ...messages.slice(from)];
3364
3423
  }
3365
3424
  function compact(m, max, focus) {
3366
3425
  const hasSystem = m[0]?.role === "system";
@@ -3812,11 +3871,11 @@ var Scheduler = class {
3812
3871
  this.jobs.clear();
3813
3872
  }
3814
3873
  };
3815
- function makeScheduleTools(scheduler) {
3874
+ function makeScheduleTools(scheduler, os) {
3816
3875
  return [
3817
3876
  {
3818
3877
  name: "ScheduleTask",
3819
- description: 'Schedule a prompt to fire automatically while this session is alive.\nModes:\n \u2022 One-off: {at: <epoch_ms>} \u2014 fires once at that time, then done.\n \u2022 Interval: {everyMs: <ms>} \u2014 fires repeatedly (\u22651s).\n \u2022 Cron: {cron: "min hr dom mon dow"} \u2014 standard 5-field cron.\nReturns the job id. Jobs only fire while the CLI session is running \u2014 they do NOT survive quitting.',
3878
+ description: 'Schedule a prompt to fire automatically.\nModes:\n \u2022 One-off: {at: <epoch_ms>} \u2014 fires once at that time, then done.\n \u2022 Interval: {everyMs: <ms>} \u2014 fires repeatedly (\u22651s).\n \u2022 Cron: {cron: "min hr dom mon dow"} \u2014 standard 5-field cron.\nBackend: "session" fires while this CLI session is alive (default for recurring + near one-offs); "os" registers with the OS scheduler so the job survives quitting \u2014 it headless-resumes this session when it fires (auto-chosen for one-offs \u226530min out when available). Pass backend:"os" explicitly for recurring jobs that must outlive the session.',
3820
3879
  parameters: {
3821
3880
  type: "object",
3822
3881
  required: ["prompt", "trigger"],
@@ -3831,15 +3890,22 @@ function makeScheduleTools(scheduler) {
3831
3890
  cron: { type: "string" }
3832
3891
  }
3833
3892
  },
3834
- label: { type: "string", description: "Short label for display (optional)." }
3893
+ label: { type: "string", description: "Short label for display (optional)." },
3894
+ backend: { type: "string", enum: ["auto", "session", "os"], description: "Where the job lives (default auto)." }
3835
3895
  }
3836
3896
  },
3837
- async run({ prompt, trigger, label }) {
3897
+ async run({ prompt, trigger, label, backend }) {
3838
3898
  try {
3899
+ if (os && os.route(trigger, backend) === "os") {
3900
+ const id2 = `os-${Date.now().toString(36)}`;
3901
+ const mechanism = os.schedule({ id: id2, prompt, sessionId: os.sessionId, cwd: os.cwd, trigger, label });
3902
+ return `Scheduled ${id2}${label ? ` (${label})` : ""} on the OS scheduler (${mechanism}) \u2014 survives quitting; fires \`agentx --resume ${os.sessionId}\` headless.`;
3903
+ }
3904
+ if (backend === "os") return "Error: no OS scheduler available on this platform \u2014 job not created (use the default in-session backend).";
3839
3905
  const id = scheduler.add({ prompt, trigger, label });
3840
3906
  const job = scheduler.get(id);
3841
3907
  const next = scheduler.nextFire(job);
3842
- return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}.`;
3908
+ return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}. (In-session: does not survive quitting.)`;
3843
3909
  } catch (e) {
3844
3910
  return `Error: ${e?.message ?? e}`;
3845
3911
  }
@@ -3847,24 +3913,28 @@ function makeScheduleTools(scheduler) {
3847
3913
  },
3848
3914
  {
3849
3915
  name: "ScheduleList",
3850
- description: "List all scheduled jobs and their next fire time.",
3916
+ description: "List all scheduled jobs (in-session + OS-backed) and their next fire time.",
3851
3917
  parameters: { type: "object", properties: {} },
3852
3918
  async run() {
3919
+ const osJobs = os?.list() ?? [];
3920
+ const osLines = osJobs.map((j) => `${j.id} os ${j.mechanism}${j.label ? " " + j.label : ""}`);
3853
3921
  const jobs = scheduler.list();
3854
- if (!jobs.length) return "(no scheduled jobs)";
3855
- return jobs.map((j) => {
3922
+ if (!jobs.length && !osLines.length) return "(no scheduled jobs)";
3923
+ return [...osLines, ...jobs.map((j) => {
3856
3924
  const next = scheduler.nextFire(j);
3857
3925
  const trig = "at" in j.trigger ? `once @ ${new Date(j.trigger.at).toLocaleString()}` : "everyMs" in j.trigger ? `every ${(j.trigger.everyMs / 1e3).toFixed(0)}s` : `cron: ${j.trigger.cron}`;
3858
3926
  return `${j.id} ${j.status} ${trig} runs:${j.runs} next:${next ? new Date(next).toLocaleTimeString() : "\u2014"}${j.label ? " " + j.label : ""}`;
3859
- }).join("\n");
3927
+ })].join("\n");
3860
3928
  }
3861
3929
  },
3862
3930
  {
3863
3931
  name: "ScheduleCancel",
3864
- description: "Cancel a scheduled job by id.",
3932
+ description: "Cancel a scheduled job by id (in-session or OS-backed).",
3865
3933
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
3866
3934
  async run({ id }) {
3867
- return scheduler.cancel(String(id)) ? `Cancelled ${id}.` : `Error: no scheduled job '${id}'. Use ScheduleList to see jobs.`;
3935
+ const key = String(id);
3936
+ if (key.startsWith("os-")) return os?.cancel(key) ? `Cancelled ${key} (OS job removed).` : `Error: no OS job '${key}'.`;
3937
+ return scheduler.cancel(key) ? `Cancelled ${key}.` : `Error: no scheduled job '${key}'. Use ScheduleList to see jobs.`;
3868
3938
  }
3869
3939
  },
3870
3940
  {
@@ -4162,6 +4232,9 @@ var DuplexAgentOptions = class {
4162
4232
  askRelay = false;
4163
4233
  /** Parked questions auto-resolve empty after this long (callers map '' to deny/best-judgment). */
4164
4234
  askTimeoutMs = 12e4;
4235
+ /** Max retained task records: oldest SETTLED tasks (and their activity tails) are evicted past this,
4236
+ * bounding memory over a long-lived session. Running tasks are never evicted. */
4237
+ maxTaskRecords = 50;
4165
4238
  /** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
4166
4239
  * (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
4167
4240
  quickLook;
@@ -4452,6 +4525,11 @@ ${recent}` : brief) + verify;
4452
4525
  };
4453
4526
  const promise = new Agent(agentOpts).run(briefText).then((res) => this.maybeVerify(id, briefText, res, tier, agentOpts)).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
4454
4527
  this.tasks.set(id, { id, label, status: "running", controller, promise, tail });
4528
+ if (this.tasks.size > this.options.maxTaskRecords)
4529
+ for (const [tid, rec] of this.tasks) {
4530
+ if (this.tasks.size <= this.options.maxTaskRecords) break;
4531
+ if (rec.status !== "running") this.tasks.delete(tid);
4532
+ }
4455
4533
  }
4456
4534
  /** Fresh-context check of a successful Act task: a NEW agent (same model/fs/tools, but NO shared
4457
4535
  * conversation context) re-reads the file state against the brief and fixes any gap. The fix lands
@@ -6052,15 +6130,101 @@ function defaultOpenBrowser(url) {
6052
6130
 
6053
6131
  // cli/core.ts
6054
6132
  import { randomUUID } from "crypto";
6133
+ import { execFile as execFile2 } from "child_process";
6055
6134
  import { resolve, basename, join as join3 } from "path";
6056
- import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
6057
- import { platform, arch, release, userInfo, homedir, tmpdir } from "os";
6135
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
6136
+ import { platform, arch, release, userInfo, homedir as homedir2, tmpdir } from "os";
6058
6137
  init_tools_shell();
6138
+
6139
+ // src/tools.notify.ts
6140
+ init_logging();
6141
+ import { execFile } from "child_process";
6142
+ var log13 = forComponent("notify");
6143
+ function makeNotifyTool(opts = {}) {
6144
+ const platform2 = opts.platform ?? process.platform;
6145
+ const run = opts.exec ?? execFile;
6146
+ return {
6147
+ name: "PushNotification",
6148
+ description: "Show an OS desktop notification to the user (out-of-band alert \u2014 e.g. a long task finished, input is needed). Use sparingly: only for events the user would want to be interrupted for.",
6149
+ parameters: {
6150
+ type: "object",
6151
+ required: ["message"],
6152
+ properties: {
6153
+ message: { type: "string", description: "notification body" },
6154
+ title: { type: "string", description: 'notification title (default "agentx")' }
6155
+ }
6156
+ },
6157
+ async run({ message, title }) {
6158
+ const msg = String(message ?? "").slice(0, 256);
6159
+ const head = String(title ?? "agentx").slice(0, 64);
6160
+ if (!msg) return "Error: empty message";
6161
+ const argv = platform2 === "darwin" ? ["osascript", ["-e", `display notification ${JSON.stringify(msg)} with title ${JSON.stringify(head)}`]] : platform2 === "linux" ? ["notify-send", [head, msg]] : null;
6162
+ if (!argv) return `Notifications unavailable on ${platform2}.`;
6163
+ return new Promise((resolve4) => {
6164
+ run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
6165
+ if (e) {
6166
+ log13.debug("notification failed", e);
6167
+ resolve4(`Notification failed: ${e.message}`);
6168
+ } else resolve4("Notification shown.");
6169
+ });
6170
+ });
6171
+ }
6172
+ };
6173
+ }
6174
+
6175
+ // cli/core.ts
6059
6176
  import { BodDB as BodDB2 } from "@bod.ee/db";
6177
+
6178
+ // cli/util.ts
6179
+ init_logging();
6180
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
6181
+ import { homedir } from "os";
6182
+ var log14 = forComponent("cli-util");
6183
+ function dotDirs(base, sub, opts = {}) {
6184
+ const home = opts.home ?? homedir();
6185
+ const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
6186
+ return opts.existing ? dirs.filter((d) => existsSync2(d)) : dirs;
6187
+ }
6188
+ function truncate(s, n, suffix = "\u2026") {
6189
+ const t = s ?? "";
6190
+ return t.length > n ? t.slice(0, n) + suffix : t;
6191
+ }
6192
+ function sanitizeLabel(s, max = 60) {
6193
+ return truncate(s.replace(/\s+/g, " ").trim(), max, "");
6194
+ }
6195
+ function parseJson(text, fallback, what = "json") {
6196
+ try {
6197
+ return JSON.parse(text);
6198
+ } catch (e) {
6199
+ log14.debug(`parseJson(${what}) failed: ${e.message}`);
6200
+ return fallback;
6201
+ }
6202
+ }
6203
+ function readJsonFile(path, fallback) {
6204
+ if (!existsSync2(path)) return fallback;
6205
+ let text;
6206
+ try {
6207
+ text = readFileSync2(path, "utf8");
6208
+ } catch (e) {
6209
+ log14.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
6210
+ return fallback;
6211
+ }
6212
+ return parseJson(text, fallback, path);
6213
+ }
6214
+
6215
+ // cli/core.ts
6060
6216
  var DEFAULT_TOOLS = ["bash", "Read", "Edit", "Write", "Grep", "Glob", "MultiEdit", "ApplyEdits", "RepoMap", "TodoWrite"];
6061
6217
  function autoWebTools() {
6062
6218
  return ["WebFetch", "WebSearch"];
6063
6219
  }
6220
+ function pdfTextViaPoppler(path) {
6221
+ return new Promise((res, rej) => {
6222
+ execFile2("pdftotext", [path, "-"], { maxBuffer: 32 * 1024 * 1024, timeout: 3e4 }, (e, stdout) => {
6223
+ if (e) rej(new Error(/ENOENT/.test(String(e.code ?? e.message)) ? "pdftotext not installed (brew/apt install poppler)" : e.message));
6224
+ else res(stdout);
6225
+ });
6226
+ });
6227
+ }
6064
6228
  var SANDBOX_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"]);
6065
6229
  async function hydrate(from, to, dir = "/") {
6066
6230
  let n = 0;
@@ -6155,33 +6319,27 @@ async function buildAgent(o) {
6155
6319
  ${notes.join("\n")}
6156
6320
  Reference files in them by their mount path (the left side).`;
6157
6321
  }
6158
- const dot = (sub) => existsSync2(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
6322
+ const dot = (sub) => existsSync3(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
6159
6323
  const dots = (sub) => {
6160
- const home = homedir();
6161
- const dirs = [
6162
- dot(sub),
6163
- existsSync2(`${cwd}/.claude/${sub}`) ? `${cwd}/.claude/${sub}` : void 0,
6164
- existsSync2(`${home}/.agent/${sub}`) ? `${home}/.agent/${sub}` : void 0,
6165
- existsSync2(`${home}/.claude/${sub}`) ? `${home}/.claude/${sub}` : void 0
6166
- ].filter(Boolean);
6324
+ const dirs = dotDirs(cwd, sub, { existing: true });
6167
6325
  return dirs.length ? dirs : void 0;
6168
6326
  };
6169
6327
  const memoryDir = (() => {
6170
- const home = homedir();
6328
+ const home = homedir2();
6171
6329
  const projectDir = `${cwd}/.agent/memory`;
6172
6330
  const readDirs = [
6173
- existsSync2(projectDir) ? projectDir : void 0,
6174
- existsSync2(`${cwd}/.claude/memory`) ? `${cwd}/.claude/memory` : void 0,
6175
- existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6176
- existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6331
+ existsSync3(projectDir) ? projectDir : void 0,
6332
+ existsSync3(`${cwd}/.claude/memory`) ? `${cwd}/.claude/memory` : void 0,
6333
+ existsSync3(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6334
+ existsSync3(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6177
6335
  ].filter(Boolean);
6178
6336
  return readDirs[0] === projectDir ? readDirs : [projectDir, ...readDirs];
6179
6337
  })();
6180
6338
  const memoryUserDir = (() => {
6181
- const home = homedir();
6339
+ const home = homedir2();
6182
6340
  return [
6183
- existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6184
- existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6341
+ existsSync3(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6342
+ existsSync3(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6185
6343
  ].find(Boolean) ?? `${home}/.agent/memory`;
6186
6344
  })();
6187
6345
  const memoryWriteDir = memoryDir[0];
@@ -6198,6 +6356,8 @@ Reference files in them by their mount path (the left side).`;
6198
6356
  ai: o.ai,
6199
6357
  fs,
6200
6358
  model: o.model ?? "anthropic/claude-sonnet-4-6",
6359
+ // PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
6360
+ ...!virtual ? { pdfText: pdfTextViaPoppler } : {},
6201
6361
  // Anchor cursor to the launch dir (its adapter defaults to TMPDIR otherwise) and forward the
6202
6362
  // host's MCP servers so the delegated cursor agent runs in the same environment. Gated to cursor:
6203
6363
  // openai/google adapters Object.assign providerOptions into the request body, so a blanket cwd
@@ -6240,6 +6400,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
6240
6400
  const base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
6241
6401
  const tail = [...o.extraTools ?? []];
6242
6402
  if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "anthropic/claude-sonnet-4-6", dir: scratchDir }));
6403
+ tail.push(makeNotifyTool());
6243
6404
  if (!realShell.length) return [...base, ...tail];
6244
6405
  const filtered = base.filter((t) => t.name !== "bash");
6245
6406
  return [...filtered, ...realShell, ...tail];
@@ -6291,11 +6452,11 @@ var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0
6291
6452
  // cli/voice.ts
6292
6453
  init_logging();
6293
6454
  import { spawn as spawn2, spawnSync } from "child_process";
6294
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
6295
- import { homedir as homedir2 } from "os";
6455
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
6456
+ import { homedir as homedir3 } from "os";
6296
6457
  import { dirname as dirname3, join as join4 } from "path";
6297
6458
  import { fileURLToPath } from "url";
6298
- var log13 = forComponent("VoiceIO");
6459
+ var log15 = forComponent("VoiceIO");
6299
6460
  var now4 = () => performance.now();
6300
6461
  var Player = class {
6301
6462
  proc = null;
@@ -6309,7 +6470,7 @@ var Player = class {
6309
6470
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
6310
6471
  { stdio: ["pipe", "ignore", "ignore"] }
6311
6472
  );
6312
- this.proc.on("error", (e) => log13.warn(`ffplay error: ${e.message}`));
6473
+ this.proc.on("error", (e) => log15.warn(`ffplay error: ${e.message}`));
6313
6474
  this.proc.stdin.on("error", () => {
6314
6475
  });
6315
6476
  this.bytesWritten = 0;
@@ -6344,28 +6505,28 @@ function detectFfmpegMic() {
6344
6505
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
6345
6506
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
6346
6507
  if (!mic) throw new Error("no audio input device found");
6347
- log13.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
6508
+ log15.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
6348
6509
  return `:${mic.idx}`;
6349
6510
  }
6350
6511
  function resolveAecBinary() {
6351
6512
  if (process.env.MIC_AEC === "0" || process.platform !== "darwin") return null;
6352
6513
  const src = join4(nativeDir(), "mic-aec.swift");
6353
6514
  const plist = join4(nativeDir(), "Info.plist");
6354
- if (!existsSync3(src)) return null;
6355
- const cacheDir = join4(homedir2(), ".agent", "cache");
6515
+ if (!existsSync4(src)) return null;
6516
+ const cacheDir = join4(homedir3(), ".agent", "cache");
6356
6517
  const bin = join4(cacheDir, "mic-aec");
6357
- if (existsSync3(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
6518
+ if (existsSync4(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
6358
6519
  if (spawnSync("which", ["swiftc"]).status !== 0) return null;
6359
6520
  mkdirSync3(cacheDir, { recursive: true });
6360
- log13.info("compiling AEC mic helper (first run)\u2026");
6521
+ log15.info("compiling AEC mic helper (first run)\u2026");
6361
6522
  const build = spawnSync("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
6362
6523
  if (build.status !== 0) {
6363
- log13.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
6524
+ log15.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
6364
6525
  return null;
6365
6526
  }
6366
6527
  const sign = spawnSync("codesign", ["-fs", "-", bin], { encoding: "utf8" });
6367
6528
  if (sign.status !== 0) {
6368
- log13.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
6529
+ log15.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
6369
6530
  return null;
6370
6531
  }
6371
6532
  return bin;
@@ -6384,16 +6545,16 @@ var NodeMicSource = class {
6384
6545
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
6385
6546
  } else {
6386
6547
  if (spawnSync("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
6387
- log13.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
6548
+ log15.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
6388
6549
  this.proc = spawn2(
6389
6550
  "ffmpeg",
6390
6551
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
6391
6552
  { stdio: ["ignore", "pipe", "pipe"] }
6392
6553
  );
6393
- this.proc.stderr.on("data", (d) => log13.warn(`ffmpeg: ${String(d).trim()}`));
6554
+ this.proc.stderr.on("data", (d) => log15.warn(`ffmpeg: ${String(d).trim()}`));
6394
6555
  }
6395
6556
  this.proc.on("exit", (c) => {
6396
- if (c && !this.stopped) log13.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
6557
+ if (c && !this.stopped) log15.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
6397
6558
  });
6398
6559
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
6399
6560
  }
@@ -6427,11 +6588,11 @@ var AecDuplexAudio = class {
6427
6588
  this.proc.stdin.on("error", () => {
6428
6589
  });
6429
6590
  this.proc.on("exit", (c) => {
6430
- if (c && !this.stopped) log13.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
6591
+ if (c && !this.stopped) log15.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
6431
6592
  });
6432
6593
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
6433
6594
  this.proc.stderr.on("data", (d) => {
6434
- for (const ln of String(d).split("\n")) if (ln.trim()) log13.debug(`mic-aec: ${ln.trim()}`);
6595
+ for (const ln of String(d).split("\n")) if (ln.trim()) log15.debug(`mic-aec: ${ln.trim()}`);
6435
6596
  });
6436
6597
  }
6437
6598
  stop() {
@@ -6547,15 +6708,15 @@ var VoiceIO = class extends VoiceEngine {
6547
6708
  };
6548
6709
 
6549
6710
  // cli/config.ts
6550
- import { homedir as homedir3 } from "os";
6551
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
6711
+ import { homedir as homedir4 } from "os";
6712
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
6552
6713
  import { join as join5 } from "path";
6553
6714
  import { pathToFileURL } from "url";
6554
6715
  var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
6555
6716
  async function loadFrom(dir) {
6556
6717
  for (const f of FILES) {
6557
6718
  const p = join5(dir, ".agent", f);
6558
- if (!existsSync4(p)) continue;
6719
+ if (!existsSync5(p)) continue;
6559
6720
  try {
6560
6721
  const mod = await import(pathToFileURL(p).href, f.endsWith(".json") ? { with: { type: "json" } } : void 0);
6561
6722
  return mod.default ?? mod.config ?? mod;
@@ -6568,9 +6729,9 @@ async function loadFrom(dir) {
6568
6729
  }
6569
6730
  function loadSettings(dir) {
6570
6731
  const p = join5(dir, ".agent", "settings.json");
6571
- if (!existsSync4(p)) return {};
6732
+ if (!existsSync5(p)) return {};
6572
6733
  try {
6573
- const raw = JSON.parse(readFileSync2(p, "utf8"));
6734
+ const raw = JSON.parse(readFileSync3(p, "utf8"));
6574
6735
  const cfg = {};
6575
6736
  if (raw.mcpServers && typeof raw.mcpServers === "object") cfg.mcpServers = raw.mcpServers;
6576
6737
  if (raw.permissions && typeof raw.permissions === "object") cfg.permissions = raw.permissions;
@@ -6591,8 +6752,8 @@ function loadSettings(dir) {
6591
6752
  }
6592
6753
  }
6593
6754
  async function loadConfig(cwd) {
6594
- const userSettings = loadSettings(homedir3());
6595
- const user = await loadFrom(homedir3());
6755
+ const userSettings = loadSettings(homedir4());
6756
+ const user = await loadFrom(homedir4());
6596
6757
  const projectSettings = loadSettings(cwd);
6597
6758
  const project = await loadFrom(cwd);
6598
6759
  const merged = { ...userSettings, ...user, ...projectSettings, ...project };
@@ -6604,7 +6765,7 @@ async function loadConfig(cwd) {
6604
6765
 
6605
6766
  // cli/hooks-config.ts
6606
6767
  import { spawnSync as spawnSync2 } from "child_process";
6607
- var log14 = forComponent("hooks");
6768
+ var log16 = forComponent("hooks");
6608
6769
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6609
6770
  function ruleMatches(rule, toolName) {
6610
6771
  if (!rule.tool || rule.tool === "*") return true;
@@ -6621,7 +6782,7 @@ function runCmd(rule, env) {
6621
6782
  });
6622
6783
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
6623
6784
  } catch (e) {
6624
- log14.debug(`hook command failed: ${rule.command}`, e);
6785
+ log16.debug(`hook command failed: ${rule.command}`, e);
6625
6786
  return { code: 1, out: String(e?.message ?? e) };
6626
6787
  }
6627
6788
  }
@@ -6725,11 +6886,11 @@ function formatDiff(ops, opts = {}) {
6725
6886
  }
6726
6887
 
6727
6888
  // cli/session.ts
6728
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync, symlinkSync, unlinkSync, readlinkSync } from "fs";
6729
- import { homedir as homedir4 } from "os";
6889
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync, renameSync, symlinkSync, unlinkSync, readlinkSync } from "fs";
6890
+ import { homedir as homedir5 } from "os";
6730
6891
  import { join as join6 } from "path";
6731
- var log15 = forComponent("session");
6732
- var globalDir = () => join6(homedir4(), ".agent", "sessions");
6892
+ var log17 = forComponent("session");
6893
+ var globalDir = () => join6(homedir5(), ".agent", "sessions");
6733
6894
  var SessionStore = class {
6734
6895
  dir;
6735
6896
  constructor(cwd) {
@@ -6741,10 +6902,10 @@ var SessionStore = class {
6741
6902
  const p = (n, w = 2) => String(n).padStart(w, "0");
6742
6903
  const slug2 = (cwd ?? process.cwd()).split("/").pop()?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
6743
6904
  let id = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
6744
- if (existsSync5(this.dir) && existsSync5(join6(this.dir, `${id}.json`))) {
6905
+ if (existsSync6(this.dir) && existsSync6(join6(this.dir, `${id}.json`))) {
6745
6906
  for (let i = 2; i <= 99; i++) {
6746
6907
  const c = `${id}-${i}`;
6747
- if (!existsSync5(join6(this.dir, `${c}.json`))) {
6908
+ if (!existsSync6(join6(this.dir, `${c}.json`))) {
6748
6909
  id = c;
6749
6910
  break;
6750
6911
  }
@@ -6758,43 +6919,43 @@ var SessionStore = class {
6758
6919
  }
6759
6920
  save(data) {
6760
6921
  if (!this.safeId(data.meta.id)) throw new Error(`unsafe session id: ${data.meta.id}`);
6761
- if (!existsSync5(this.dir)) mkdirSync4(this.dir, { recursive: true });
6922
+ if (!existsSync6(this.dir)) mkdirSync4(this.dir, { recursive: true });
6762
6923
  const path = join6(this.dir, `${data.meta.id}.json`);
6763
6924
  const tmp = `${path}.${process.pid}.tmp`;
6764
6925
  writeFileSync3(tmp, JSON.stringify(data));
6765
6926
  renameSync(tmp, path);
6766
6927
  try {
6767
6928
  const gd = globalDir();
6768
- if (!existsSync5(gd)) mkdirSync4(gd, { recursive: true });
6929
+ if (!existsSync6(gd)) mkdirSync4(gd, { recursive: true });
6769
6930
  const link2 = join6(gd, `${data.meta.id}.json`);
6770
- if (!existsSync5(link2)) symlinkSync(path, link2);
6931
+ if (!existsSync6(link2)) symlinkSync(path, link2);
6771
6932
  } catch {
6772
6933
  }
6773
6934
  }
6774
6935
  load(id) {
6775
6936
  if (!this.safeId(id)) {
6776
- log15.debug(`rejecting unsafe session id: ${id}`);
6937
+ log17.debug(`rejecting unsafe session id: ${id}`);
6777
6938
  return void 0;
6778
6939
  }
6779
6940
  const path = join6(this.dir, `${id}.json`);
6780
- if (!existsSync5(path)) return void 0;
6941
+ if (!existsSync6(path)) return void 0;
6781
6942
  try {
6782
- return JSON.parse(readFileSync3(path, "utf8"));
6943
+ return JSON.parse(readFileSync4(path, "utf8"));
6783
6944
  } catch (e) {
6784
- log15.debug(`unreadable session ${id} \u2014 ignoring`, e);
6945
+ log17.debug(`unreadable session ${id} \u2014 ignoring`, e);
6785
6946
  return void 0;
6786
6947
  }
6787
6948
  }
6788
6949
  /** All sessions' metadata, most-recently-updated first. */
6789
6950
  list() {
6790
- if (!existsSync5(this.dir)) return [];
6951
+ if (!existsSync6(this.dir)) return [];
6791
6952
  const metas = [];
6792
6953
  for (const f of readdirSync(this.dir)) {
6793
6954
  if (!f.endsWith(".json")) continue;
6794
6955
  try {
6795
- metas.push(JSON.parse(readFileSync3(join6(this.dir, f), "utf8")).meta);
6956
+ metas.push(JSON.parse(readFileSync4(join6(this.dir, f), "utf8")).meta);
6796
6957
  } catch (e) {
6797
- log15.debug(`skipping unreadable session file ${f}`, e);
6958
+ log17.debug(`skipping unreadable session file ${f}`, e);
6798
6959
  }
6799
6960
  }
6800
6961
  return metas.sort((a, b) => b.updated - a.updated);
@@ -6810,12 +6971,12 @@ var SessionStore = class {
6810
6971
  };
6811
6972
  function globalSessionLoad(idOrPrefix) {
6812
6973
  const gd = globalDir();
6813
- if (!existsSync5(gd)) return void 0;
6974
+ if (!existsSync6(gd)) return void 0;
6814
6975
  const exact = join6(gd, `${idOrPrefix}.json`);
6815
- if (existsSync5(exact)) {
6976
+ if (existsSync6(exact)) {
6816
6977
  try {
6817
6978
  const target = readlinkSync(exact);
6818
- return JSON.parse(readFileSync3(target, "utf8"));
6979
+ return JSON.parse(readFileSync4(target, "utf8"));
6819
6980
  } catch {
6820
6981
  return void 0;
6821
6982
  }
@@ -6826,7 +6987,7 @@ function globalSessionLoad(idOrPrefix) {
6826
6987
  const base = f.slice(0, -5);
6827
6988
  if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
6828
6989
  const target = readlinkSync(join6(gd, f));
6829
- return JSON.parse(readFileSync3(target, "utf8"));
6990
+ return JSON.parse(readFileSync4(target, "utf8"));
6830
6991
  }
6831
6992
  }
6832
6993
  } catch {
@@ -6835,20 +6996,20 @@ function globalSessionLoad(idOrPrefix) {
6835
6996
  }
6836
6997
  function globalSessionList() {
6837
6998
  const gd = globalDir();
6838
- if (!existsSync5(gd)) return [];
6999
+ if (!existsSync6(gd)) return [];
6839
7000
  const metas = [];
6840
7001
  for (const f of readdirSync(gd)) {
6841
7002
  if (!f.endsWith(".json")) continue;
6842
7003
  try {
6843
7004
  const target = readlinkSync(join6(gd, f));
6844
- if (!existsSync5(target)) {
7005
+ if (!existsSync6(target)) {
6845
7006
  try {
6846
7007
  unlinkSync(join6(gd, f));
6847
7008
  } catch {
6848
7009
  }
6849
7010
  continue;
6850
7011
  }
6851
- metas.push(JSON.parse(readFileSync3(target, "utf8")).meta);
7012
+ metas.push(JSON.parse(readFileSync4(target, "utf8")).meta);
6852
7013
  } catch {
6853
7014
  }
6854
7015
  }
@@ -6873,7 +7034,7 @@ var CheckpointStack = class {
6873
7034
  current;
6874
7035
  /** Open a new turn frame (call right before sending a user turn). */
6875
7036
  begin(label) {
6876
- this.current = { label: label.replace(/\s+/g, " ").trim().slice(0, 60) || "(turn)", at: Date.now(), saved: /* @__PURE__ */ new Map() };
7037
+ this.current = { label: sanitizeLabel(label) || "(turn)", at: Date.now(), saved: /* @__PURE__ */ new Map() };
6877
7038
  this.frames.push(this.current);
6878
7039
  if (this.frames.length > this.max) this.frames.shift();
6879
7040
  }
@@ -6901,6 +7062,24 @@ var CheckpointStack = class {
6901
7062
  list() {
6902
7063
  return this.frames.map((f, i) => ({ index: i, label: f.label, at: f.at, files: f.saved.size })).reverse();
6903
7064
  }
7065
+ /** Unified-style diff of all session edits: each file's OLDEST saved content vs its current content. */
7066
+ async diff() {
7067
+ const base = /* @__PURE__ */ new Map();
7068
+ for (const f of this.frames) for (const [path, prior] of f.saved) if (!base.has(path)) base.set(path, prior);
7069
+ const parts = [];
7070
+ for (const [path, prior] of base) {
7071
+ let now5 = null;
7072
+ try {
7073
+ now5 = await this.fs.readFile(path);
7074
+ } catch {
7075
+ }
7076
+ if ((prior ?? "") === (now5 ?? "")) continue;
7077
+ const ops = diffLines(prior ?? "", now5 ?? "");
7078
+ parts.push(`--- ${path}${prior == null ? " (new)" : now5 == null ? " (deleted)" : ""}
7079
+ ${formatDiff(ops)}`);
7080
+ }
7081
+ return parts.join("\n");
7082
+ }
6904
7083
  /**
6905
7084
  * Restore the working tree to BEFORE frame `index` — undo that frame and every later one.
6906
7085
  * Frames are replayed newest→oldest so the OLDEST saved content for a path wins (its true
@@ -6930,12 +7109,12 @@ var CheckpointStack = class {
6930
7109
  };
6931
7110
 
6932
7111
  // cli/gitCheckpoints.ts
6933
- import { execFile } from "child_process";
7112
+ import { execFile as execFile3 } from "child_process";
6934
7113
  import { promisify } from "util";
6935
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
7114
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
6936
7115
  import { join as join7, resolve as resolve2, sep as sep2 } from "path";
6937
- var log16 = forComponent("checkpoints");
6938
- var exec = promisify(execFile);
7116
+ var log18 = forComponent("checkpoints");
7117
+ var exec = promisify(execFile3);
6939
7118
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
6940
7119
  var ShadowRepo = class {
6941
7120
  // undefined = unprobed; false = git/this root unusable
@@ -6961,14 +7140,14 @@ var ShadowRepo = class {
6961
7140
  if (this.ready !== void 0) return this.ready;
6962
7141
  try {
6963
7142
  await exec(this.git, ["--version"]);
6964
- if (!existsSync6(this.gitDir)) {
7143
+ if (!existsSync7(this.gitDir)) {
6965
7144
  mkdirSync5(this.gitDir, { recursive: true });
6966
7145
  await this.run("init", "-q");
6967
7146
  }
6968
7147
  writeFileSync4(join7(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
6969
7148
  this.ready = true;
6970
7149
  } catch (e) {
6971
- log16.debug(`git checkpoints unavailable for ${this.workTree}`, e);
7150
+ log18.debug(`git checkpoints unavailable for ${this.workTree}`, e);
6972
7151
  this.ready = false;
6973
7152
  }
6974
7153
  return this.ready;
@@ -7001,6 +7180,14 @@ var ShadowRepo = class {
7001
7180
  return 0;
7002
7181
  }
7003
7182
  }
7183
+ /** Unified diff between `sha` and the current work-tree (for `/diff`). */
7184
+ async diffSince(sha) {
7185
+ try {
7186
+ return await this.run("diff", sha);
7187
+ } catch {
7188
+ return "";
7189
+ }
7190
+ }
7004
7191
  /** Restore the tree to `sha`; returns counts of reverted tracked files + removed untracked-new. */
7005
7192
  async resetTo(sha) {
7006
7193
  let restored = 0, deleted = 0;
@@ -7031,7 +7218,7 @@ var ShadowRepo = class {
7031
7218
  await this.run("gc", "--auto", "-q").catch(() => {
7032
7219
  });
7033
7220
  } catch (e) {
7034
- log16.debug("checkpoint prune failed", e);
7221
+ log18.debug("checkpoint prune failed", e);
7035
7222
  }
7036
7223
  }
7037
7224
  };
@@ -7088,18 +7275,18 @@ var GitCheckpoints = class {
7088
7275
  use(sessionId) {
7089
7276
  if (sessionId === this.session) return;
7090
7277
  this.session = sessionId;
7091
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log16.debug("re-point failed", e));
7278
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log18.debug("re-point failed", e));
7092
7279
  }
7093
7280
  async begin(label) {
7094
7281
  if (!await this.start()) return;
7095
- const msg = label.replace(/\s+/g, " ").trim().slice(0, 72) || "(turn)";
7282
+ const msg = sanitizeLabel(label, 72) || "(turn)";
7096
7283
  let slow;
7097
7284
  if (!this.snapshotted) slow = setTimeout(() => process.stderr.write("\x1B[2m checkpointing initial workspace snapshot\u2026\x1B[0m\n"), 1500);
7098
7285
  for (const r of this.repos) {
7099
7286
  try {
7100
7287
  await r.commit(msg);
7101
7288
  } catch (e) {
7102
- log16.debug("checkpoint commit failed", e);
7289
+ log18.debug("checkpoint commit failed", e);
7103
7290
  }
7104
7291
  }
7105
7292
  if (slow) clearTimeout(slow);
@@ -7123,6 +7310,16 @@ var GitCheckpoints = class {
7123
7310
  get size() {
7124
7311
  return this.caches[0]?.length ?? 0;
7125
7312
  }
7313
+ /** Unified diff of everything this session changed: oldest session checkpoint → current tree, across all roots. */
7314
+ async diff() {
7315
+ if (!this.caches[0]?.length) await this.refresh();
7316
+ const parts = [];
7317
+ for (let i = 0; i < this.repos.length; i++) {
7318
+ const base = this.caches[i]?.[0];
7319
+ if (base) parts.push(await this.repos[i].diffSince(base.sha));
7320
+ }
7321
+ return parts.filter(Boolean).join("\n");
7322
+ }
7126
7323
  async rewindTo(index) {
7127
7324
  if (!this.caches[0]?.length) await this.refresh();
7128
7325
  if (index < 0 || index >= (this.caches[0]?.length ?? 0)) throw new Error("no such checkpoint");
@@ -7156,8 +7353,8 @@ var GitCheckpointsOptions = class {
7156
7353
  };
7157
7354
 
7158
7355
  // cli/permissions.ts
7159
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
7160
- import { homedir as homedir5 } from "os";
7356
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
7357
+ import { homedir as homedir6 } from "os";
7161
7358
  import { join as join8 } from "path";
7162
7359
  var RULE_RE = /^(\w+)(?:\((.+)\))?$/;
7163
7360
  function parseOne(raw, decision) {
@@ -7182,25 +7379,15 @@ function describeRule(r) {
7182
7379
  }
7183
7380
  var PERM_FILE = (cwd) => join8(cwd, ".agent", "permissions.json");
7184
7381
  function loadPersistedRules(cwd) {
7185
- const p = PERM_FILE(cwd);
7186
- if (!existsSync7(p)) return {};
7187
- try {
7188
- const j = JSON.parse(readFileSync4(p, "utf8"));
7189
- return { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] };
7190
- } catch {
7191
- return {};
7192
- }
7382
+ const j = readJsonFile(PERM_FILE(cwd), null);
7383
+ return j ? { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] } : {};
7193
7384
  }
7194
- function loadClaudeSettings(cwd, home = homedir5()) {
7385
+ function loadClaudeSettings(cwd, home = homedir6()) {
7195
7386
  const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
7196
7387
  let out = {};
7197
7388
  for (const p of files) {
7198
- if (!existsSync7(p)) continue;
7199
- try {
7200
- const perms = JSON.parse(readFileSync4(p, "utf8"))?.permissions;
7201
- if (perms) out = mergePerms(out, { allow: perms.allow, ask: perms.ask, deny: perms.deny }) ?? out;
7202
- } catch {
7203
- }
7389
+ const perms = readJsonFile(p, null)?.permissions;
7390
+ if (perms) out = mergePerms(out, { allow: perms.allow, ask: perms.ask, deny: perms.deny }) ?? out;
7204
7391
  }
7205
7392
  return out;
7206
7393
  }
@@ -7223,20 +7410,14 @@ function mergePerms(a, b) {
7223
7410
  }
7224
7411
  return Object.keys(out).length ? out : void 0;
7225
7412
  }
7226
- var TRUST_FILE = join8(homedir5(), ".agent", "trusted.json");
7413
+ var TRUST_FILE = join8(homedir6(), ".agent", "trusted.json");
7227
7414
  function isTrusted(cwd, file = TRUST_FILE) {
7228
- try {
7229
- return existsSync7(file) && JSON.parse(readFileSync4(file, "utf8")).includes(cwd);
7230
- } catch {
7231
- return false;
7232
- }
7415
+ const list = readJsonFile(file, []);
7416
+ return Array.isArray(list) && list.includes(cwd);
7233
7417
  }
7234
7418
  function trustDir(cwd, file = TRUST_FILE) {
7235
- let list = [];
7236
- try {
7237
- if (existsSync7(file)) list = JSON.parse(readFileSync4(file, "utf8"));
7238
- } catch {
7239
- }
7419
+ let list = readJsonFile(file, []);
7420
+ if (!Array.isArray(list)) list = [];
7240
7421
  if (!list.includes(cwd)) list.push(cwd);
7241
7422
  try {
7242
7423
  mkdirSync6(join8(file, ".."), { recursive: true });
@@ -7291,14 +7472,18 @@ function completePath(listDir, ref) {
7291
7472
  for (const e of entries) {
7292
7473
  if (!fuzzy(base, e.name)) continue;
7293
7474
  if (e.name.startsWith(".") && !base.startsWith(".")) continue;
7294
- const rel = dir ? `${dir}/${e.name}` : e.name;
7295
- matched.push("@" + rel + (e.dir ? "/" : ""));
7475
+ const rel = (dir ? `${dir}/${e.name}` : e.name) + (e.dir ? "/" : "");
7476
+ matched.push(/\s/.test(rel) ? `@"${rel}"` : "@" + rel);
7296
7477
  }
7297
7478
  return rank(matched, "@" + (dir ? dir + "/" : "") + base);
7298
7479
  }
7299
7480
 
7300
7481
  // cli/lineEditor.ts
7301
7482
  import { emitKeypressEvents } from "readline";
7483
+ import { spawnSync as spawnSync3 } from "child_process";
7484
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
7485
+ import { tmpdir as tmpdir2 } from "os";
7486
+ import { join as join9 } from "path";
7302
7487
 
7303
7488
  // cli/bidi.ts
7304
7489
  var RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
@@ -7429,8 +7614,61 @@ function applyBidi(buf, cursor) {
7429
7614
  return { text: out.join("\n"), cursor: visCursor };
7430
7615
  }
7431
7616
 
7617
+ // cli/stdinSource.ts
7618
+ import { PassThrough } from "stream";
7619
+ var cancelPump;
7620
+ var keyInput = (() => {
7621
+ const bun = globalThis.Bun;
7622
+ if (!bun || !process.stdin.isTTY) return process.stdin;
7623
+ const pt = new PassThrough();
7624
+ pt.isTTY = true;
7625
+ pt.setRawMode = (mode) => {
7626
+ process.stdin.setRawMode(mode);
7627
+ return pt;
7628
+ };
7629
+ Object.defineProperty(pt, "isRaw", { get: () => process.stdin.isRaw });
7630
+ let pump = () => {
7631
+ pump = void 0;
7632
+ const reader = bun.stdin.stream().getReader();
7633
+ let stopped = false;
7634
+ cancelPump = () => {
7635
+ stopped = true;
7636
+ void reader.cancel().catch(() => {
7637
+ });
7638
+ };
7639
+ void (async () => {
7640
+ try {
7641
+ while (!stopped) {
7642
+ const { value, done } = await reader.read();
7643
+ if (done) break;
7644
+ if (value?.length) pt.write(Buffer.from(value));
7645
+ }
7646
+ } catch {
7647
+ }
7648
+ pt.end();
7649
+ })();
7650
+ };
7651
+ const ptResume = pt.resume.bind(pt);
7652
+ pt.resume = () => {
7653
+ pump?.();
7654
+ return ptResume();
7655
+ };
7656
+ return pt;
7657
+ })();
7658
+ function releaseKeyInput() {
7659
+ cancelPump?.();
7660
+ }
7661
+
7432
7662
  // cli/lineEditor.ts
7433
- var visibleWidth = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
7663
+ var cpWidth = (cp) => {
7664
+ if (cp === 8205 || cp === 65039 || cp >= 768 && cp <= 879) return 0;
7665
+ return cp >= 4352 && (cp <= 4447 || cp === 9001 || cp === 9002 || cp >= 11904 && cp <= 42191 && cp !== 12351 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 126976 && cp <= 129791 || cp >= 131072 && cp <= 262141) ? 2 : 1;
7666
+ };
7667
+ var visibleWidth = (s) => {
7668
+ let w = 0;
7669
+ for (const ch of s.replace(/\x1b\[[0-9;]*m/g, "")) w += cpWidth(ch.codePointAt(0));
7670
+ return w;
7671
+ };
7434
7672
  var isPrintable = (str) => !!str && !/[\x00-\x1f\x7f]/.test(str);
7435
7673
  var EditorState = class _EditorState {
7436
7674
  // pending operator awaiting a motion (dd/dw/cc)
@@ -7564,6 +7802,17 @@ var EditorState = class _EditorState {
7564
7802
  this.histIdx = -1;
7565
7803
  this.refresh();
7566
7804
  }
7805
+ // Code-point boundaries: cursor/delete ops must never split a surrogate pair (emoji = 2 code units;
7806
+ // a half-pair in the buffer reaches the model as mojibake). Steps are by code POINT (lean — ZWJ
7807
+ // families delete component-wise, which even many terminals do).
7808
+ prevCp(i) {
7809
+ if (i >= 2 && /[\uDC00-\uDFFF]/.test(this.buf[i - 1]) && /[\uD800-\uDBFF]/.test(this.buf[i - 2])) return i - 2;
7810
+ return Math.max(0, i - 1);
7811
+ }
7812
+ nextCp(i) {
7813
+ if (i <= this.buf.length - 2 && /[\uD800-\uDBFF]/.test(this.buf[i]) && /[\uDC00-\uDFFF]/.test(this.buf[i + 1])) return i + 2;
7814
+ return Math.min(this.buf.length, i + 1);
7815
+ }
7567
7816
  insert(s) {
7568
7817
  this.snapshot();
7569
7818
  this.buf = this.buf.slice(0, this.cursor) + s + this.buf.slice(this.cursor);
@@ -7574,22 +7823,23 @@ var EditorState = class _EditorState {
7574
7823
  backspace() {
7575
7824
  if (this.cursor === 0) return;
7576
7825
  this.snapshot();
7577
- this.buf = this.buf.slice(0, this.cursor - 1) + this.buf.slice(this.cursor);
7578
- this.cursor--;
7826
+ const start = this.prevCp(this.cursor);
7827
+ this.buf = this.buf.slice(0, start) + this.buf.slice(this.cursor);
7828
+ this.cursor = start;
7579
7829
  this.histIdx = -1;
7580
7830
  this.refresh();
7581
7831
  }
7582
7832
  del() {
7583
7833
  if (this.cursor >= this.buf.length) return;
7584
7834
  this.snapshot();
7585
- this.buf = this.buf.slice(0, this.cursor) + this.buf.slice(this.cursor + 1);
7835
+ this.buf = this.buf.slice(0, this.cursor) + this.buf.slice(this.nextCp(this.cursor));
7586
7836
  this.refresh();
7587
7837
  }
7588
7838
  left() {
7589
- if (this.cursor > 0) this.cursor--;
7839
+ if (this.cursor > 0) this.cursor = this.prevCp(this.cursor);
7590
7840
  }
7591
7841
  right() {
7592
- if (this.cursor < this.buf.length) this.cursor++;
7842
+ if (this.cursor < this.buf.length) this.cursor = this.nextCp(this.cursor);
7593
7843
  }
7594
7844
  home() {
7595
7845
  this.cursor = 0;
@@ -8073,6 +8323,30 @@ function createLineEditor(out) {
8073
8323
  if (cursorCol > 0) out.write(`\x1B[${cursorCol}C`);
8074
8324
  curRow = cursorRow;
8075
8325
  }
8326
+ function externalEdit(s) {
8327
+ const spec = process.env.VISUAL || process.env.EDITOR || "vi";
8328
+ const [cmd, ...cargs] = spec.split(" ").filter(Boolean);
8329
+ const file = join9(tmpdir2(), `agentx-edit-${process.pid}-${Date.now()}.md`);
8330
+ try {
8331
+ writeFileSync6(file, s.buf);
8332
+ process.stdin.setRawMode(false);
8333
+ out.write("\x1B[?2004l");
8334
+ const r = spawnSync3(cmd, [...cargs, file], { stdio: "inherit" });
8335
+ if (r.status === 0) {
8336
+ const text = readFileSync5(file, "utf8").replace(/\n$/, "");
8337
+ s.reset();
8338
+ if (text) s.insert(text);
8339
+ }
8340
+ } catch {
8341
+ } finally {
8342
+ try {
8343
+ unlinkSync2(file);
8344
+ } catch {
8345
+ }
8346
+ process.stdin.setRawMode(true);
8347
+ out.write("\x1B[?2004h");
8348
+ }
8349
+ }
8076
8350
  async function readLine(opts) {
8077
8351
  const maxVisible = opts.maxVisible ?? 8;
8078
8352
  if (!isTTY) return readPlainLine();
@@ -8080,9 +8354,9 @@ function createLineEditor(out) {
8080
8354
  curRow = 0;
8081
8355
  if (opts.initial) s.insert(opts.initial);
8082
8356
  s.refresh();
8083
- emitKeypressEvents(process.stdin);
8084
- process.stdin.setRawMode(true);
8085
- process.stdin.resume();
8357
+ emitKeypressEvents(keyInput);
8358
+ keyInput.setRawMode(true);
8359
+ keyInput.resume();
8086
8360
  out.write("\x1B[?2004h");
8087
8361
  const promptOf = () => typeof opts.prompt === "function" ? opts.prompt() : opts.prompt;
8088
8362
  render(s, promptOf(), maxVisible, opts.status);
@@ -8109,6 +8383,7 @@ function createLineEditor(out) {
8109
8383
  lastStatus = cur;
8110
8384
  redraw();
8111
8385
  }, opts.statusTickMs) : void 0;
8386
+ let chordCtrlX = false;
8112
8387
  const onKey = (str, key) => {
8113
8388
  if (key?.ctrl && key.name === "l") {
8114
8389
  out.write("\x1B[2J\x1B[3J\x1B[H");
@@ -8116,6 +8391,19 @@ function createLineEditor(out) {
8116
8391
  redraw();
8117
8392
  return;
8118
8393
  }
8394
+ if (key?.ctrl && key.name === "x" && !s.pasting) {
8395
+ chordCtrlX = true;
8396
+ return;
8397
+ }
8398
+ if (chordCtrlX) {
8399
+ chordCtrlX = false;
8400
+ if (key?.ctrl && key.name === "e") {
8401
+ externalEdit(s);
8402
+ curRow = 0;
8403
+ redraw();
8404
+ return;
8405
+ }
8406
+ }
8119
8407
  if (key?.name === "tab" && key.shift && opts.onCyclePosture) {
8120
8408
  opts.onCyclePosture();
8121
8409
  redraw();
@@ -8152,9 +8440,9 @@ function createLineEditor(out) {
8152
8440
  return;
8153
8441
  }
8154
8442
  if (key?.meta && key.name === "p" && opts.onPickModel) {
8155
- process.stdin.off("keypress", onKey);
8443
+ keyInput.off("keypress", onKey);
8156
8444
  void opts.onPickModel().finally(() => {
8157
- process.stdin.on("keypress", onKey);
8445
+ keyInput.on("keypress", onKey);
8158
8446
  curRow = 0;
8159
8447
  redraw();
8160
8448
  });
@@ -8163,6 +8451,7 @@ function createLineEditor(out) {
8163
8451
  const action = applyKey(s, key ?? {}, str);
8164
8452
  if (action === "submit") {
8165
8453
  finish();
8454
+ opts.onSubmit?.();
8166
8455
  return resolve4(s.expand());
8167
8456
  }
8168
8457
  if (action === "eof") {
@@ -8190,15 +8479,14 @@ function createLineEditor(out) {
8190
8479
  activeRedraw = void 0;
8191
8480
  activeAbort = void 0;
8192
8481
  if (ticker) clearInterval(ticker);
8193
- process.stdin.off("keypress", onKey);
8482
+ keyInput.off("keypress", onKey);
8194
8483
  process.removeListener("SIGWINCH", onResize);
8195
- out.write("\x1B[?2004l");
8196
8484
  s.closeMenu();
8197
8485
  s.end();
8198
8486
  render(s, promptOf(), maxVisible);
8199
8487
  out.write("\r\n");
8200
8488
  };
8201
- process.stdin.on("keypress", onKey);
8489
+ keyInput.on("keypress", onKey);
8202
8490
  });
8203
8491
  }
8204
8492
  return {
@@ -8220,6 +8508,8 @@ function createLineEditor(out) {
8220
8508
  abort: () => activeAbort?.()
8221
8509
  };
8222
8510
  }
8511
+ var menusOpen = 0;
8512
+ var isMenuActive = () => menusOpen > 0;
8223
8513
  function selectMenu(out, opts) {
8224
8514
  if (!out.isTTY || !process.stdin.isTTY || !opts.items.length) return Promise.resolve(null);
8225
8515
  const maxVisible = opts.maxVisible ?? 10;
@@ -8278,18 +8568,20 @@ function selectMenu(out, opts) {
8278
8568
  prevLines++;
8279
8569
  };
8280
8570
  return new Promise((resolve4) => {
8281
- const wasRaw = !!process.stdin.isRaw;
8282
- emitKeypressEvents(process.stdin);
8283
- process.stdin.setRawMode(true);
8284
- process.stdin.resume();
8571
+ const wasRaw = !!keyInput.isRaw;
8572
+ menusOpen++;
8573
+ emitKeypressEvents(keyInput);
8574
+ keyInput.setRawMode(true);
8575
+ keyInput.resume();
8285
8576
  draw();
8286
8577
  const finish = (val) => {
8287
- process.stdin.off("keypress", onKey);
8578
+ menusOpen--;
8579
+ keyInput.off("keypress", onKey);
8288
8580
  if (prevLines > 1) out.write(`\x1B[${prevLines - 1}A`);
8289
8581
  out.write("\r\x1B[J");
8290
8582
  if (!wasRaw) {
8291
8583
  try {
8292
- process.stdin.setRawMode(false);
8584
+ keyInput.setRawMode(false);
8293
8585
  } catch {
8294
8586
  }
8295
8587
  }
@@ -8352,7 +8644,7 @@ function selectMenu(out, opts) {
8352
8644
  draw();
8353
8645
  }
8354
8646
  };
8355
- process.stdin.on("keypress", onKey);
8647
+ keyInput.on("keypress", onKey);
8356
8648
  });
8357
8649
  }
8358
8650
  var INPUT_MODES = [
@@ -8372,26 +8664,35 @@ function wrapLayout(promptW, buf, cursor, cols) {
8372
8664
  rows.push(line);
8373
8665
  line = "";
8374
8666
  };
8375
- for (let i = 0; i <= buf.length; i++) {
8667
+ for (let i = 0; i <= buf.length; ) {
8668
+ const cp = i < buf.length ? buf.codePointAt(i) : -1;
8669
+ const ch = cp >= 0 ? String.fromCodePoint(cp) : "";
8670
+ const w = ch && ch !== "\n" ? cpWidth(cp) : 0;
8671
+ if (w === 2 && col + w > width && col > 0) {
8672
+ pushRow();
8673
+ r++;
8674
+ col = 0;
8675
+ }
8376
8676
  if (i === cursor) {
8377
8677
  cursorRow = r;
8378
8678
  cursorCol = col;
8379
8679
  }
8380
8680
  if (i === buf.length) break;
8381
- const ch = buf[i];
8382
8681
  if (ch === "\n") {
8383
8682
  pushRow();
8384
8683
  r++;
8385
8684
  col = 0;
8685
+ i++;
8386
8686
  continue;
8387
8687
  }
8388
8688
  line += ch;
8389
- col++;
8689
+ col += w;
8390
8690
  if (col >= width) {
8391
8691
  pushRow();
8392
8692
  r++;
8393
8693
  col = 0;
8394
8694
  }
8695
+ i += ch.length;
8395
8696
  }
8396
8697
  pushRow();
8397
8698
  return { rows, cursorRow, cursorCol };
@@ -8608,6 +8909,184 @@ var MarkdownStream = class {
8608
8909
  }
8609
8910
  };
8610
8911
 
8912
+ // cli/osScheduler.ts
8913
+ import { spawnSync as spawnSync4 } from "child_process";
8914
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync8 } from "fs";
8915
+ import { homedir as homedir7 } from "os";
8916
+ import { join as join10 } from "path";
8917
+ var log19 = forComponent("os-sched");
8918
+ var OsScheduler = class {
8919
+ options;
8920
+ constructor(options) {
8921
+ this.options = { ...new OsSchedulerOptions(), ...options };
8922
+ }
8923
+ get dir() {
8924
+ return join10(this.options.home, ".agent", "sched");
8925
+ }
8926
+ label(id) {
8927
+ return `cc.livx.agentx.sched-${id}`;
8928
+ }
8929
+ plistPath(id) {
8930
+ return join10(this.options.home, "Library", "LaunchAgents", `${this.label(id)}.plist`);
8931
+ }
8932
+ run(cmd, args, input) {
8933
+ return this.options.exec(cmd, args, input);
8934
+ }
8935
+ available() {
8936
+ return this.options.platform === "darwin" || this.options.platform === "linux";
8937
+ }
8938
+ /** Register the job with the OS. Returns a human description of the mechanism used. Throws on failure. */
8939
+ schedule(spec) {
8940
+ if (!this.available()) throw new Error(`no OS scheduler on ${this.options.platform}`);
8941
+ mkdirSync7(this.dir, { recursive: true });
8942
+ const oneOff = "at" in spec.trigger;
8943
+ const script = this.writeScript(spec, oneOff);
8944
+ const mechanism = this.options.platform === "darwin" ? this.scheduleDarwin(spec, script) : this.scheduleLinux(spec, script, oneOff);
8945
+ const meta = { ...spec, created: Date.now(), mechanism };
8946
+ writeFileSync7(join10(this.dir, `${spec.id}.json`), JSON.stringify(meta, null, 2));
8947
+ return mechanism;
8948
+ }
8949
+ cancel(id) {
8950
+ const meta = readJsonFile(join10(this.dir, `${id}.json`), null);
8951
+ if (!meta) return false;
8952
+ try {
8953
+ if (this.options.platform === "darwin") {
8954
+ try {
8955
+ this.run("launchctl", ["remove", this.label(id)]);
8956
+ } catch {
8957
+ }
8958
+ try {
8959
+ unlinkSync3(this.plistPath(id));
8960
+ } catch {
8961
+ }
8962
+ } else if (meta.mechanism.startsWith("crontab")) {
8963
+ const cur = (() => {
8964
+ try {
8965
+ return this.run("crontab", ["-l"]);
8966
+ } catch {
8967
+ return "";
8968
+ }
8969
+ })();
8970
+ const next = cur.split("\n").filter((l) => !l.includes(`# agentx-sched-${id}`)).join("\n");
8971
+ this.run("crontab", ["-"], next.trim() ? next.trimEnd() + "\n" : "");
8972
+ } else if (meta.mechanism.startsWith("at:")) {
8973
+ try {
8974
+ this.run("atrm", [meta.mechanism.slice(3)]);
8975
+ } catch {
8976
+ }
8977
+ }
8978
+ } catch (e) {
8979
+ log19.debug(`cancel ${id}`, e);
8980
+ }
8981
+ for (const f of [`${id}.json`, `${id}.sh`]) {
8982
+ try {
8983
+ unlinkSync3(join10(this.dir, f));
8984
+ } catch {
8985
+ }
8986
+ }
8987
+ return true;
8988
+ }
8989
+ list() {
8990
+ if (!existsSync8(this.dir)) return [];
8991
+ return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(join10(this.dir, f), null)).filter(Boolean);
8992
+ }
8993
+ /** The per-job runner script: cd to the project, headless-resume the session, log, notify. */
8994
+ writeScript(spec, oneOff) {
8995
+ const p = join10(this.dir, `${spec.id}.sh`);
8996
+ const q2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
8997
+ const cleanup = oneOff ? this.options.platform === "darwin" ? `launchctl remove ${this.label(spec.id)} 2>/dev/null; rm -f ${q2(this.plistPath(spec.id))} ${q2(join10(this.dir, `${spec.id}.json`))} ${q2(p)}
8998
+ ` : `rm -f ${q2(join10(this.dir, `${spec.id}.json`))} ${q2(p)}
8999
+ ` : "";
9000
+ writeFileSync7(p, `#!/bin/sh
9001
+ # agentx scheduled job ${spec.id}${spec.label ? ` \u2014 ${spec.label}` : ""}
9002
+ cd ${q2(spec.cwd)} || exit 1
9003
+ ${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(join10(this.dir, `${spec.id}.log`))} 2>&1
9004
+ ${cleanup}`);
9005
+ chmodSync(p, 493);
9006
+ return p;
9007
+ }
9008
+ scheduleDarwin(spec, script) {
9009
+ const t = spec.trigger;
9010
+ let trigger;
9011
+ if ("everyMs" in t) {
9012
+ trigger = `<key>StartInterval</key><integer>${Math.max(60, Math.round(t.everyMs / 1e3))}</integer>`;
9013
+ } else if ("at" in t) {
9014
+ const d = new Date(t.at);
9015
+ trigger = `<key>StartCalendarInterval</key><dict><key>Minute</key><integer>${d.getMinutes()}</integer><key>Hour</key><integer>${d.getHours()}</integer><key>Day</key><integer>${d.getDate()}</integer><key>Month</key><integer>${d.getMonth() + 1}</integer></dict>`;
9016
+ } else {
9017
+ const f = parseCron(t.cron);
9018
+ const dict = [];
9019
+ const put = (key, vals, full) => {
9020
+ if (vals.length === full) return;
9021
+ if (vals.length !== 1) throw new Error(`macOS launchd supports only single-value cron fields (got "${t.cron}") \u2014 use {everyMs} or a simpler cron`);
9022
+ dict.push(`<key>${key}</key><integer>${vals[0]}</integer>`);
9023
+ };
9024
+ put("Minute", f.minute, 60);
9025
+ put("Hour", f.hour, 24);
9026
+ put("Day", f.dom, 31);
9027
+ put("Month", f.month, 12);
9028
+ put("Weekday", f.dow, 7);
9029
+ trigger = `<key>StartCalendarInterval</key><dict>${dict.join("")}</dict>`;
9030
+ }
9031
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
9032
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9033
+ <plist version="1.0"><dict>
9034
+ <key>Label</key><string>${this.label(spec.id)}</string>
9035
+ <key>ProgramArguments</key><array><string>/bin/sh</string><string>${script}</string></array>
9036
+ ${trigger}
9037
+ <key>RunAtLoad</key><false/>
9038
+ </dict></plist>
9039
+ `;
9040
+ mkdirSync7(join10(this.options.home, "Library", "LaunchAgents"), { recursive: true });
9041
+ writeFileSync7(this.plistPath(spec.id), plist);
9042
+ this.run("launchctl", ["load", this.plistPath(spec.id)]);
9043
+ return `launchd:${this.label(spec.id)}`;
9044
+ }
9045
+ scheduleLinux(spec, script, oneOff) {
9046
+ const t = spec.trigger;
9047
+ if (oneOff) {
9048
+ const d = new Date("at" in t ? t.at : Date.now());
9049
+ const stamp = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")} ${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
9050
+ const out = this.run("at", [stamp], `/bin/sh ${script}
9051
+ `);
9052
+ const jobId = /job (\d+)/.exec(out)?.[1] ?? "";
9053
+ return `at:${jobId}`;
9054
+ }
9055
+ const expr = "cron" in t ? t.cron : `*/${Math.max(1, Math.round(t.everyMs / 6e4))} * * * *`;
9056
+ parseCron(expr);
9057
+ const cur = (() => {
9058
+ try {
9059
+ return this.run("crontab", ["-l"]);
9060
+ } catch {
9061
+ return "";
9062
+ }
9063
+ })();
9064
+ this.run("crontab", ["-"], `${cur.trimEnd()}
9065
+ ${expr} /bin/sh ${script} # agentx-sched-${spec.id}
9066
+ `.replace(/^\n/, ""));
9067
+ return `crontab:${expr}`;
9068
+ }
9069
+ };
9070
+ var OsSchedulerOptions = class {
9071
+ platform = process.platform;
9072
+ home = homedir7();
9073
+ /** How the fired job invokes the CLI — a RAW shell snippet (may be `bun /path/cli.ts`). Default: `agentx` on PATH. */
9074
+ agentx = "agentx";
9075
+ /** Injectable executor for tests. Returns stdout+stderr merged — `at` reports "job N" on STDERR,
9076
+ * and the job id is what atrm needs for cancel. Throws on non-zero exit. */
9077
+ exec = (cmd, args, input) => {
9078
+ const r = spawnSync4(cmd, args, { input, encoding: "utf8" });
9079
+ if (r.error) throw r.error;
9080
+ if (r.status !== 0) throw new Error(`${cmd} exited ${r.status}: ${r.stderr || r.stdout}`);
9081
+ return `${r.stdout ?? ""}${r.stderr ?? ""}`;
9082
+ };
9083
+ };
9084
+ function routeTrigger(trigger, backendHint, now5 = Date.now()) {
9085
+ if (backendHint === "os") return "os";
9086
+ if (backendHint === "session") return "session";
9087
+ return "at" in trigger && trigger.at - now5 >= 30 * 6e4 ? "os" : "session";
9088
+ }
9089
+
8611
9090
  // cli/cli.ts
8612
9091
  var forceColor = process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "" && process.env.FORCE_COLOR !== "0";
8613
9092
  var useColor = forceColor || !process.env.NO_COLOR && !!process.stdout.isTTY && !!process.stderr.isTTY;
@@ -8623,10 +9102,10 @@ var italic = C("3");
8623
9102
  var strike = C("9");
8624
9103
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
8625
9104
  var err = (s) => process.stderr.write(s);
8626
- var log17 = forComponent("cli");
9105
+ var log20 = forComponent("cli");
8627
9106
  var VERSION = (() => {
8628
9107
  try {
8629
- return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
9108
+ return JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
8630
9109
  } catch {
8631
9110
  return "?";
8632
9111
  }
@@ -8639,12 +9118,19 @@ var spinner = /* @__PURE__ */ (() => {
8639
9118
  return {
8640
9119
  /** Anchor for the elapsed counter; runTurn sets it once per turn so tool-call stop/start cycles don't reset it. */
8641
9120
  turnStart: 0,
9121
+ /** Mid-turn type-ahead, rendered as the frame's tail — the spinner repaints its row every 90ms,
9122
+ * so the typed text must live INSIDE the frame or it gets erased on the next tick. */
9123
+ tail: void 0,
9124
+ get active() {
9125
+ return !!timer;
9126
+ },
8642
9127
  start(label = "thinking\u2026") {
8643
9128
  if (!tty || timer) return;
8644
9129
  t0 = this.turnStart || Date.now();
8645
9130
  timer = setInterval(() => {
8646
9131
  const secs = Math.round((Date.now() - t0) / 1e3);
8647
- err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs ? `${secs}s \xB7 ` : ""}esc to interrupt`));
9132
+ const tail = this.tail?.();
9133
+ err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs ? `${secs}s \xB7 ` : ""}esc to interrupt`) + (tail ? " " + tail : ""));
8648
9134
  }, 90);
8649
9135
  },
8650
9136
  stop() {
@@ -8662,7 +9148,9 @@ var setTermTitle = (t) => {
8662
9148
  var activeTurn = null;
8663
9149
  var exitRequested = false;
8664
9150
  var inputStash = [];
8665
- var stashBuf = "";
9151
+ var stashEd = null;
9152
+ var stashText = () => stashEd?.buf ?? "";
9153
+ var dispatchPending = false;
8666
9154
  var latestTodos = [];
8667
9155
  function numFlag(raw, flag) {
8668
9156
  const n = Number(raw);
@@ -8818,7 +9306,7 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
8818
9306
  REPL slash commands: /help /version /tools /permissions /status /cost /context /transcript /doctor /cwd /model /reasoning /config /rename /compact /memory /rewind /undo /clear /sessions /resume /commands /skills /reload /mcp /init /export /paste /goal /exit (duplex: /act /think /tasks /voice /voice-model /think-model)
8819
9307
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8820
9308
  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.
8821
- 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.
9309
+ 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 Ctrl+X Ctrl+E edits the buffer in $EDITOR \xB7 \u2192 or Tab accepts the dim history ghost-suggestion \xB7 Alt+S/Ctrl+S stash/unstash.
8822
9310
  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.
8823
9311
  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 Ctrl-_ undo \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
8824
9312
  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]; Ctrl-V or /paste grabs a clipboard image (macOS).`;
@@ -8838,11 +9326,11 @@ function resolveModelOrNewest(model) {
8838
9326
  var ENV_KEY_ALIASES = { google: ["GEMINI_API_KEY"] };
8839
9327
  function loadInstallEnv() {
8840
9328
  let dir = dirname4(import.meta.path);
8841
- for (let i = 0; i < 5 && !existsSync8(join9(dir, "package.json")); i++) dir = dirname4(dir);
9329
+ for (let i = 0; i < 5 && !existsSync9(join11(dir, "package.json")); i++) dir = dirname4(dir);
8842
9330
  for (const name of [".env", ".env.local"]) {
8843
- const file = join9(dir, name);
8844
- if (!existsSync8(file)) continue;
8845
- for (const line of readFileSync5(file, "utf8").split("\n")) {
9331
+ const file = join11(dir, name);
9332
+ if (!existsSync9(file)) continue;
9333
+ for (const line of readFileSync6(file, "utf8").split("\n")) {
8846
9334
  const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
8847
9335
  if (!m || m[1] in process.env) continue;
8848
9336
  let val = m[2].trim();
@@ -8919,7 +9407,7 @@ function makeHost(format = "text", opts) {
8919
9407
  async confirm(prompt) {
8920
9408
  const v = await selectMenu(process.stderr, { title: `? ${prompt}`, items: [{ label: "Yes", value: "y" }, { label: "No", value: "n" }], current: "n" });
8921
9409
  if (v !== null) return v === "y";
8922
- const io = createInterface({ input: process.stdin, output: process.stderr });
9410
+ const io = createInterface({ input: keyInput, output: process.stderr });
8923
9411
  try {
8924
9412
  return /^y(es)?$/i.test((await io.question(yellow(` ? ${prompt} [y/N] `))).trim());
8925
9413
  } finally {
@@ -8930,7 +9418,7 @@ function makeHost(format = "text", opts) {
8930
9418
  const title = `? ${q2.header ? "[" + q2.header + "] " : ""}${q2.question}`;
8931
9419
  const v = await selectMenu(process.stderr, { title, items: q2.options.map((o) => ({ label: o.label, value: o.label, desc: o.description })) });
8932
9420
  if (v !== null) return v;
8933
- const io = createInterface({ input: process.stdin, output: process.stderr });
9421
+ const io = createInterface({ input: keyInput, output: process.stderr });
8934
9422
  try {
8935
9423
  const lines = [yellow(" " + title)];
8936
9424
  q2.options.forEach((o, i) => lines.push(` ${i + 1}) ${o.label}${o.description ? dim(" \u2014 " + o.description) : ""}`));
@@ -9150,7 +9638,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
9150
9638
  function turnCost(model, usage) {
9151
9639
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
9152
9640
  }
9153
- async function evaluateGoal(ai, condition, transcript, log18) {
9641
+ async function evaluateGoal(ai, condition, transcript, log21) {
9154
9642
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
9155
9643
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
9156
9644
  return text.slice(0, 600);
@@ -9170,7 +9658,7 @@ ${recent}` }
9170
9658
  const match = r.content.match(/\{[\s\S]*\}/);
9171
9659
  if (match) return JSON.parse(match[0]);
9172
9660
  } catch (e) {
9173
- log18(dim(` (goal evaluator error: ${e?.message ?? e})
9661
+ log21(dim(` (goal evaluator error: ${e?.message ?? e})
9174
9662
  `));
9175
9663
  }
9176
9664
  return { met: false, reason: "evaluation unclear" };
@@ -9247,7 +9735,7 @@ function makeAskResolver(cwd) {
9247
9735
  current: "allow"
9248
9736
  });
9249
9737
  if (v === null) {
9250
- const io = createInterface({ input: process.stdin, output: process.stderr });
9738
+ const io = createInterface({ input: keyInput, output: process.stderr });
9251
9739
  try {
9252
9740
  return { decision: /^y(es)?$/i.test((await io.question(yellow(` ? Allow ${call.name}${tgt}? [y/N] `))).trim()) ? "allow" : "deny" };
9253
9741
  } finally {
@@ -9357,19 +9845,22 @@ async function mountMcp(cfg, oauth) {
9357
9845
  return mounted;
9358
9846
  }
9359
9847
  async function closeMcp(mounted) {
9360
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log17.debug("mcp close failed", e))));
9848
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log20.debug("mcp close failed", e))));
9361
9849
  }
9362
9850
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
9363
- var untilde = (p) => p.startsWith("~/") ? join9(homedir6(), p.slice(2)) : p;
9851
+ function mentionRefs(line) {
9852
+ return [...line.matchAll(/(?:^|\s)@(?:"([^"]+)"|(\S+))/g)].map((m) => m[1] ?? m[2].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9853
+ }
9854
+ var untilde = (p) => p.startsWith("~/") ? join11(homedir8(), p.slice(2)) : p;
9364
9855
  function readImageParts(cwd, line) {
9365
- const refs = [...line.matchAll(/(?:^|\s)@(\S+)/g)].map((m) => m[1].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9856
+ const refs = mentionRefs(line);
9366
9857
  const parts = [];
9367
9858
  for (const ref of refs) {
9368
9859
  const mime = IMG_EXT[extname(ref).toLowerCase()];
9369
9860
  if (!mime) continue;
9370
9861
  const abs = ref.startsWith("~/") ? untilde(ref) : resolve3(cwd, ref);
9371
9862
  try {
9372
- parts.push(imagePart(`data:${mime};base64,${readFileSync5(abs).toString("base64")}`));
9863
+ parts.push(imagePart(`data:${mime};base64,${readFileSync6(abs).toString("base64")}`));
9373
9864
  } catch {
9374
9865
  }
9375
9866
  }
@@ -9380,7 +9871,6 @@ function pastePathClassifier(cwd) {
9380
9871
  let t = text.trim();
9381
9872
  if (!t || t.includes("\n")) return null;
9382
9873
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
9383
- if (/\s/.test(t)) return null;
9384
9874
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
9385
9875
  const abs = t.startsWith("~/") ? untilde(t) : resolve3(cwd, t);
9386
9876
  try {
@@ -9389,7 +9879,7 @@ function pastePathClassifier(cwd) {
9389
9879
  return null;
9390
9880
  }
9391
9881
  const isImg = !!IMG_EXT[extname(abs).toLowerCase()];
9392
- return { display: isImg ? "Image" : `File ${basename2(abs)}`, ref: "@" + abs };
9882
+ return { display: isImg ? "Image" : `File ${basename2(abs)}`, ref: /\s/.test(abs) ? `@"${abs}"` : "@" + abs };
9393
9883
  };
9394
9884
  }
9395
9885
  var mcpMentionResolver;
@@ -9397,7 +9887,7 @@ function setMcpMentionResolver(fn) {
9397
9887
  mcpMentionResolver = fn;
9398
9888
  }
9399
9889
  async function expandMentions(fs, line) {
9400
- const refs = [...line.matchAll(/(?:^|\s)@(\S+)/g)].map((m) => m[1].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9890
+ const refs = mentionRefs(line);
9401
9891
  if (!refs.length) return { text: line, loaded: [], missing: [] };
9402
9892
  const loaded = [], missing = [], blocks = [];
9403
9893
  for (const ref of refs) {
@@ -9405,7 +9895,7 @@ async function expandMentions(fs, line) {
9405
9895
  if (loaded.includes(ref) || missing.includes(ref)) continue;
9406
9896
  if (ref.includes(":") && mcpMentionResolver) {
9407
9897
  const body = await mcpMentionResolver(ref).catch((e) => {
9408
- log17.debug("mcp mention resolve failed", e);
9898
+ log20.debug("mcp mention resolve failed", e);
9409
9899
  return null;
9410
9900
  });
9411
9901
  if (body != null) {
@@ -9583,25 +10073,25 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
9583
10073
  `;
9584
10074
  function initInstructions(cwd) {
9585
10075
  for (const f of ["AGENTS.md", "CLAUDE.md"]) {
9586
- if (existsSync8(join9(cwd, f))) {
10076
+ if (existsSync9(join11(cwd, f))) {
9587
10077
  err(yellow(` ${f} already exists \u2014 leaving it as-is
9588
10078
  `));
9589
10079
  return;
9590
10080
  }
9591
10081
  }
9592
- const path = join9(cwd, "AGENTS.md");
9593
- writeFileSync6(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
10082
+ const path = join11(cwd, "AGENTS.md");
10083
+ writeFileSync8(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
9594
10084
  err(green(` created ${path}
9595
10085
  `) + dim(" edit it, then it auto-loads into every run.\n"));
9596
10086
  }
9597
10087
  function persistSetting(cwd, key, value) {
9598
- const path = join9(cwd, ".agent", "settings.json");
10088
+ const path = join11(cwd, ".agent", "settings.json");
9599
10089
  try {
9600
- const obj = existsSync8(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
10090
+ const obj = existsSync9(path) ? JSON.parse(readFileSync6(path, "utf8")) : {};
9601
10091
  if (obj[key] === value) return;
9602
10092
  obj[key] = value;
9603
- mkdirSync7(dirname4(path), { recursive: true });
9604
- writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
10093
+ mkdirSync8(dirname4(path), { recursive: true });
10094
+ writeFileSync8(path, JSON.stringify(obj, null, 2) + "\n");
9605
10095
  } catch (e) {
9606
10096
  err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
9607
10097
  `));
@@ -9617,14 +10107,14 @@ var isCancelTeardown = (e) => {
9617
10107
  function installCancelGuards(mounted) {
9618
10108
  process.on("unhandledRejection", (e) => {
9619
10109
  if (isCancelTeardown(e)) {
9620
- log17.debug("suppressed unhandledRejection (cursor stream cancel)", e);
10110
+ log20.debug("suppressed unhandledRejection (cursor stream cancel)", e);
9621
10111
  return;
9622
10112
  }
9623
- log17.error("unhandledRejection", e);
10113
+ log20.error("unhandledRejection", e);
9624
10114
  });
9625
10115
  process.on("uncaughtException", (e) => {
9626
10116
  if (isCancelTeardown(e)) {
9627
- log17.debug("suppressed uncaughtException (cursor stream cancel)", e);
10117
+ log20.debug("suppressed uncaughtException (cursor stream cancel)", e);
9628
10118
  return;
9629
10119
  }
9630
10120
  console.error(e);
@@ -9633,7 +10123,7 @@ function installCancelGuards(mounted) {
9633
10123
  });
9634
10124
  }
9635
10125
  async function repl(args, ai, cfg, cwd) {
9636
- const oauth = new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") });
10126
+ const oauth = new McpOAuth({ storePath: join11(cwd, ".agent", "mcp-auth.json") });
9637
10127
  const mounted = await mountMcp(cfg, oauth);
9638
10128
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
9639
10129
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
@@ -9653,7 +10143,19 @@ async function repl(args, ai, cfg, cwd) {
9653
10143
  },
9654
10144
  tickMs: 15e3
9655
10145
  });
9656
- agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler)];
10146
+ const agentxInvocation = () => process.argv[1] ? `${process.execPath} ${resolve3(process.argv[1])}` : "agentx";
10147
+ const osSched = new OsScheduler({ agentx: agentxInvocation() });
10148
+ const osBackend = osSched.available() ? {
10149
+ get sessionId() {
10150
+ return session.meta.id;
10151
+ },
10152
+ cwd,
10153
+ route: (t, hint) => routeTrigger(t, hint),
10154
+ schedule: (spec) => osSched.schedule(spec),
10155
+ cancel: (id) => osSched.cancel(id),
10156
+ list: () => osSched.list()
10157
+ } : void 0;
10158
+ agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler, osBackend)];
9657
10159
  const duplex = args.duplex;
9658
10160
  let dx;
9659
10161
  let voiceIO;
@@ -9732,7 +10234,7 @@ async function repl(args, ai, cfg, cwd) {
9732
10234
  editorRef?.suspend();
9733
10235
  voiceEcho(e.message);
9734
10236
  return;
9735
- } else if (e.kind === "text_delta" && stashBuf) {
10237
+ } else if (e.kind === "text_delta" && stashText()) {
9736
10238
  process.stdout.write("\r\x1B[K");
9737
10239
  base.notify(e);
9738
10240
  repaintStash();
@@ -9815,7 +10317,7 @@ async function repl(args, ai, cfg, cwd) {
9815
10317
  quickLook: {
9816
10318
  branch: () => {
9817
10319
  try {
9818
- const head = readFileSync5(join9(cwd, ".git", "HEAD"), "utf8").trim();
10320
+ const head = readFileSync6(join11(cwd, ".git", "HEAD"), "utf8").trim();
9819
10321
  return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
9820
10322
  } catch {
9821
10323
  return "not a git repository";
@@ -9937,9 +10439,9 @@ async function repl(args, ai, cfg, cwd) {
9937
10439
  };
9938
10440
  const pendingImages = [];
9939
10441
  const grabClipboardAttachment = () => {
9940
- const dir = join9(tmpdir2(), "agentx-pasted");
10442
+ const dir = join11(tmpdir3(), "agentx-pasted");
9941
10443
  try {
9942
- mkdirSync7(dir, { recursive: true });
10444
+ mkdirSync8(dir, { recursive: true });
9943
10445
  } catch {
9944
10446
  }
9945
10447
  const img = grabClipboardImage(dir, String(Date.now()));
@@ -9989,7 +10491,7 @@ async function repl(args, ai, cfg, cwd) {
9989
10491
  err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
9990
10492
  `));
9991
10493
  }
9992
- 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 });
10494
+ const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join11(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
9993
10495
  const cpHooks = checkpoints.hooks?.();
9994
10496
  if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
9995
10497
  duplexPersist = () => {
@@ -10016,7 +10518,7 @@ async function repl(args, ai, cfg, cwd) {
10016
10518
  const fs = agent.options.fs;
10017
10519
  const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
10018
10520
  const adot = (sub) => `${fsBase}/.agent/${sub}`;
10019
- const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir6()}/.agent/${sub}`, `${homedir6()}/.claude/${sub}`];
10521
+ const adots = (sub) => dotDirs(fsBase, sub);
10020
10522
  const cmds = (await loadCommands(fs, adots("commands"))).commands;
10021
10523
  const skills = (await loadSkills(fs, adots("skills"))).skills;
10022
10524
  const refreshCatalogs = async () => {
@@ -10038,14 +10540,14 @@ ${lines.join("\n")}
10038
10540
  Added entries are loadable now via the Skill/SlashCommand tools; removed ones are gone even if still listed in the system prompt.
10039
10541
  </system-reminder>`;
10040
10542
  };
10041
- const histPath = join9(cwd, ".agent", "history");
10042
- const history = existsSync8(histPath) ? readFileSync5(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
10543
+ const histPath = join11(cwd, ".agent", "history");
10544
+ const history = existsSync9(histPath) ? readFileSync6(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
10043
10545
  const remember = (line) => {
10044
10546
  try {
10045
- mkdirSync7(join9(cwd, ".agent"), { recursive: true });
10547
+ mkdirSync8(join11(cwd, ".agent"), { recursive: true });
10046
10548
  appendFileSync(histPath, line + "\n");
10047
10549
  } catch (e) {
10048
- log17.debug("history write failed", e);
10550
+ log20.debug("history write failed", e);
10049
10551
  }
10050
10552
  };
10051
10553
  const ago = (t) => {
@@ -10115,7 +10617,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
10115
10617
  try {
10116
10618
  store.save(session);
10117
10619
  } catch (e) {
10118
- log17.debug("session save after rewind failed", e);
10620
+ log20.debug("session save after rewind failed", e);
10119
10621
  }
10120
10622
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
10121
10623
  `));
@@ -10143,7 +10645,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
10143
10645
  const announcedTasks = /* @__PURE__ */ new Set();
10144
10646
  const turn = async (task) => {
10145
10647
  const delta = await refreshCatalogs().catch((e) => {
10146
- log17.debug("catalog refresh failed", e);
10648
+ log20.debug("catalog refresh failed", e);
10147
10649
  return "";
10148
10650
  });
10149
10651
  if (delta) {
@@ -10338,8 +10840,8 @@ ${extra}` : body);
10338
10840
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10339
10841
  if (wasRaw) process.stdin.setRawMode(false);
10340
10842
  try {
10341
- const { spawnSync: spawnSync3 } = await import("child_process");
10342
- const r = spawnSync3("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
10843
+ const { spawnSync: spawnSync5 } = await import("child_process");
10844
+ const r = spawnSync5("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
10343
10845
  if (r.error) err(text);
10344
10846
  } finally {
10345
10847
  if (wasRaw) process.stdin.setRawMode(true);
@@ -10359,13 +10861,13 @@ ${extra}` : body);
10359
10861
  keys.length ? ok(`provider keys: ${keys.join(", ")}`) : bad("no provider keys set (ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY)");
10360
10862
  const info = getModelInfo(work.model);
10361
10863
  info?.pricing ? ok(`model ${work.model} \u2014 priced (${info.pricing.inputCostPer1K}/${info.pricing.outputCostPer1K} per 1k in/out)`) : warn(`model ${work.model} \u2014 no pricing in the catalog (costs will show ~$0; verify the id)`);
10362
- const cfgFiles = ["ts", "js", "json"].flatMap((e) => [`${cwd}/.agent/config.${e}`, `${homedir6()}/.agent/config.${e}`]).filter((p) => existsSync8(p));
10864
+ const cfgFiles = ["ts", "js", "json"].flatMap((e) => [`${cwd}/.agent/config.${e}`, `${homedir8()}/.agent/config.${e}`]).filter((p) => existsSync9(p));
10363
10865
  cfgFiles.length ? ok(`config: ${cfgFiles.join(", ")}`) : warn("no .agent/config.* found (project or ~) \u2014 running on defaults");
10364
10866
  try {
10365
10867
  const probe = `${cwd}/.agent/sessions/.doctor-probe`;
10366
- mkdirSync7(`${cwd}/.agent/sessions`, { recursive: true });
10367
- writeFileSync6(probe, "ok");
10368
- unlinkSync2(probe);
10868
+ mkdirSync8(`${cwd}/.agent/sessions`, { recursive: true });
10869
+ writeFileSync8(probe, "ok");
10870
+ unlinkSync4(probe);
10369
10871
  ok(`session store writable (${cwd}/.agent/sessions)`);
10370
10872
  } catch (e) {
10371
10873
  bad(`session store not writable: ${e?.message ?? e}`);
@@ -10392,7 +10894,7 @@ ${extra}` : body);
10392
10894
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
10393
10895
  run: async () => {
10394
10896
  await refreshCatalogs().catch((e) => {
10395
- log17.debug("catalog refresh failed", e);
10897
+ log20.debug("catalog refresh failed", e);
10396
10898
  });
10397
10899
  face.reprepare();
10398
10900
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -10658,6 +11160,47 @@ ${extra}` : body);
10658
11160
  `));
10659
11161
  }
10660
11162
  },
11163
+ copy: {
11164
+ desc: "copy the last reply to the clipboard \u2014 /copy code = last code block only",
11165
+ run: (a) => {
11166
+ const last = [...face.transcript].reverse().find((m) => m.role === "assistant" && contentText(m.content).trim());
11167
+ if (!last) {
11168
+ err(dim(" (nothing to copy yet)\n"));
11169
+ return;
11170
+ }
11171
+ let text = contentText(last.content).trim();
11172
+ if (a[0] === "code") {
11173
+ const fences = [...text.matchAll(/```[^\n]*\n([\s\S]*?)```/g)];
11174
+ if (!fences.length) {
11175
+ err(dim(" (no code block in the last reply)\n"));
11176
+ return;
11177
+ }
11178
+ text = fences[fences.length - 1][1].trimEnd();
11179
+ }
11180
+ err(dim(copyTextToClipboard(text) ? ` \u2713 copied ${text.length} chars
11181
+ ` : " no clipboard tool found (pbcopy/wl-copy/xclip)\n"));
11182
+ }
11183
+ },
11184
+ diff: {
11185
+ desc: "show all file changes this session (oldest checkpoint \u2192 now)",
11186
+ run: async () => {
11187
+ if (!checkpoints.diff) {
11188
+ err(dim(" (diff not supported by this checkpoint backend)\n"));
11189
+ return;
11190
+ }
11191
+ await checkpoints.refresh?.();
11192
+ if (!checkpoints.size) {
11193
+ err(dim(" (no checkpoints yet \u2014 make a turn first)\n"));
11194
+ return;
11195
+ }
11196
+ const d = (await checkpoints.diff()).trim();
11197
+ if (!d) {
11198
+ err(dim(" (no file changes this session)\n"));
11199
+ return;
11200
+ }
11201
+ err(d.split("\n").map((l) => l.startsWith("+") && !l.startsWith("+++") ? green(l) : l.startsWith("-") && !l.startsWith("---") ? red(l) : dim(l)).join("\n") + "\n");
11202
+ }
11203
+ },
10661
11204
  memory: {
10662
11205
  desc: "open the memory index in $EDITOR (.agent/memory/MEMORY.md)",
10663
11206
  run: async () => {
@@ -10688,8 +11231,8 @@ ${extra}` : body);
10688
11231
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10689
11232
  if (wasRaw) process.stdin.setRawMode(false);
10690
11233
  try {
10691
- const { spawnSync: spawnSync3 } = await import("child_process");
10692
- spawnSync3(ed, [idx], { stdio: "inherit" });
11234
+ const { spawnSync: spawnSync5 } = await import("child_process");
11235
+ spawnSync5(ed, [idx], { stdio: "inherit" });
10693
11236
  } finally {
10694
11237
  if (wasRaw) process.stdin.setRawMode(true);
10695
11238
  }
@@ -10764,7 +11307,7 @@ ${extra}` : body);
10764
11307
  if (a[0] === "new") {
10765
11308
  let name = a[1];
10766
11309
  if (!name) {
10767
- const io = createInterface({ input: process.stdin, output: process.stderr });
11310
+ const io = createInterface({ input: keyInput, output: process.stderr });
10768
11311
  try {
10769
11312
  name = (await io.question(yellow(" agent name: "))).trim();
10770
11313
  } finally {
@@ -10806,7 +11349,7 @@ ${extra}` : body);
10806
11349
  try {
10807
11350
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
10808
11351
  } catch (e) {
10809
- log17.debug(`loadAgents(${d}) failed`, e);
11352
+ log20.debug(`loadAgents(${d}) failed`, e);
10810
11353
  }
10811
11354
  }
10812
11355
  if (!seen.size) {
@@ -10898,7 +11441,7 @@ ${extra}` : body);
10898
11441
  if (idx >= 0) {
10899
11442
  const old = mounted.splice(idx, 1)[0];
10900
11443
  removeWorkTools(old.tools.map((t) => t.name));
10901
- await old.client.close().catch((e) => log17.debug("mcp close failed", e));
11444
+ await old.client.close().catch((e) => log20.debug("mcp close failed", e));
10902
11445
  }
10903
11446
  try {
10904
11447
  const m = await mountMcpServer(name, conf);
@@ -10926,7 +11469,7 @@ ${extra}` : body);
10926
11469
  }
10927
11470
  const m = mounted.splice(idx, 1)[0];
10928
11471
  removeWorkTools(m.tools.map((t) => t.name));
10929
- await m.client.close().catch((e) => log17.debug("mcp close failed", e));
11472
+ await m.client.close().catch((e) => log20.debug("mcp close failed", e));
10930
11473
  err(dim(` removed "${name}"
10931
11474
  `));
10932
11475
  return;
@@ -11004,7 +11547,7 @@ ${extra}` : body);
11004
11547
  }
11005
11548
  a = [picked];
11006
11549
  if (picked === "add") {
11007
- const io = createInterface({ input: process.stdin, output: process.stderr });
11550
+ const io = createInterface({ input: keyInput, output: process.stderr });
11008
11551
  try {
11009
11552
  const name = (await io.question(yellow(" name: "))).trim();
11010
11553
  const target = (await io.question(yellow(" command or url: "))).trim();
@@ -11058,11 +11601,11 @@ ${extra}` : body);
11058
11601
  return;
11059
11602
  }
11060
11603
  const md = exportMarkdown(session.meta, shown);
11061
- const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join9(".agent", "exports", `${session.meta.id}.md`);
11604
+ const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join11(".agent", "exports", `${session.meta.id}.md`);
11062
11605
  const path = resolve3(cwd, name);
11063
11606
  try {
11064
- mkdirSync7(dirname4(path), { recursive: true });
11065
- writeFileSync6(path, md);
11607
+ mkdirSync8(dirname4(path), { recursive: true });
11608
+ writeFileSync8(path, md);
11066
11609
  err(green(` \u2713 exported \u2192 ${path}
11067
11610
  `) + dim(` ${shown.length} message(s) \xB7 ${md.length} chars
11068
11611
  `));
@@ -11125,9 +11668,9 @@ ${extra}` : body);
11125
11668
  `));
11126
11669
  const listDir = (absDir) => {
11127
11670
  try {
11128
- return readdirSync2(join9(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
11671
+ return readdirSync3(join11(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
11129
11672
  } catch (e) {
11130
- log17.debug("completion readdir failed", absDir, e);
11673
+ log20.debug("completion readdir failed", absDir, e);
11131
11674
  return null;
11132
11675
  }
11133
11676
  };
@@ -11141,30 +11684,36 @@ ${extra}` : body);
11141
11684
  editorRef = editor;
11142
11685
  let aborting = false;
11143
11686
  let pendingRewind = false;
11687
+ const classifyPaste = pastePathClassifier(cwd);
11144
11688
  if (process.stdin.isTTY) {
11145
- const renderStashBuf = () => {
11146
- if (!stashBuf) return;
11689
+ const sEd = new EditorState(() => ({ hits: [], token: "" }), [], classifyPaste, grabClipboardAttachment);
11690
+ stashEd = sEd;
11691
+ const inverse = (x) => `\x1B[7m${x}\x1B[0m`;
11692
+ const stashView = () => {
11693
+ const v = sEd.buf.replace(/\n/g, "\u23CE");
11694
+ const c = Math.min(sEd.cursor, v.length);
11695
+ const at = c < v.length ? String.fromCodePoint(v.codePointAt(c)) : " ";
11147
11696
  const q2 = inputStash.length ? dim(` [${inputStash.length} queued]`) : "";
11148
- err(`\r\x1B[K${dim(" stash \u203A ")}${stashBuf}${q2}`);
11697
+ return `${dim("\u203A ")}${v.slice(0, c)}${inverse(at)}${v.slice(c + at.length)}${q2}`;
11698
+ };
11699
+ spinner.tail = () => sEd.buf ? stashView() : "";
11700
+ const renderStashBuf = () => {
11701
+ if (spinner.active) return;
11702
+ err(`\r\x1B[K${sEd.buf ? " " + stashView() : ""}`);
11149
11703
  };
11150
11704
  repaintStash = renderStashBuf;
11151
- process.stdin.on("keypress", (_s, key) => {
11152
- if (!activeTurn) return;
11705
+ keyInput.on("keypress", (_s, key) => {
11706
+ if (isMenuActive()) return;
11707
+ if (!activeTurn && !dispatchPending) return;
11153
11708
  if (key?.ctrl && key?.name === "o") {
11154
11709
  toggleVerbose();
11155
11710
  return;
11156
11711
  }
11157
11712
  const k = key?.name;
11158
- const cancel = k === "escape" || key?.ctrl && k === "c";
11159
- if (cancel) {
11160
- if (stashBuf) {
11161
- stashBuf = "";
11162
- err("\r\x1B[K");
11163
- return;
11164
- }
11713
+ if (!sEd.pasting && (k === "escape" || key?.ctrl && k === "c")) {
11165
11714
  if (!aborting) {
11166
11715
  aborting = true;
11167
- activeTurn.abort();
11716
+ activeTurn?.abort();
11168
11717
  voiceIO?.interrupt();
11169
11718
  err(yellow("\n \u238B cancelling\u2026") + dim(" (Ctrl-C again to force-quit)\n"));
11170
11719
  setTimeout(() => {
@@ -11179,35 +11728,29 @@ ${extra}` : body);
11179
11728
  }
11180
11729
  return;
11181
11730
  }
11182
- if (k === "return" || k === "enter") {
11183
- if (stashBuf.trim()) {
11184
- inputStash.push(stashBuf.trim());
11185
- err(`\r\x1B[K${green(" \u2713 stashed")} ${dim(`#${inputStash.length}: ${stashBuf.trim().slice(0, 50)}${stashBuf.trim().length > 50 ? "\u2026" : ""}`)}
11731
+ const action = applyKey(sEd, key ?? {}, _s);
11732
+ if (action === "submit") {
11733
+ const text = sEd.expand().trim();
11734
+ sEd.reset();
11735
+ if (text) {
11736
+ inputStash.push(text);
11737
+ const view = text.replace(/\n+/g, " \u23CE ");
11738
+ err(`\r\x1B[K${green(" \u2713 stashed")} ${dim(`#${inputStash.length}: ${view.slice(0, 50)}${view.length > 50 ? "\u2026" : ""}`)}
11186
11739
  `);
11187
11740
  }
11188
- stashBuf = "";
11189
- return;
11190
- }
11191
- if (k === "backspace") {
11192
- if (stashBuf.length) {
11193
- stashBuf = stashBuf.slice(0, -1);
11194
- if (stashBuf) renderStashBuf();
11195
- else err("\r\x1B[K");
11196
- }
11197
- return;
11198
- }
11199
- if (!key?.ctrl && !key?.meta && isPrintable(_s)) {
11200
- stashBuf += _s;
11201
- renderStashBuf();
11202
11741
  return;
11203
11742
  }
11743
+ if (sEd.pasting) return;
11744
+ if (action === "eof" || action === "rewind") return;
11745
+ renderStashBuf();
11204
11746
  });
11205
11747
  }
11206
11748
  const promptStr = bold(cyan("agentx \u203A "));
11207
11749
  const contPrompt = dim(" \u2026 \u203A ");
11208
- const classifyPaste = pastePathClassifier(cwd);
11209
11750
  const releaseStdin = () => {
11751
+ releaseKeyInput();
11210
11752
  if (process.stdin.isTTY) {
11753
+ err("\x1B[?2004l");
11211
11754
  try {
11212
11755
  process.stdin.setRawMode(false);
11213
11756
  } catch {
@@ -11218,6 +11761,7 @@ ${extra}` : body);
11218
11761
  let prefill;
11219
11762
  let tick = 0;
11220
11763
  const dispatchLine = async (line) => {
11764
+ if (/^[!#/]/.test(line)) dispatchPending = false;
11221
11765
  history.unshift(line.replace(/\n+/g, " \u23CE "));
11222
11766
  remember(line.replace(/\n+/g, " \u23CE "));
11223
11767
  if (line.startsWith("!")) {
@@ -11372,8 +11916,8 @@ ${extra}` : body);
11372
11916
  if (t !== void 0) prefill = t;
11373
11917
  }
11374
11918
  aborting = false;
11375
- const carry = stashBuf;
11376
- stashBuf = "";
11919
+ const carry = stashText() ? stashEd.expand() : "";
11920
+ stashEd?.reset();
11377
11921
  err("\n");
11378
11922
  const initial = prefill ?? (carry || void 0);
11379
11923
  prefill = void 0;
@@ -11428,6 +11972,10 @@ ${extra}` : body);
11428
11972
  history,
11429
11973
  classifyPaste,
11430
11974
  onEmptyPaste: grabClipboardAttachment,
11975
+ onSubmit: () => {
11976
+ dispatchPending = true;
11977
+ },
11978
+ // claim keys buffered behind the Enter (turn isn't active yet)
11431
11979
  initial: cont ? void 0 : initial,
11432
11980
  status: computeFooter,
11433
11981
  vimMode: cfg.editorMode === "vim",
@@ -11478,7 +12026,8 @@ ${extra}` : body);
11478
12026
  let quit = await dispatchLine(line) === "quit";
11479
12027
  while (!quit && inputStash.length) {
11480
12028
  const next = inputStash.shift();
11481
- err(dim(` \u23CE stashed \u203A ${next.slice(0, 60)}${next.length > 60 ? "\u2026" : ""}
12029
+ const nview = next.replace(/\n+/g, " \u23CE ");
12030
+ err(dim(` \u23CE stashed \u203A ${nview.slice(0, 60)}${nview.length > 60 ? "\u2026" : ""}
11482
12031
  `));
11483
12032
  quit = await dispatchLine(next) === "quit";
11484
12033
  }
@@ -11488,6 +12037,7 @@ ${extra}` : body);
11488
12037
  `));
11489
12038
  await turn(prompt);
11490
12039
  }
12040
+ dispatchPending = false;
11491
12041
  session.meta.scheduledJobs = scheduler.snapshot();
11492
12042
  try {
11493
12043
  store.save(session);
@@ -11524,11 +12074,11 @@ ${extra}` : body);
11524
12074
  `));
11525
12075
  onCtrlC();
11526
12076
  };
11527
- process.stdin.on("data", onByte);
12077
+ keyInput.on("data", onByte);
11528
12078
  await Promise.race([dx.idle(), new Promise((res) => {
11529
12079
  onCtrlC = res;
11530
12080
  })]);
11531
- process.stdin.off("data", onByte);
12081
+ keyInput.off("data", onByte);
11532
12082
  if (forced) {
11533
12083
  voiceIO?.stop();
11534
12084
  releaseStdin();
@@ -11610,7 +12160,7 @@ async function main() {
11610
12160
  }
11611
12161
  });
11612
12162
  if (args.task) {
11613
- const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") }));
12163
+ const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join11(cwd, ".agent", "mcp-auth.json") }));
11614
12164
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
11615
12165
  const store = new SessionStore(cwd);
11616
12166
  const session = startSession(args, store, agent, cwd);
@@ -11649,6 +12199,7 @@ export {
11649
12199
  formatTranscriptFull,
11650
12200
  jsonResult,
11651
12201
  mcpMentionResolver,
12202
+ mentionRefs,
11652
12203
  parseArgs,
11653
12204
  pastePathClassifier,
11654
12205
  readImageParts,