agent.libx.js 0.94.23 → 0.94.25

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;
@@ -1441,6 +1465,15 @@ var init_shell_sandbox = __esm({
1441
1465
  });
1442
1466
 
1443
1467
  // src/tools.shell.ts
1468
+ function killGroup(proc, signal) {
1469
+ if (!proc?.pid) return false;
1470
+ try {
1471
+ process.kill(-proc.pid, signal);
1472
+ return true;
1473
+ } catch {
1474
+ return false;
1475
+ }
1476
+ }
1444
1477
  async function spawnArgvFor(command, cwd, osSandbox) {
1445
1478
  if (!osSandbox) return { bin: "/bin/sh", args: ["-c", command] };
1446
1479
  const opts = osSandbox === true ? {} : osSandbox;
@@ -1463,7 +1496,7 @@ function makeRealShellTool(options) {
1463
1496
  const timeoutMs = options.timeoutMs ?? 12e4;
1464
1497
  return {
1465
1498
  name: "Shell",
1466
- description: "Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. Set `background:true` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.",
1499
+ description: "Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input fail fast rather than hang \u2014 for privileged actions use a non-interactive flag (e.g. `sudo -n`), or ask the user to run the command themselves. Set `background:true` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.",
1467
1500
  parameters: {
1468
1501
  type: "object",
1469
1502
  required: ["command"],
@@ -1523,10 +1556,12 @@ function makeRealShellTool(options) {
1523
1556
  };
1524
1557
  let proc;
1525
1558
  try {
1526
- proc = spawn3(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });
1559
+ proc = spawn3(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });
1527
1560
  } catch (e) {
1528
1561
  return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
1529
1562
  }
1563
+ if (ctl.signal.aborted) killGroup(proc, "SIGKILL");
1564
+ else ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
1530
1565
  const collect = (chunk) => {
1531
1566
  const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
1532
1567
  out += s;
@@ -1602,7 +1637,7 @@ ${clean(out) || "(no output yet)"}`;
1602
1637
  }
1603
1638
  ];
1604
1639
  }
1605
- var log12, clean, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1640
+ var log12, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1606
1641
  var init_tools_shell = __esm({
1607
1642
  "src/tools.shell.ts"() {
1608
1643
  "use strict";
@@ -1612,6 +1647,7 @@ var init_tools_shell = __esm({
1612
1647
  init_shell_sandbox();
1613
1648
  log12 = forComponent("shell");
1614
1649
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
1650
+ DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
1615
1651
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
1616
1652
  ShellJobRegistry = class {
1617
1653
  constructor(cfg) {
@@ -1632,7 +1668,7 @@ var init_tools_shell = __esm({
1632
1668
  try {
1633
1669
  const spawn3 = this.cfg.spawn ?? await nodeSpawn();
1634
1670
  const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
1635
- const proc = spawn3(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg) });
1671
+ const proc = spawn3(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });
1636
1672
  job.proc = proc;
1637
1673
  proc.stdout?.on("data", append);
1638
1674
  proc.stderr?.on("data", append);
@@ -1671,9 +1707,11 @@ var init_tools_shell = __esm({
1671
1707
  const j = this.jobs.get(id);
1672
1708
  if (!j) return false;
1673
1709
  if (j.status === "running") {
1674
- try {
1675
- j.proc?.kill("SIGTERM");
1676
- } catch {
1710
+ if (!killGroup(j.proc, "SIGTERM")) {
1711
+ try {
1712
+ j.proc?.kill("SIGTERM");
1713
+ } catch {
1714
+ }
1677
1715
  }
1678
1716
  j.status = "killed";
1679
1717
  }
@@ -1689,8 +1727,8 @@ var init_tools_shell = __esm({
1689
1727
 
1690
1728
  // cli/cli.ts
1691
1729
  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";
1730
+ 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";
1731
+ import { homedir as homedir8, tmpdir as tmpdir3 } from "os";
1694
1732
 
1695
1733
  // cli/clipboard.ts
1696
1734
  import { execFileSync } from "child_process";
@@ -1744,9 +1782,20 @@ close access f`;
1744
1782
  }
1745
1783
  return null;
1746
1784
  }
1785
+ function copyTextToClipboard(text, platform2 = process.platform) {
1786
+ const candidates = platform2 === "darwin" ? [["pbcopy", []]] : platform2 === "linux" ? [["wl-copy", []], ["xclip", ["-selection", "clipboard"]]] : [];
1787
+ for (const [cmd, args] of candidates) {
1788
+ try {
1789
+ execFileSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
1790
+ return true;
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ return false;
1795
+ }
1747
1796
 
1748
1797
  // cli/cli.ts
1749
- import { join as join9, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
1798
+ import { join as join11, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
1750
1799
  import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions } from "ai.libx.js";
1751
1800
 
1752
1801
  // src/llm.ts
@@ -2839,6 +2888,8 @@ var AgentOptions = class {
2839
2888
  permissions;
2840
2889
  /** Opt-in syntax guardrail: refuse to persist a syntactically-broken code-file write/edit. Default off. */
2841
2890
  lintOnWrite;
2891
+ /** Optional PDF text extraction for Read on .pdf files (node hosts wire pdftotext); absent => Read explains. */
2892
+ pdfText;
2842
2893
  /** Opt-in: after a write-class tool runs, run `command` over the VFS and append any failure to the tool result.
2843
2894
  * `tools` defaults to ['Write','Edit','MultiEdit','ApplyEdits']. */
2844
2895
  autoTest;
@@ -2889,8 +2940,12 @@ var Agent = class _Agent {
2889
2940
  reprepare() {
2890
2941
  this.prepared = false;
2891
2942
  }
2892
- /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn. */
2943
+ /** Tools injected via addTools(); kept separate from options.tools so prepare() rebuilds don't drop them. */
2944
+ injectedTools = [];
2945
+ /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn
2946
+ * and survives prepare() rebuilds (reprepare(), new conversations). */
2893
2947
  addTools(tools) {
2948
+ this.injectedTools.push(...tools);
2894
2949
  this.activeTools.push(...tools);
2895
2950
  }
2896
2951
  /** Remove tools by name from a running agent. Returns the count removed. */
@@ -2898,6 +2953,7 @@ var Agent = class _Agent {
2898
2953
  const s = names instanceof Set ? names : new Set(names);
2899
2954
  const before = this.activeTools.length;
2900
2955
  this.activeTools = this.activeTools.filter((t) => !s.has(t.name));
2956
+ this.injectedTools = this.injectedTools.filter((t) => !s.has(t.name));
2901
2957
  return before - this.activeTools.length;
2902
2958
  }
2903
2959
  constructor(options) {
@@ -2909,6 +2965,7 @@ var Agent = class _Agent {
2909
2965
  this.ctx = makeContext(this.options.fs, this.options.host);
2910
2966
  this.ctx.signal = this.options.signal;
2911
2967
  if (this.options.lintOnWrite) this.ctx.lint = checkSyntax;
2968
+ if (this.options.pdfText) this.ctx.pdfText = this.options.pdfText;
2912
2969
  this.ctx.ai = this.options.ai;
2913
2970
  this.ctx.model = this.options.model;
2914
2971
  this.ctx.parkHuman = (p) => this.park(p);
@@ -2981,7 +3038,7 @@ var Agent = class _Agent {
2981
3038
  const plan = o.planMode ? planMode({ host: o.host }) : void 0;
2982
3039
  if (plan) tools = [...tools, plan.tool];
2983
3040
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
2984
- this.activeTools = tools;
3041
+ this.activeTools = [...tools, ...this.injectedTools];
2985
3042
  this.systemPromptCache = systemPrompt;
2986
3043
  this.prepared = true;
2987
3044
  return systemPrompt;
@@ -3116,7 +3173,15 @@ var Agent = class _Agent {
3116
3173
  } catch (err2) {
3117
3174
  if (err2?.code === "budget") return kill("budget");
3118
3175
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
3119
- log3.error(`chat() failed: ${err2?.message ?? err2}`, err2);
3176
+ const body = err2?.body ?? err2?.response?.data ?? err2?.error;
3177
+ let bodyStr;
3178
+ try {
3179
+ bodyStr = body && typeof body !== "string" ? JSON.stringify(body).slice(0, 2e3) : body;
3180
+ } catch {
3181
+ bodyStr = void 0;
3182
+ }
3183
+ if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
3184
+ log3.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err2);
3120
3185
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
3121
3186
  }
3122
3187
  if (o.signal?.aborted) return kill("aborted");
@@ -3347,20 +3412,28 @@ function stubOldToolResults(messages, keep) {
3347
3412
  return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
3348
3413
  });
3349
3414
  }
3350
- var hasCallFor = (msgs, id) => msgs.some((m) => m.role === "assistant" && m.tool_calls?.some((tc) => tc.id === id));
3415
+ var callIdSet = (msgs) => {
3416
+ const ids = /* @__PURE__ */ new Set();
3417
+ for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
3418
+ return ids;
3419
+ };
3351
3420
  function dropOrphanToolResults(messages) {
3352
- const ok = (m) => m.role !== "tool" || hasCallFor(messages, m.tool_call_id);
3421
+ const ids = callIdSet(messages);
3422
+ const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
3353
3423
  return messages.every(ok) ? messages : messages.filter(ok);
3354
3424
  }
3355
3425
  function fitTokenBudget(messages, maxTokens) {
3356
- if (estimateTokens(messages) <= maxTokens) return messages;
3426
+ const per = messages.map((x) => estimateTokens([x]));
3427
+ let total = per.reduce((a, b) => a + b, 0);
3428
+ if (total <= maxTokens) return messages;
3357
3429
  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];
3430
+ let from = head.length;
3431
+ while (from < messages.length && total > maxTokens) total -= per[from++];
3432
+ const ids = callIdSet(messages.slice(from));
3433
+ while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
3434
+ if (total > maxTokens)
3435
+ log3.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3436
+ return [...head, ...messages.slice(from)];
3364
3437
  }
3365
3438
  function compact(m, max, focus) {
3366
3439
  const hasSystem = m[0]?.role === "system";
@@ -3812,11 +3885,11 @@ var Scheduler = class {
3812
3885
  this.jobs.clear();
3813
3886
  }
3814
3887
  };
3815
- function makeScheduleTools(scheduler) {
3888
+ function makeScheduleTools(scheduler, os) {
3816
3889
  return [
3817
3890
  {
3818
3891
  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.',
3892
+ 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
3893
  parameters: {
3821
3894
  type: "object",
3822
3895
  required: ["prompt", "trigger"],
@@ -3831,15 +3904,22 @@ function makeScheduleTools(scheduler) {
3831
3904
  cron: { type: "string" }
3832
3905
  }
3833
3906
  },
3834
- label: { type: "string", description: "Short label for display (optional)." }
3907
+ label: { type: "string", description: "Short label for display (optional)." },
3908
+ backend: { type: "string", enum: ["auto", "session", "os"], description: "Where the job lives (default auto)." }
3835
3909
  }
3836
3910
  },
3837
- async run({ prompt, trigger, label }) {
3911
+ async run({ prompt, trigger, label, backend }) {
3838
3912
  try {
3913
+ if (os && os.route(trigger, backend) === "os") {
3914
+ const id2 = `os-${Date.now().toString(36)}`;
3915
+ const mechanism = os.schedule({ id: id2, prompt, sessionId: os.sessionId, cwd: os.cwd, trigger, label });
3916
+ return `Scheduled ${id2}${label ? ` (${label})` : ""} on the OS scheduler (${mechanism}) \u2014 survives quitting; fires \`agentx --resume ${os.sessionId}\` headless.`;
3917
+ }
3918
+ if (backend === "os") return "Error: no OS scheduler available on this platform \u2014 job not created (use the default in-session backend).";
3839
3919
  const id = scheduler.add({ prompt, trigger, label });
3840
3920
  const job = scheduler.get(id);
3841
3921
  const next = scheduler.nextFire(job);
3842
- return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}.`;
3922
+ return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}. (In-session: does not survive quitting.)`;
3843
3923
  } catch (e) {
3844
3924
  return `Error: ${e?.message ?? e}`;
3845
3925
  }
@@ -3847,24 +3927,28 @@ function makeScheduleTools(scheduler) {
3847
3927
  },
3848
3928
  {
3849
3929
  name: "ScheduleList",
3850
- description: "List all scheduled jobs and their next fire time.",
3930
+ description: "List all scheduled jobs (in-session + OS-backed) and their next fire time.",
3851
3931
  parameters: { type: "object", properties: {} },
3852
3932
  async run() {
3933
+ const osJobs = os?.list() ?? [];
3934
+ const osLines = osJobs.map((j) => `${j.id} os ${j.mechanism}${j.label ? " " + j.label : ""}`);
3853
3935
  const jobs = scheduler.list();
3854
- if (!jobs.length) return "(no scheduled jobs)";
3855
- return jobs.map((j) => {
3936
+ if (!jobs.length && !osLines.length) return "(no scheduled jobs)";
3937
+ return [...osLines, ...jobs.map((j) => {
3856
3938
  const next = scheduler.nextFire(j);
3857
3939
  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
3940
  return `${j.id} ${j.status} ${trig} runs:${j.runs} next:${next ? new Date(next).toLocaleTimeString() : "\u2014"}${j.label ? " " + j.label : ""}`;
3859
- }).join("\n");
3941
+ })].join("\n");
3860
3942
  }
3861
3943
  },
3862
3944
  {
3863
3945
  name: "ScheduleCancel",
3864
- description: "Cancel a scheduled job by id.",
3946
+ description: "Cancel a scheduled job by id (in-session or OS-backed).",
3865
3947
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
3866
3948
  async run({ id }) {
3867
- return scheduler.cancel(String(id)) ? `Cancelled ${id}.` : `Error: no scheduled job '${id}'. Use ScheduleList to see jobs.`;
3949
+ const key = String(id);
3950
+ if (key.startsWith("os-")) return os?.cancel(key) ? `Cancelled ${key} (OS job removed).` : `Error: no OS job '${key}'.`;
3951
+ return scheduler.cancel(key) ? `Cancelled ${key}.` : `Error: no scheduled job '${key}'. Use ScheduleList to see jobs.`;
3868
3952
  }
3869
3953
  },
3870
3954
  {
@@ -4162,6 +4246,9 @@ var DuplexAgentOptions = class {
4162
4246
  askRelay = false;
4163
4247
  /** Parked questions auto-resolve empty after this long (callers map '' to deny/best-judgment). */
4164
4248
  askTimeoutMs = 12e4;
4249
+ /** Max retained task records: oldest SETTLED tasks (and their activity tails) are evicted past this,
4250
+ * bounding memory over a long-lived session. Running tasks are never evicted. */
4251
+ maxTaskRecords = 50;
4165
4252
  /** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
4166
4253
  * (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
4167
4254
  quickLook;
@@ -4452,6 +4539,11 @@ ${recent}` : brief) + verify;
4452
4539
  };
4453
4540
  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
4541
  this.tasks.set(id, { id, label, status: "running", controller, promise, tail });
4542
+ if (this.tasks.size > this.options.maxTaskRecords)
4543
+ for (const [tid, rec] of this.tasks) {
4544
+ if (this.tasks.size <= this.options.maxTaskRecords) break;
4545
+ if (rec.status !== "running") this.tasks.delete(tid);
4546
+ }
4455
4547
  }
4456
4548
  /** Fresh-context check of a successful Act task: a NEW agent (same model/fs/tools, but NO shared
4457
4549
  * conversation context) re-reads the file state against the brief and fixes any gap. The fix lands
@@ -6052,15 +6144,101 @@ function defaultOpenBrowser(url) {
6052
6144
 
6053
6145
  // cli/core.ts
6054
6146
  import { randomUUID } from "crypto";
6147
+ import { execFile as execFile2 } from "child_process";
6055
6148
  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";
6149
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
6150
+ import { platform, arch, release, userInfo, homedir as homedir2, tmpdir } from "os";
6058
6151
  init_tools_shell();
6152
+
6153
+ // src/tools.notify.ts
6154
+ init_logging();
6155
+ import { execFile } from "child_process";
6156
+ var log13 = forComponent("notify");
6157
+ function makeNotifyTool(opts = {}) {
6158
+ const platform2 = opts.platform ?? process.platform;
6159
+ const run = opts.exec ?? execFile;
6160
+ return {
6161
+ name: "PushNotification",
6162
+ 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.",
6163
+ parameters: {
6164
+ type: "object",
6165
+ required: ["message"],
6166
+ properties: {
6167
+ message: { type: "string", description: "notification body" },
6168
+ title: { type: "string", description: 'notification title (default "agentx")' }
6169
+ }
6170
+ },
6171
+ async run({ message, title }) {
6172
+ const msg = String(message ?? "").slice(0, 256);
6173
+ const head = String(title ?? "agentx").slice(0, 64);
6174
+ if (!msg) return "Error: empty message";
6175
+ const argv = platform2 === "darwin" ? ["osascript", ["-e", `display notification ${JSON.stringify(msg)} with title ${JSON.stringify(head)}`]] : platform2 === "linux" ? ["notify-send", [head, msg]] : null;
6176
+ if (!argv) return `Notifications unavailable on ${platform2}.`;
6177
+ return new Promise((resolve4) => {
6178
+ run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
6179
+ if (e) {
6180
+ log13.debug("notification failed", e);
6181
+ resolve4(`Notification failed: ${e.message}`);
6182
+ } else resolve4("Notification shown.");
6183
+ });
6184
+ });
6185
+ }
6186
+ };
6187
+ }
6188
+
6189
+ // cli/core.ts
6059
6190
  import { BodDB as BodDB2 } from "@bod.ee/db";
6191
+
6192
+ // cli/util.ts
6193
+ init_logging();
6194
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
6195
+ import { homedir } from "os";
6196
+ var log14 = forComponent("cli-util");
6197
+ function dotDirs(base, sub, opts = {}) {
6198
+ const home = opts.home ?? homedir();
6199
+ const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
6200
+ return opts.existing ? dirs.filter((d) => existsSync2(d)) : dirs;
6201
+ }
6202
+ function truncate(s, n, suffix = "\u2026") {
6203
+ const t = s ?? "";
6204
+ return t.length > n ? t.slice(0, n) + suffix : t;
6205
+ }
6206
+ function sanitizeLabel(s, max = 60) {
6207
+ return truncate(s.replace(/\s+/g, " ").trim(), max, "");
6208
+ }
6209
+ function parseJson(text, fallback, what = "json") {
6210
+ try {
6211
+ return JSON.parse(text);
6212
+ } catch (e) {
6213
+ log14.debug(`parseJson(${what}) failed: ${e.message}`);
6214
+ return fallback;
6215
+ }
6216
+ }
6217
+ function readJsonFile(path, fallback) {
6218
+ if (!existsSync2(path)) return fallback;
6219
+ let text;
6220
+ try {
6221
+ text = readFileSync2(path, "utf8");
6222
+ } catch (e) {
6223
+ log14.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
6224
+ return fallback;
6225
+ }
6226
+ return parseJson(text, fallback, path);
6227
+ }
6228
+
6229
+ // cli/core.ts
6060
6230
  var DEFAULT_TOOLS = ["bash", "Read", "Edit", "Write", "Grep", "Glob", "MultiEdit", "ApplyEdits", "RepoMap", "TodoWrite"];
6061
6231
  function autoWebTools() {
6062
6232
  return ["WebFetch", "WebSearch"];
6063
6233
  }
6234
+ function pdfTextViaPoppler(path) {
6235
+ return new Promise((res, rej) => {
6236
+ execFile2("pdftotext", [path, "-"], { maxBuffer: 32 * 1024 * 1024, timeout: 3e4 }, (e, stdout) => {
6237
+ if (e) rej(new Error(/ENOENT/.test(String(e.code ?? e.message)) ? "pdftotext not installed (brew/apt install poppler)" : e.message));
6238
+ else res(stdout);
6239
+ });
6240
+ });
6241
+ }
6064
6242
  var SANDBOX_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"]);
6065
6243
  async function hydrate(from, to, dir = "/") {
6066
6244
  let n = 0;
@@ -6155,33 +6333,27 @@ async function buildAgent(o) {
6155
6333
  ${notes.join("\n")}
6156
6334
  Reference files in them by their mount path (the left side).`;
6157
6335
  }
6158
- const dot = (sub) => existsSync2(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
6336
+ const dot = (sub) => existsSync3(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
6159
6337
  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);
6338
+ const dirs = dotDirs(cwd, sub, { existing: true });
6167
6339
  return dirs.length ? dirs : void 0;
6168
6340
  };
6169
6341
  const memoryDir = (() => {
6170
- const home = homedir();
6342
+ const home = homedir2();
6171
6343
  const projectDir = `${cwd}/.agent/memory`;
6172
6344
  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
6345
+ existsSync3(projectDir) ? projectDir : void 0,
6346
+ existsSync3(`${cwd}/.claude/memory`) ? `${cwd}/.claude/memory` : void 0,
6347
+ existsSync3(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6348
+ existsSync3(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6177
6349
  ].filter(Boolean);
6178
6350
  return readDirs[0] === projectDir ? readDirs : [projectDir, ...readDirs];
6179
6351
  })();
6180
6352
  const memoryUserDir = (() => {
6181
- const home = homedir();
6353
+ const home = homedir2();
6182
6354
  return [
6183
- existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6184
- existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6355
+ existsSync3(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6356
+ existsSync3(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6185
6357
  ].find(Boolean) ?? `${home}/.agent/memory`;
6186
6358
  })();
6187
6359
  const memoryWriteDir = memoryDir[0];
@@ -6198,6 +6370,8 @@ Reference files in them by their mount path (the left side).`;
6198
6370
  ai: o.ai,
6199
6371
  fs,
6200
6372
  model: o.model ?? "anthropic/claude-sonnet-4-6",
6373
+ // PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
6374
+ ...!virtual ? { pdfText: pdfTextViaPoppler } : {},
6201
6375
  // Anchor cursor to the launch dir (its adapter defaults to TMPDIR otherwise) and forward the
6202
6376
  // host's MCP servers so the delegated cursor agent runs in the same environment. Gated to cursor:
6203
6377
  // openai/google adapters Object.assign providerOptions into the request body, so a blanket cwd
@@ -6240,6 +6414,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
6240
6414
  const base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
6241
6415
  const tail = [...o.extraTools ?? []];
6242
6416
  if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "anthropic/claude-sonnet-4-6", dir: scratchDir }));
6417
+ tail.push(makeNotifyTool());
6243
6418
  if (!realShell.length) return [...base, ...tail];
6244
6419
  const filtered = base.filter((t) => t.name !== "bash");
6245
6420
  return [...filtered, ...realShell, ...tail];
@@ -6291,11 +6466,11 @@ var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0
6291
6466
  // cli/voice.ts
6292
6467
  init_logging();
6293
6468
  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";
6469
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
6470
+ import { homedir as homedir3 } from "os";
6296
6471
  import { dirname as dirname3, join as join4 } from "path";
6297
6472
  import { fileURLToPath } from "url";
6298
- var log13 = forComponent("VoiceIO");
6473
+ var log15 = forComponent("VoiceIO");
6299
6474
  var now4 = () => performance.now();
6300
6475
  var Player = class {
6301
6476
  proc = null;
@@ -6309,7 +6484,7 @@ var Player = class {
6309
6484
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
6310
6485
  { stdio: ["pipe", "ignore", "ignore"] }
6311
6486
  );
6312
- this.proc.on("error", (e) => log13.warn(`ffplay error: ${e.message}`));
6487
+ this.proc.on("error", (e) => log15.warn(`ffplay error: ${e.message}`));
6313
6488
  this.proc.stdin.on("error", () => {
6314
6489
  });
6315
6490
  this.bytesWritten = 0;
@@ -6344,28 +6519,28 @@ function detectFfmpegMic() {
6344
6519
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
6345
6520
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
6346
6521
  if (!mic) throw new Error("no audio input device found");
6347
- log13.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
6522
+ log15.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
6348
6523
  return `:${mic.idx}`;
6349
6524
  }
6350
6525
  function resolveAecBinary() {
6351
6526
  if (process.env.MIC_AEC === "0" || process.platform !== "darwin") return null;
6352
6527
  const src = join4(nativeDir(), "mic-aec.swift");
6353
6528
  const plist = join4(nativeDir(), "Info.plist");
6354
- if (!existsSync3(src)) return null;
6355
- const cacheDir = join4(homedir2(), ".agent", "cache");
6529
+ if (!existsSync4(src)) return null;
6530
+ const cacheDir = join4(homedir3(), ".agent", "cache");
6356
6531
  const bin = join4(cacheDir, "mic-aec");
6357
- if (existsSync3(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
6532
+ if (existsSync4(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
6358
6533
  if (spawnSync("which", ["swiftc"]).status !== 0) return null;
6359
6534
  mkdirSync3(cacheDir, { recursive: true });
6360
- log13.info("compiling AEC mic helper (first run)\u2026");
6535
+ log15.info("compiling AEC mic helper (first run)\u2026");
6361
6536
  const build = spawnSync("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
6362
6537
  if (build.status !== 0) {
6363
- log13.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
6538
+ log15.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
6364
6539
  return null;
6365
6540
  }
6366
6541
  const sign = spawnSync("codesign", ["-fs", "-", bin], { encoding: "utf8" });
6367
6542
  if (sign.status !== 0) {
6368
- log13.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
6543
+ log15.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
6369
6544
  return null;
6370
6545
  }
6371
6546
  return bin;
@@ -6384,16 +6559,16 @@ var NodeMicSource = class {
6384
6559
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
6385
6560
  } else {
6386
6561
  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");
6562
+ log15.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
6388
6563
  this.proc = spawn2(
6389
6564
  "ffmpeg",
6390
6565
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
6391
6566
  { stdio: ["ignore", "pipe", "pipe"] }
6392
6567
  );
6393
- this.proc.stderr.on("data", (d) => log13.warn(`ffmpeg: ${String(d).trim()}`));
6568
+ this.proc.stderr.on("data", (d) => log15.warn(`ffmpeg: ${String(d).trim()}`));
6394
6569
  }
6395
6570
  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`);
6571
+ if (c && !this.stopped) log15.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
6397
6572
  });
6398
6573
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
6399
6574
  }
@@ -6427,11 +6602,11 @@ var AecDuplexAudio = class {
6427
6602
  this.proc.stdin.on("error", () => {
6428
6603
  });
6429
6604
  this.proc.on("exit", (c) => {
6430
- if (c && !this.stopped) log13.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
6605
+ if (c && !this.stopped) log15.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
6431
6606
  });
6432
6607
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
6433
6608
  this.proc.stderr.on("data", (d) => {
6434
- for (const ln of String(d).split("\n")) if (ln.trim()) log13.debug(`mic-aec: ${ln.trim()}`);
6609
+ for (const ln of String(d).split("\n")) if (ln.trim()) log15.debug(`mic-aec: ${ln.trim()}`);
6435
6610
  });
6436
6611
  }
6437
6612
  stop() {
@@ -6547,15 +6722,15 @@ var VoiceIO = class extends VoiceEngine {
6547
6722
  };
6548
6723
 
6549
6724
  // cli/config.ts
6550
- import { homedir as homedir3 } from "os";
6551
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
6725
+ import { homedir as homedir4 } from "os";
6726
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
6552
6727
  import { join as join5 } from "path";
6553
6728
  import { pathToFileURL } from "url";
6554
6729
  var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
6555
6730
  async function loadFrom(dir) {
6556
6731
  for (const f of FILES) {
6557
6732
  const p = join5(dir, ".agent", f);
6558
- if (!existsSync4(p)) continue;
6733
+ if (!existsSync5(p)) continue;
6559
6734
  try {
6560
6735
  const mod = await import(pathToFileURL(p).href, f.endsWith(".json") ? { with: { type: "json" } } : void 0);
6561
6736
  return mod.default ?? mod.config ?? mod;
@@ -6568,9 +6743,9 @@ async function loadFrom(dir) {
6568
6743
  }
6569
6744
  function loadSettings(dir) {
6570
6745
  const p = join5(dir, ".agent", "settings.json");
6571
- if (!existsSync4(p)) return {};
6746
+ if (!existsSync5(p)) return {};
6572
6747
  try {
6573
- const raw = JSON.parse(readFileSync2(p, "utf8"));
6748
+ const raw = JSON.parse(readFileSync3(p, "utf8"));
6574
6749
  const cfg = {};
6575
6750
  if (raw.mcpServers && typeof raw.mcpServers === "object") cfg.mcpServers = raw.mcpServers;
6576
6751
  if (raw.permissions && typeof raw.permissions === "object") cfg.permissions = raw.permissions;
@@ -6591,8 +6766,8 @@ function loadSettings(dir) {
6591
6766
  }
6592
6767
  }
6593
6768
  async function loadConfig(cwd) {
6594
- const userSettings = loadSettings(homedir3());
6595
- const user = await loadFrom(homedir3());
6769
+ const userSettings = loadSettings(homedir4());
6770
+ const user = await loadFrom(homedir4());
6596
6771
  const projectSettings = loadSettings(cwd);
6597
6772
  const project = await loadFrom(cwd);
6598
6773
  const merged = { ...userSettings, ...user, ...projectSettings, ...project };
@@ -6604,7 +6779,7 @@ async function loadConfig(cwd) {
6604
6779
 
6605
6780
  // cli/hooks-config.ts
6606
6781
  import { spawnSync as spawnSync2 } from "child_process";
6607
- var log14 = forComponent("hooks");
6782
+ var log16 = forComponent("hooks");
6608
6783
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6609
6784
  function ruleMatches(rule, toolName) {
6610
6785
  if (!rule.tool || rule.tool === "*") return true;
@@ -6621,7 +6796,7 @@ function runCmd(rule, env) {
6621
6796
  });
6622
6797
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
6623
6798
  } catch (e) {
6624
- log14.debug(`hook command failed: ${rule.command}`, e);
6799
+ log16.debug(`hook command failed: ${rule.command}`, e);
6625
6800
  return { code: 1, out: String(e?.message ?? e) };
6626
6801
  }
6627
6802
  }
@@ -6725,11 +6900,11 @@ function formatDiff(ops, opts = {}) {
6725
6900
  }
6726
6901
 
6727
6902
  // 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";
6903
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync, renameSync, symlinkSync, unlinkSync, readlinkSync } from "fs";
6904
+ import { homedir as homedir5 } from "os";
6730
6905
  import { join as join6 } from "path";
6731
- var log15 = forComponent("session");
6732
- var globalDir = () => join6(homedir4(), ".agent", "sessions");
6906
+ var log17 = forComponent("session");
6907
+ var globalDir = () => join6(homedir5(), ".agent", "sessions");
6733
6908
  var SessionStore = class {
6734
6909
  dir;
6735
6910
  constructor(cwd) {
@@ -6741,10 +6916,10 @@ var SessionStore = class {
6741
6916
  const p = (n, w = 2) => String(n).padStart(w, "0");
6742
6917
  const slug2 = (cwd ?? process.cwd()).split("/").pop()?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
6743
6918
  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`))) {
6919
+ if (existsSync6(this.dir) && existsSync6(join6(this.dir, `${id}.json`))) {
6745
6920
  for (let i = 2; i <= 99; i++) {
6746
6921
  const c = `${id}-${i}`;
6747
- if (!existsSync5(join6(this.dir, `${c}.json`))) {
6922
+ if (!existsSync6(join6(this.dir, `${c}.json`))) {
6748
6923
  id = c;
6749
6924
  break;
6750
6925
  }
@@ -6758,43 +6933,43 @@ var SessionStore = class {
6758
6933
  }
6759
6934
  save(data) {
6760
6935
  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 });
6936
+ if (!existsSync6(this.dir)) mkdirSync4(this.dir, { recursive: true });
6762
6937
  const path = join6(this.dir, `${data.meta.id}.json`);
6763
6938
  const tmp = `${path}.${process.pid}.tmp`;
6764
6939
  writeFileSync3(tmp, JSON.stringify(data));
6765
6940
  renameSync(tmp, path);
6766
6941
  try {
6767
6942
  const gd = globalDir();
6768
- if (!existsSync5(gd)) mkdirSync4(gd, { recursive: true });
6943
+ if (!existsSync6(gd)) mkdirSync4(gd, { recursive: true });
6769
6944
  const link2 = join6(gd, `${data.meta.id}.json`);
6770
- if (!existsSync5(link2)) symlinkSync(path, link2);
6945
+ if (!existsSync6(link2)) symlinkSync(path, link2);
6771
6946
  } catch {
6772
6947
  }
6773
6948
  }
6774
6949
  load(id) {
6775
6950
  if (!this.safeId(id)) {
6776
- log15.debug(`rejecting unsafe session id: ${id}`);
6951
+ log17.debug(`rejecting unsafe session id: ${id}`);
6777
6952
  return void 0;
6778
6953
  }
6779
6954
  const path = join6(this.dir, `${id}.json`);
6780
- if (!existsSync5(path)) return void 0;
6955
+ if (!existsSync6(path)) return void 0;
6781
6956
  try {
6782
- return JSON.parse(readFileSync3(path, "utf8"));
6957
+ return JSON.parse(readFileSync4(path, "utf8"));
6783
6958
  } catch (e) {
6784
- log15.debug(`unreadable session ${id} \u2014 ignoring`, e);
6959
+ log17.debug(`unreadable session ${id} \u2014 ignoring`, e);
6785
6960
  return void 0;
6786
6961
  }
6787
6962
  }
6788
6963
  /** All sessions' metadata, most-recently-updated first. */
6789
6964
  list() {
6790
- if (!existsSync5(this.dir)) return [];
6965
+ if (!existsSync6(this.dir)) return [];
6791
6966
  const metas = [];
6792
6967
  for (const f of readdirSync(this.dir)) {
6793
6968
  if (!f.endsWith(".json")) continue;
6794
6969
  try {
6795
- metas.push(JSON.parse(readFileSync3(join6(this.dir, f), "utf8")).meta);
6970
+ metas.push(JSON.parse(readFileSync4(join6(this.dir, f), "utf8")).meta);
6796
6971
  } catch (e) {
6797
- log15.debug(`skipping unreadable session file ${f}`, e);
6972
+ log17.debug(`skipping unreadable session file ${f}`, e);
6798
6973
  }
6799
6974
  }
6800
6975
  return metas.sort((a, b) => b.updated - a.updated);
@@ -6810,12 +6985,12 @@ var SessionStore = class {
6810
6985
  };
6811
6986
  function globalSessionLoad(idOrPrefix) {
6812
6987
  const gd = globalDir();
6813
- if (!existsSync5(gd)) return void 0;
6988
+ if (!existsSync6(gd)) return void 0;
6814
6989
  const exact = join6(gd, `${idOrPrefix}.json`);
6815
- if (existsSync5(exact)) {
6990
+ if (existsSync6(exact)) {
6816
6991
  try {
6817
6992
  const target = readlinkSync(exact);
6818
- return JSON.parse(readFileSync3(target, "utf8"));
6993
+ return JSON.parse(readFileSync4(target, "utf8"));
6819
6994
  } catch {
6820
6995
  return void 0;
6821
6996
  }
@@ -6826,7 +7001,7 @@ function globalSessionLoad(idOrPrefix) {
6826
7001
  const base = f.slice(0, -5);
6827
7002
  if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
6828
7003
  const target = readlinkSync(join6(gd, f));
6829
- return JSON.parse(readFileSync3(target, "utf8"));
7004
+ return JSON.parse(readFileSync4(target, "utf8"));
6830
7005
  }
6831
7006
  }
6832
7007
  } catch {
@@ -6835,20 +7010,20 @@ function globalSessionLoad(idOrPrefix) {
6835
7010
  }
6836
7011
  function globalSessionList() {
6837
7012
  const gd = globalDir();
6838
- if (!existsSync5(gd)) return [];
7013
+ if (!existsSync6(gd)) return [];
6839
7014
  const metas = [];
6840
7015
  for (const f of readdirSync(gd)) {
6841
7016
  if (!f.endsWith(".json")) continue;
6842
7017
  try {
6843
7018
  const target = readlinkSync(join6(gd, f));
6844
- if (!existsSync5(target)) {
7019
+ if (!existsSync6(target)) {
6845
7020
  try {
6846
7021
  unlinkSync(join6(gd, f));
6847
7022
  } catch {
6848
7023
  }
6849
7024
  continue;
6850
7025
  }
6851
- metas.push(JSON.parse(readFileSync3(target, "utf8")).meta);
7026
+ metas.push(JSON.parse(readFileSync4(target, "utf8")).meta);
6852
7027
  } catch {
6853
7028
  }
6854
7029
  }
@@ -6873,7 +7048,7 @@ var CheckpointStack = class {
6873
7048
  current;
6874
7049
  /** Open a new turn frame (call right before sending a user turn). */
6875
7050
  begin(label) {
6876
- this.current = { label: label.replace(/\s+/g, " ").trim().slice(0, 60) || "(turn)", at: Date.now(), saved: /* @__PURE__ */ new Map() };
7051
+ this.current = { label: sanitizeLabel(label) || "(turn)", at: Date.now(), saved: /* @__PURE__ */ new Map() };
6877
7052
  this.frames.push(this.current);
6878
7053
  if (this.frames.length > this.max) this.frames.shift();
6879
7054
  }
@@ -6901,6 +7076,24 @@ var CheckpointStack = class {
6901
7076
  list() {
6902
7077
  return this.frames.map((f, i) => ({ index: i, label: f.label, at: f.at, files: f.saved.size })).reverse();
6903
7078
  }
7079
+ /** Unified-style diff of all session edits: each file's OLDEST saved content vs its current content. */
7080
+ async diff() {
7081
+ const base = /* @__PURE__ */ new Map();
7082
+ for (const f of this.frames) for (const [path, prior] of f.saved) if (!base.has(path)) base.set(path, prior);
7083
+ const parts = [];
7084
+ for (const [path, prior] of base) {
7085
+ let now5 = null;
7086
+ try {
7087
+ now5 = await this.fs.readFile(path);
7088
+ } catch {
7089
+ }
7090
+ if ((prior ?? "") === (now5 ?? "")) continue;
7091
+ const ops = diffLines(prior ?? "", now5 ?? "");
7092
+ parts.push(`--- ${path}${prior == null ? " (new)" : now5 == null ? " (deleted)" : ""}
7093
+ ${formatDiff(ops)}`);
7094
+ }
7095
+ return parts.join("\n");
7096
+ }
6904
7097
  /**
6905
7098
  * Restore the working tree to BEFORE frame `index` — undo that frame and every later one.
6906
7099
  * Frames are replayed newest→oldest so the OLDEST saved content for a path wins (its true
@@ -6930,12 +7123,12 @@ var CheckpointStack = class {
6930
7123
  };
6931
7124
 
6932
7125
  // cli/gitCheckpoints.ts
6933
- import { execFile } from "child_process";
7126
+ import { execFile as execFile3 } from "child_process";
6934
7127
  import { promisify } from "util";
6935
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
7128
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
6936
7129
  import { join as join7, resolve as resolve2, sep as sep2 } from "path";
6937
- var log16 = forComponent("checkpoints");
6938
- var exec = promisify(execFile);
7130
+ var log18 = forComponent("checkpoints");
7131
+ var exec = promisify(execFile3);
6939
7132
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
6940
7133
  var ShadowRepo = class {
6941
7134
  // undefined = unprobed; false = git/this root unusable
@@ -6961,14 +7154,14 @@ var ShadowRepo = class {
6961
7154
  if (this.ready !== void 0) return this.ready;
6962
7155
  try {
6963
7156
  await exec(this.git, ["--version"]);
6964
- if (!existsSync6(this.gitDir)) {
7157
+ if (!existsSync7(this.gitDir)) {
6965
7158
  mkdirSync5(this.gitDir, { recursive: true });
6966
7159
  await this.run("init", "-q");
6967
7160
  }
6968
7161
  writeFileSync4(join7(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
6969
7162
  this.ready = true;
6970
7163
  } catch (e) {
6971
- log16.debug(`git checkpoints unavailable for ${this.workTree}`, e);
7164
+ log18.debug(`git checkpoints unavailable for ${this.workTree}`, e);
6972
7165
  this.ready = false;
6973
7166
  }
6974
7167
  return this.ready;
@@ -7001,6 +7194,14 @@ var ShadowRepo = class {
7001
7194
  return 0;
7002
7195
  }
7003
7196
  }
7197
+ /** Unified diff between `sha` and the current work-tree (for `/diff`). */
7198
+ async diffSince(sha) {
7199
+ try {
7200
+ return await this.run("diff", sha);
7201
+ } catch {
7202
+ return "";
7203
+ }
7204
+ }
7004
7205
  /** Restore the tree to `sha`; returns counts of reverted tracked files + removed untracked-new. */
7005
7206
  async resetTo(sha) {
7006
7207
  let restored = 0, deleted = 0;
@@ -7031,7 +7232,7 @@ var ShadowRepo = class {
7031
7232
  await this.run("gc", "--auto", "-q").catch(() => {
7032
7233
  });
7033
7234
  } catch (e) {
7034
- log16.debug("checkpoint prune failed", e);
7235
+ log18.debug("checkpoint prune failed", e);
7035
7236
  }
7036
7237
  }
7037
7238
  };
@@ -7088,18 +7289,18 @@ var GitCheckpoints = class {
7088
7289
  use(sessionId) {
7089
7290
  if (sessionId === this.session) return;
7090
7291
  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));
7292
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log18.debug("re-point failed", e));
7092
7293
  }
7093
7294
  async begin(label) {
7094
7295
  if (!await this.start()) return;
7095
- const msg = label.replace(/\s+/g, " ").trim().slice(0, 72) || "(turn)";
7296
+ const msg = sanitizeLabel(label, 72) || "(turn)";
7096
7297
  let slow;
7097
7298
  if (!this.snapshotted) slow = setTimeout(() => process.stderr.write("\x1B[2m checkpointing initial workspace snapshot\u2026\x1B[0m\n"), 1500);
7098
7299
  for (const r of this.repos) {
7099
7300
  try {
7100
7301
  await r.commit(msg);
7101
7302
  } catch (e) {
7102
- log16.debug("checkpoint commit failed", e);
7303
+ log18.debug("checkpoint commit failed", e);
7103
7304
  }
7104
7305
  }
7105
7306
  if (slow) clearTimeout(slow);
@@ -7123,6 +7324,16 @@ var GitCheckpoints = class {
7123
7324
  get size() {
7124
7325
  return this.caches[0]?.length ?? 0;
7125
7326
  }
7327
+ /** Unified diff of everything this session changed: oldest session checkpoint → current tree, across all roots. */
7328
+ async diff() {
7329
+ if (!this.caches[0]?.length) await this.refresh();
7330
+ const parts = [];
7331
+ for (let i = 0; i < this.repos.length; i++) {
7332
+ const base = this.caches[i]?.[0];
7333
+ if (base) parts.push(await this.repos[i].diffSince(base.sha));
7334
+ }
7335
+ return parts.filter(Boolean).join("\n");
7336
+ }
7126
7337
  async rewindTo(index) {
7127
7338
  if (!this.caches[0]?.length) await this.refresh();
7128
7339
  if (index < 0 || index >= (this.caches[0]?.length ?? 0)) throw new Error("no such checkpoint");
@@ -7156,8 +7367,8 @@ var GitCheckpointsOptions = class {
7156
7367
  };
7157
7368
 
7158
7369
  // 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";
7370
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
7371
+ import { homedir as homedir6 } from "os";
7161
7372
  import { join as join8 } from "path";
7162
7373
  var RULE_RE = /^(\w+)(?:\((.+)\))?$/;
7163
7374
  function parseOne(raw, decision) {
@@ -7182,25 +7393,15 @@ function describeRule(r) {
7182
7393
  }
7183
7394
  var PERM_FILE = (cwd) => join8(cwd, ".agent", "permissions.json");
7184
7395
  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
- }
7396
+ const j = readJsonFile(PERM_FILE(cwd), null);
7397
+ return j ? { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] } : {};
7193
7398
  }
7194
- function loadClaudeSettings(cwd, home = homedir5()) {
7399
+ function loadClaudeSettings(cwd, home = homedir6()) {
7195
7400
  const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
7196
7401
  let out = {};
7197
7402
  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
- }
7403
+ const perms = readJsonFile(p, null)?.permissions;
7404
+ if (perms) out = mergePerms(out, { allow: perms.allow, ask: perms.ask, deny: perms.deny }) ?? out;
7204
7405
  }
7205
7406
  return out;
7206
7407
  }
@@ -7223,20 +7424,14 @@ function mergePerms(a, b) {
7223
7424
  }
7224
7425
  return Object.keys(out).length ? out : void 0;
7225
7426
  }
7226
- var TRUST_FILE = join8(homedir5(), ".agent", "trusted.json");
7427
+ var TRUST_FILE = join8(homedir6(), ".agent", "trusted.json");
7227
7428
  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
- }
7429
+ const list = readJsonFile(file, []);
7430
+ return Array.isArray(list) && list.includes(cwd);
7233
7431
  }
7234
7432
  function trustDir(cwd, file = TRUST_FILE) {
7235
- let list = [];
7236
- try {
7237
- if (existsSync7(file)) list = JSON.parse(readFileSync4(file, "utf8"));
7238
- } catch {
7239
- }
7433
+ let list = readJsonFile(file, []);
7434
+ if (!Array.isArray(list)) list = [];
7240
7435
  if (!list.includes(cwd)) list.push(cwd);
7241
7436
  try {
7242
7437
  mkdirSync6(join8(file, ".."), { recursive: true });
@@ -7291,14 +7486,18 @@ function completePath(listDir, ref) {
7291
7486
  for (const e of entries) {
7292
7487
  if (!fuzzy(base, e.name)) continue;
7293
7488
  if (e.name.startsWith(".") && !base.startsWith(".")) continue;
7294
- const rel = dir ? `${dir}/${e.name}` : e.name;
7295
- matched.push("@" + rel + (e.dir ? "/" : ""));
7489
+ const rel = (dir ? `${dir}/${e.name}` : e.name) + (e.dir ? "/" : "");
7490
+ matched.push(/\s/.test(rel) ? `@"${rel}"` : "@" + rel);
7296
7491
  }
7297
7492
  return rank(matched, "@" + (dir ? dir + "/" : "") + base);
7298
7493
  }
7299
7494
 
7300
7495
  // cli/lineEditor.ts
7301
7496
  import { emitKeypressEvents } from "readline";
7497
+ import { spawnSync as spawnSync3 } from "child_process";
7498
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
7499
+ import { tmpdir as tmpdir2 } from "os";
7500
+ import { join as join9 } from "path";
7302
7501
 
7303
7502
  // cli/bidi.ts
7304
7503
  var RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
@@ -8138,6 +8337,30 @@ function createLineEditor(out) {
8138
8337
  if (cursorCol > 0) out.write(`\x1B[${cursorCol}C`);
8139
8338
  curRow = cursorRow;
8140
8339
  }
8340
+ function externalEdit(s) {
8341
+ const spec = process.env.VISUAL || process.env.EDITOR || "vi";
8342
+ const [cmd, ...cargs] = spec.split(" ").filter(Boolean);
8343
+ const file = join9(tmpdir2(), `agentx-edit-${process.pid}-${Date.now()}.md`);
8344
+ try {
8345
+ writeFileSync6(file, s.buf);
8346
+ process.stdin.setRawMode(false);
8347
+ out.write("\x1B[?2004l");
8348
+ const r = spawnSync3(cmd, [...cargs, file], { stdio: "inherit" });
8349
+ if (r.status === 0) {
8350
+ const text = readFileSync5(file, "utf8").replace(/\n$/, "");
8351
+ s.reset();
8352
+ if (text) s.insert(text);
8353
+ }
8354
+ } catch {
8355
+ } finally {
8356
+ try {
8357
+ unlinkSync2(file);
8358
+ } catch {
8359
+ }
8360
+ process.stdin.setRawMode(true);
8361
+ out.write("\x1B[?2004h");
8362
+ }
8363
+ }
8141
8364
  async function readLine(opts) {
8142
8365
  const maxVisible = opts.maxVisible ?? 8;
8143
8366
  if (!isTTY) return readPlainLine();
@@ -8174,6 +8397,7 @@ function createLineEditor(out) {
8174
8397
  lastStatus = cur;
8175
8398
  redraw();
8176
8399
  }, opts.statusTickMs) : void 0;
8400
+ let chordCtrlX = false;
8177
8401
  const onKey = (str, key) => {
8178
8402
  if (key?.ctrl && key.name === "l") {
8179
8403
  out.write("\x1B[2J\x1B[3J\x1B[H");
@@ -8181,6 +8405,19 @@ function createLineEditor(out) {
8181
8405
  redraw();
8182
8406
  return;
8183
8407
  }
8408
+ if (key?.ctrl && key.name === "x" && !s.pasting) {
8409
+ chordCtrlX = true;
8410
+ return;
8411
+ }
8412
+ if (chordCtrlX) {
8413
+ chordCtrlX = false;
8414
+ if (key?.ctrl && key.name === "e") {
8415
+ externalEdit(s);
8416
+ curRow = 0;
8417
+ redraw();
8418
+ return;
8419
+ }
8420
+ }
8184
8421
  if (key?.name === "tab" && key.shift && opts.onCyclePosture) {
8185
8422
  opts.onCyclePosture();
8186
8423
  redraw();
@@ -8686,6 +8923,184 @@ var MarkdownStream = class {
8686
8923
  }
8687
8924
  };
8688
8925
 
8926
+ // cli/osScheduler.ts
8927
+ import { spawnSync as spawnSync4 } from "child_process";
8928
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync8 } from "fs";
8929
+ import { homedir as homedir7 } from "os";
8930
+ import { join as join10 } from "path";
8931
+ var log19 = forComponent("os-sched");
8932
+ var OsScheduler = class {
8933
+ options;
8934
+ constructor(options) {
8935
+ this.options = { ...new OsSchedulerOptions(), ...options };
8936
+ }
8937
+ get dir() {
8938
+ return join10(this.options.home, ".agent", "sched");
8939
+ }
8940
+ label(id) {
8941
+ return `cc.livx.agentx.sched-${id}`;
8942
+ }
8943
+ plistPath(id) {
8944
+ return join10(this.options.home, "Library", "LaunchAgents", `${this.label(id)}.plist`);
8945
+ }
8946
+ run(cmd, args, input) {
8947
+ return this.options.exec(cmd, args, input);
8948
+ }
8949
+ available() {
8950
+ return this.options.platform === "darwin" || this.options.platform === "linux";
8951
+ }
8952
+ /** Register the job with the OS. Returns a human description of the mechanism used. Throws on failure. */
8953
+ schedule(spec) {
8954
+ if (!this.available()) throw new Error(`no OS scheduler on ${this.options.platform}`);
8955
+ mkdirSync7(this.dir, { recursive: true });
8956
+ const oneOff = "at" in spec.trigger;
8957
+ const script = this.writeScript(spec, oneOff);
8958
+ const mechanism = this.options.platform === "darwin" ? this.scheduleDarwin(spec, script) : this.scheduleLinux(spec, script, oneOff);
8959
+ const meta = { ...spec, created: Date.now(), mechanism };
8960
+ writeFileSync7(join10(this.dir, `${spec.id}.json`), JSON.stringify(meta, null, 2));
8961
+ return mechanism;
8962
+ }
8963
+ cancel(id) {
8964
+ const meta = readJsonFile(join10(this.dir, `${id}.json`), null);
8965
+ if (!meta) return false;
8966
+ try {
8967
+ if (this.options.platform === "darwin") {
8968
+ try {
8969
+ this.run("launchctl", ["remove", this.label(id)]);
8970
+ } catch {
8971
+ }
8972
+ try {
8973
+ unlinkSync3(this.plistPath(id));
8974
+ } catch {
8975
+ }
8976
+ } else if (meta.mechanism.startsWith("crontab")) {
8977
+ const cur = (() => {
8978
+ try {
8979
+ return this.run("crontab", ["-l"]);
8980
+ } catch {
8981
+ return "";
8982
+ }
8983
+ })();
8984
+ const next = cur.split("\n").filter((l) => !l.includes(`# agentx-sched-${id}`)).join("\n");
8985
+ this.run("crontab", ["-"], next.trim() ? next.trimEnd() + "\n" : "");
8986
+ } else if (meta.mechanism.startsWith("at:")) {
8987
+ try {
8988
+ this.run("atrm", [meta.mechanism.slice(3)]);
8989
+ } catch {
8990
+ }
8991
+ }
8992
+ } catch (e) {
8993
+ log19.debug(`cancel ${id}`, e);
8994
+ }
8995
+ for (const f of [`${id}.json`, `${id}.sh`]) {
8996
+ try {
8997
+ unlinkSync3(join10(this.dir, f));
8998
+ } catch {
8999
+ }
9000
+ }
9001
+ return true;
9002
+ }
9003
+ list() {
9004
+ if (!existsSync8(this.dir)) return [];
9005
+ return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(join10(this.dir, f), null)).filter(Boolean);
9006
+ }
9007
+ /** The per-job runner script: cd to the project, headless-resume the session, log, notify. */
9008
+ writeScript(spec, oneOff) {
9009
+ const p = join10(this.dir, `${spec.id}.sh`);
9010
+ const q2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
9011
+ 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)}
9012
+ ` : `rm -f ${q2(join10(this.dir, `${spec.id}.json`))} ${q2(p)}
9013
+ ` : "";
9014
+ writeFileSync7(p, `#!/bin/sh
9015
+ # agentx scheduled job ${spec.id}${spec.label ? ` \u2014 ${spec.label}` : ""}
9016
+ cd ${q2(spec.cwd)} || exit 1
9017
+ ${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(join10(this.dir, `${spec.id}.log`))} 2>&1
9018
+ ${cleanup}`);
9019
+ chmodSync(p, 493);
9020
+ return p;
9021
+ }
9022
+ scheduleDarwin(spec, script) {
9023
+ const t = spec.trigger;
9024
+ let trigger;
9025
+ if ("everyMs" in t) {
9026
+ trigger = `<key>StartInterval</key><integer>${Math.max(60, Math.round(t.everyMs / 1e3))}</integer>`;
9027
+ } else if ("at" in t) {
9028
+ const d = new Date(t.at);
9029
+ 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>`;
9030
+ } else {
9031
+ const f = parseCron(t.cron);
9032
+ const dict = [];
9033
+ const put = (key, vals, full) => {
9034
+ if (vals.length === full) return;
9035
+ 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`);
9036
+ dict.push(`<key>${key}</key><integer>${vals[0]}</integer>`);
9037
+ };
9038
+ put("Minute", f.minute, 60);
9039
+ put("Hour", f.hour, 24);
9040
+ put("Day", f.dom, 31);
9041
+ put("Month", f.month, 12);
9042
+ put("Weekday", f.dow, 7);
9043
+ trigger = `<key>StartCalendarInterval</key><dict>${dict.join("")}</dict>`;
9044
+ }
9045
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
9046
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9047
+ <plist version="1.0"><dict>
9048
+ <key>Label</key><string>${this.label(spec.id)}</string>
9049
+ <key>ProgramArguments</key><array><string>/bin/sh</string><string>${script}</string></array>
9050
+ ${trigger}
9051
+ <key>RunAtLoad</key><false/>
9052
+ </dict></plist>
9053
+ `;
9054
+ mkdirSync7(join10(this.options.home, "Library", "LaunchAgents"), { recursive: true });
9055
+ writeFileSync7(this.plistPath(spec.id), plist);
9056
+ this.run("launchctl", ["load", this.plistPath(spec.id)]);
9057
+ return `launchd:${this.label(spec.id)}`;
9058
+ }
9059
+ scheduleLinux(spec, script, oneOff) {
9060
+ const t = spec.trigger;
9061
+ if (oneOff) {
9062
+ const d = new Date("at" in t ? t.at : Date.now());
9063
+ 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")}`;
9064
+ const out = this.run("at", [stamp], `/bin/sh ${script}
9065
+ `);
9066
+ const jobId = /job (\d+)/.exec(out)?.[1] ?? "";
9067
+ return `at:${jobId}`;
9068
+ }
9069
+ const expr = "cron" in t ? t.cron : `*/${Math.max(1, Math.round(t.everyMs / 6e4))} * * * *`;
9070
+ parseCron(expr);
9071
+ const cur = (() => {
9072
+ try {
9073
+ return this.run("crontab", ["-l"]);
9074
+ } catch {
9075
+ return "";
9076
+ }
9077
+ })();
9078
+ this.run("crontab", ["-"], `${cur.trimEnd()}
9079
+ ${expr} /bin/sh ${script} # agentx-sched-${spec.id}
9080
+ `.replace(/^\n/, ""));
9081
+ return `crontab:${expr}`;
9082
+ }
9083
+ };
9084
+ var OsSchedulerOptions = class {
9085
+ platform = process.platform;
9086
+ home = homedir7();
9087
+ /** How the fired job invokes the CLI — a RAW shell snippet (may be `bun /path/cli.ts`). Default: `agentx` on PATH. */
9088
+ agentx = "agentx";
9089
+ /** Injectable executor for tests. Returns stdout+stderr merged — `at` reports "job N" on STDERR,
9090
+ * and the job id is what atrm needs for cancel. Throws on non-zero exit. */
9091
+ exec = (cmd, args, input) => {
9092
+ const r = spawnSync4(cmd, args, { input, encoding: "utf8" });
9093
+ if (r.error) throw r.error;
9094
+ if (r.status !== 0) throw new Error(`${cmd} exited ${r.status}: ${r.stderr || r.stdout}`);
9095
+ return `${r.stdout ?? ""}${r.stderr ?? ""}`;
9096
+ };
9097
+ };
9098
+ function routeTrigger(trigger, backendHint, now5 = Date.now()) {
9099
+ if (backendHint === "os") return "os";
9100
+ if (backendHint === "session") return "session";
9101
+ return "at" in trigger && trigger.at - now5 >= 30 * 6e4 ? "os" : "session";
9102
+ }
9103
+
8689
9104
  // cli/cli.ts
8690
9105
  var forceColor = process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "" && process.env.FORCE_COLOR !== "0";
8691
9106
  var useColor = forceColor || !process.env.NO_COLOR && !!process.stdout.isTTY && !!process.stderr.isTTY;
@@ -8701,10 +9116,10 @@ var italic = C("3");
8701
9116
  var strike = C("9");
8702
9117
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
8703
9118
  var err = (s) => process.stderr.write(s);
8704
- var log17 = forComponent("cli");
9119
+ var log20 = forComponent("cli");
8705
9120
  var VERSION = (() => {
8706
9121
  try {
8707
- return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
9122
+ return JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
8708
9123
  } catch {
8709
9124
  return "?";
8710
9125
  }
@@ -8905,7 +9320,7 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
8905
9320
  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)
8906
9321
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8907
9322
  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.
8908
- 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.
9323
+ 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.
8909
9324
  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.
8910
9325
  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.
8911
9326
  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).`;
@@ -8925,11 +9340,11 @@ function resolveModelOrNewest(model) {
8925
9340
  var ENV_KEY_ALIASES = { google: ["GEMINI_API_KEY"] };
8926
9341
  function loadInstallEnv() {
8927
9342
  let dir = dirname4(import.meta.path);
8928
- for (let i = 0; i < 5 && !existsSync8(join9(dir, "package.json")); i++) dir = dirname4(dir);
9343
+ for (let i = 0; i < 5 && !existsSync9(join11(dir, "package.json")); i++) dir = dirname4(dir);
8929
9344
  for (const name of [".env", ".env.local"]) {
8930
- const file = join9(dir, name);
8931
- if (!existsSync8(file)) continue;
8932
- for (const line of readFileSync5(file, "utf8").split("\n")) {
9345
+ const file = join11(dir, name);
9346
+ if (!existsSync9(file)) continue;
9347
+ for (const line of readFileSync6(file, "utf8").split("\n")) {
8933
9348
  const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
8934
9349
  if (!m || m[1] in process.env) continue;
8935
9350
  let val = m[2].trim();
@@ -9237,7 +9652,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
9237
9652
  function turnCost(model, usage) {
9238
9653
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
9239
9654
  }
9240
- async function evaluateGoal(ai, condition, transcript, log18) {
9655
+ async function evaluateGoal(ai, condition, transcript, log21) {
9241
9656
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
9242
9657
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
9243
9658
  return text.slice(0, 600);
@@ -9257,7 +9672,7 @@ ${recent}` }
9257
9672
  const match = r.content.match(/\{[\s\S]*\}/);
9258
9673
  if (match) return JSON.parse(match[0]);
9259
9674
  } catch (e) {
9260
- log18(dim(` (goal evaluator error: ${e?.message ?? e})
9675
+ log21(dim(` (goal evaluator error: ${e?.message ?? e})
9261
9676
  `));
9262
9677
  }
9263
9678
  return { met: false, reason: "evaluation unclear" };
@@ -9444,19 +9859,22 @@ async function mountMcp(cfg, oauth) {
9444
9859
  return mounted;
9445
9860
  }
9446
9861
  async function closeMcp(mounted) {
9447
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log17.debug("mcp close failed", e))));
9862
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log20.debug("mcp close failed", e))));
9448
9863
  }
9449
9864
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
9450
- var untilde = (p) => p.startsWith("~/") ? join9(homedir6(), p.slice(2)) : p;
9865
+ function mentionRefs(line) {
9866
+ return [...line.matchAll(/(?:^|\s)@(?:"([^"]+)"|(\S+))/g)].map((m) => m[1] ?? m[2].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9867
+ }
9868
+ var untilde = (p) => p.startsWith("~/") ? join11(homedir8(), p.slice(2)) : p;
9451
9869
  function readImageParts(cwd, line) {
9452
- const refs = [...line.matchAll(/(?:^|\s)@(\S+)/g)].map((m) => m[1].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9870
+ const refs = mentionRefs(line);
9453
9871
  const parts = [];
9454
9872
  for (const ref of refs) {
9455
9873
  const mime = IMG_EXT[extname(ref).toLowerCase()];
9456
9874
  if (!mime) continue;
9457
9875
  const abs = ref.startsWith("~/") ? untilde(ref) : resolve3(cwd, ref);
9458
9876
  try {
9459
- parts.push(imagePart(`data:${mime};base64,${readFileSync5(abs).toString("base64")}`));
9877
+ parts.push(imagePart(`data:${mime};base64,${readFileSync6(abs).toString("base64")}`));
9460
9878
  } catch {
9461
9879
  }
9462
9880
  }
@@ -9467,7 +9885,6 @@ function pastePathClassifier(cwd) {
9467
9885
  let t = text.trim();
9468
9886
  if (!t || t.includes("\n")) return null;
9469
9887
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
9470
- if (/\s/.test(t)) return null;
9471
9888
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
9472
9889
  const abs = t.startsWith("~/") ? untilde(t) : resolve3(cwd, t);
9473
9890
  try {
@@ -9476,7 +9893,7 @@ function pastePathClassifier(cwd) {
9476
9893
  return null;
9477
9894
  }
9478
9895
  const isImg = !!IMG_EXT[extname(abs).toLowerCase()];
9479
- return { display: isImg ? "Image" : `File ${basename2(abs)}`, ref: "@" + abs };
9896
+ return { display: isImg ? "Image" : `File ${basename2(abs)}`, ref: /\s/.test(abs) ? `@"${abs}"` : "@" + abs };
9480
9897
  };
9481
9898
  }
9482
9899
  var mcpMentionResolver;
@@ -9484,7 +9901,7 @@ function setMcpMentionResolver(fn) {
9484
9901
  mcpMentionResolver = fn;
9485
9902
  }
9486
9903
  async function expandMentions(fs, line) {
9487
- const refs = [...line.matchAll(/(?:^|\s)@(\S+)/g)].map((m) => m[1].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9904
+ const refs = mentionRefs(line);
9488
9905
  if (!refs.length) return { text: line, loaded: [], missing: [] };
9489
9906
  const loaded = [], missing = [], blocks = [];
9490
9907
  for (const ref of refs) {
@@ -9492,7 +9909,7 @@ async function expandMentions(fs, line) {
9492
9909
  if (loaded.includes(ref) || missing.includes(ref)) continue;
9493
9910
  if (ref.includes(":") && mcpMentionResolver) {
9494
9911
  const body = await mcpMentionResolver(ref).catch((e) => {
9495
- log17.debug("mcp mention resolve failed", e);
9912
+ log20.debug("mcp mention resolve failed", e);
9496
9913
  return null;
9497
9914
  });
9498
9915
  if (body != null) {
@@ -9670,25 +10087,25 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
9670
10087
  `;
9671
10088
  function initInstructions(cwd) {
9672
10089
  for (const f of ["AGENTS.md", "CLAUDE.md"]) {
9673
- if (existsSync8(join9(cwd, f))) {
10090
+ if (existsSync9(join11(cwd, f))) {
9674
10091
  err(yellow(` ${f} already exists \u2014 leaving it as-is
9675
10092
  `));
9676
10093
  return;
9677
10094
  }
9678
10095
  }
9679
- const path = join9(cwd, "AGENTS.md");
9680
- writeFileSync6(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
10096
+ const path = join11(cwd, "AGENTS.md");
10097
+ writeFileSync8(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
9681
10098
  err(green(` created ${path}
9682
10099
  `) + dim(" edit it, then it auto-loads into every run.\n"));
9683
10100
  }
9684
10101
  function persistSetting(cwd, key, value) {
9685
- const path = join9(cwd, ".agent", "settings.json");
10102
+ const path = join11(cwd, ".agent", "settings.json");
9686
10103
  try {
9687
- const obj = existsSync8(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
10104
+ const obj = existsSync9(path) ? JSON.parse(readFileSync6(path, "utf8")) : {};
9688
10105
  if (obj[key] === value) return;
9689
10106
  obj[key] = value;
9690
- mkdirSync7(dirname4(path), { recursive: true });
9691
- writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
10107
+ mkdirSync8(dirname4(path), { recursive: true });
10108
+ writeFileSync8(path, JSON.stringify(obj, null, 2) + "\n");
9692
10109
  } catch (e) {
9693
10110
  err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
9694
10111
  `));
@@ -9704,14 +10121,14 @@ var isCancelTeardown = (e) => {
9704
10121
  function installCancelGuards(mounted) {
9705
10122
  process.on("unhandledRejection", (e) => {
9706
10123
  if (isCancelTeardown(e)) {
9707
- log17.debug("suppressed unhandledRejection (cursor stream cancel)", e);
10124
+ log20.debug("suppressed unhandledRejection (cursor stream cancel)", e);
9708
10125
  return;
9709
10126
  }
9710
- log17.error("unhandledRejection", e);
10127
+ log20.error("unhandledRejection", e);
9711
10128
  });
9712
10129
  process.on("uncaughtException", (e) => {
9713
10130
  if (isCancelTeardown(e)) {
9714
- log17.debug("suppressed uncaughtException (cursor stream cancel)", e);
10131
+ log20.debug("suppressed uncaughtException (cursor stream cancel)", e);
9715
10132
  return;
9716
10133
  }
9717
10134
  console.error(e);
@@ -9720,7 +10137,7 @@ function installCancelGuards(mounted) {
9720
10137
  });
9721
10138
  }
9722
10139
  async function repl(args, ai, cfg, cwd) {
9723
- const oauth = new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") });
10140
+ const oauth = new McpOAuth({ storePath: join11(cwd, ".agent", "mcp-auth.json") });
9724
10141
  const mounted = await mountMcp(cfg, oauth);
9725
10142
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
9726
10143
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
@@ -9740,7 +10157,19 @@ async function repl(args, ai, cfg, cwd) {
9740
10157
  },
9741
10158
  tickMs: 15e3
9742
10159
  });
9743
- agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler)];
10160
+ const agentxInvocation = () => process.argv[1] ? `${process.execPath} ${resolve3(process.argv[1])}` : "agentx";
10161
+ const osSched = new OsScheduler({ agentx: agentxInvocation() });
10162
+ const osBackend = osSched.available() ? {
10163
+ get sessionId() {
10164
+ return session.meta.id;
10165
+ },
10166
+ cwd,
10167
+ route: (t, hint) => routeTrigger(t, hint),
10168
+ schedule: (spec) => osSched.schedule(spec),
10169
+ cancel: (id) => osSched.cancel(id),
10170
+ list: () => osSched.list()
10171
+ } : void 0;
10172
+ agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler, osBackend)];
9744
10173
  const duplex = args.duplex;
9745
10174
  let dx;
9746
10175
  let voiceIO;
@@ -9902,7 +10331,7 @@ async function repl(args, ai, cfg, cwd) {
9902
10331
  quickLook: {
9903
10332
  branch: () => {
9904
10333
  try {
9905
- const head = readFileSync5(join9(cwd, ".git", "HEAD"), "utf8").trim();
10334
+ const head = readFileSync6(join11(cwd, ".git", "HEAD"), "utf8").trim();
9906
10335
  return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
9907
10336
  } catch {
9908
10337
  return "not a git repository";
@@ -10024,9 +10453,9 @@ async function repl(args, ai, cfg, cwd) {
10024
10453
  };
10025
10454
  const pendingImages = [];
10026
10455
  const grabClipboardAttachment = () => {
10027
- const dir = join9(tmpdir2(), "agentx-pasted");
10456
+ const dir = join11(tmpdir3(), "agentx-pasted");
10028
10457
  try {
10029
- mkdirSync7(dir, { recursive: true });
10458
+ mkdirSync8(dir, { recursive: true });
10030
10459
  } catch {
10031
10460
  }
10032
10461
  const img = grabClipboardImage(dir, String(Date.now()));
@@ -10076,7 +10505,7 @@ async function repl(args, ai, cfg, cwd) {
10076
10505
  err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
10077
10506
  `));
10078
10507
  }
10079
- 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 });
10508
+ 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 });
10080
10509
  const cpHooks = checkpoints.hooks?.();
10081
10510
  if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
10082
10511
  duplexPersist = () => {
@@ -10103,7 +10532,7 @@ async function repl(args, ai, cfg, cwd) {
10103
10532
  const fs = agent.options.fs;
10104
10533
  const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
10105
10534
  const adot = (sub) => `${fsBase}/.agent/${sub}`;
10106
- const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir6()}/.agent/${sub}`, `${homedir6()}/.claude/${sub}`];
10535
+ const adots = (sub) => dotDirs(fsBase, sub);
10107
10536
  const cmds = (await loadCommands(fs, adots("commands"))).commands;
10108
10537
  const skills = (await loadSkills(fs, adots("skills"))).skills;
10109
10538
  const refreshCatalogs = async () => {
@@ -10125,14 +10554,14 @@ ${lines.join("\n")}
10125
10554
  Added entries are loadable now via the Skill/SlashCommand tools; removed ones are gone even if still listed in the system prompt.
10126
10555
  </system-reminder>`;
10127
10556
  };
10128
- const histPath = join9(cwd, ".agent", "history");
10129
- const history = existsSync8(histPath) ? readFileSync5(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
10557
+ const histPath = join11(cwd, ".agent", "history");
10558
+ const history = existsSync9(histPath) ? readFileSync6(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
10130
10559
  const remember = (line) => {
10131
10560
  try {
10132
- mkdirSync7(join9(cwd, ".agent"), { recursive: true });
10561
+ mkdirSync8(join11(cwd, ".agent"), { recursive: true });
10133
10562
  appendFileSync(histPath, line + "\n");
10134
10563
  } catch (e) {
10135
- log17.debug("history write failed", e);
10564
+ log20.debug("history write failed", e);
10136
10565
  }
10137
10566
  };
10138
10567
  const ago = (t) => {
@@ -10202,7 +10631,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
10202
10631
  try {
10203
10632
  store.save(session);
10204
10633
  } catch (e) {
10205
- log17.debug("session save after rewind failed", e);
10634
+ log20.debug("session save after rewind failed", e);
10206
10635
  }
10207
10636
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
10208
10637
  `));
@@ -10230,7 +10659,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
10230
10659
  const announcedTasks = /* @__PURE__ */ new Set();
10231
10660
  const turn = async (task) => {
10232
10661
  const delta = await refreshCatalogs().catch((e) => {
10233
- log17.debug("catalog refresh failed", e);
10662
+ log20.debug("catalog refresh failed", e);
10234
10663
  return "";
10235
10664
  });
10236
10665
  if (delta) {
@@ -10425,8 +10854,8 @@ ${extra}` : body);
10425
10854
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10426
10855
  if (wasRaw) process.stdin.setRawMode(false);
10427
10856
  try {
10428
- const { spawnSync: spawnSync3 } = await import("child_process");
10429
- const r = spawnSync3("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
10857
+ const { spawnSync: spawnSync5 } = await import("child_process");
10858
+ const r = spawnSync5("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
10430
10859
  if (r.error) err(text);
10431
10860
  } finally {
10432
10861
  if (wasRaw) process.stdin.setRawMode(true);
@@ -10446,13 +10875,13 @@ ${extra}` : body);
10446
10875
  keys.length ? ok(`provider keys: ${keys.join(", ")}`) : bad("no provider keys set (ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY)");
10447
10876
  const info = getModelInfo(work.model);
10448
10877
  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)`);
10449
- const cfgFiles = ["ts", "js", "json"].flatMap((e) => [`${cwd}/.agent/config.${e}`, `${homedir6()}/.agent/config.${e}`]).filter((p) => existsSync8(p));
10878
+ const cfgFiles = ["ts", "js", "json"].flatMap((e) => [`${cwd}/.agent/config.${e}`, `${homedir8()}/.agent/config.${e}`]).filter((p) => existsSync9(p));
10450
10879
  cfgFiles.length ? ok(`config: ${cfgFiles.join(", ")}`) : warn("no .agent/config.* found (project or ~) \u2014 running on defaults");
10451
10880
  try {
10452
10881
  const probe = `${cwd}/.agent/sessions/.doctor-probe`;
10453
- mkdirSync7(`${cwd}/.agent/sessions`, { recursive: true });
10454
- writeFileSync6(probe, "ok");
10455
- unlinkSync2(probe);
10882
+ mkdirSync8(`${cwd}/.agent/sessions`, { recursive: true });
10883
+ writeFileSync8(probe, "ok");
10884
+ unlinkSync4(probe);
10456
10885
  ok(`session store writable (${cwd}/.agent/sessions)`);
10457
10886
  } catch (e) {
10458
10887
  bad(`session store not writable: ${e?.message ?? e}`);
@@ -10479,7 +10908,7 @@ ${extra}` : body);
10479
10908
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
10480
10909
  run: async () => {
10481
10910
  await refreshCatalogs().catch((e) => {
10482
- log17.debug("catalog refresh failed", e);
10911
+ log20.debug("catalog refresh failed", e);
10483
10912
  });
10484
10913
  face.reprepare();
10485
10914
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -10745,6 +11174,47 @@ ${extra}` : body);
10745
11174
  `));
10746
11175
  }
10747
11176
  },
11177
+ copy: {
11178
+ desc: "copy the last reply to the clipboard \u2014 /copy code = last code block only",
11179
+ run: (a) => {
11180
+ const last = [...face.transcript].reverse().find((m) => m.role === "assistant" && contentText(m.content).trim());
11181
+ if (!last) {
11182
+ err(dim(" (nothing to copy yet)\n"));
11183
+ return;
11184
+ }
11185
+ let text = contentText(last.content).trim();
11186
+ if (a[0] === "code") {
11187
+ const fences = [...text.matchAll(/```[^\n]*\n([\s\S]*?)```/g)];
11188
+ if (!fences.length) {
11189
+ err(dim(" (no code block in the last reply)\n"));
11190
+ return;
11191
+ }
11192
+ text = fences[fences.length - 1][1].trimEnd();
11193
+ }
11194
+ err(dim(copyTextToClipboard(text) ? ` \u2713 copied ${text.length} chars
11195
+ ` : " no clipboard tool found (pbcopy/wl-copy/xclip)\n"));
11196
+ }
11197
+ },
11198
+ diff: {
11199
+ desc: "show all file changes this session (oldest checkpoint \u2192 now)",
11200
+ run: async () => {
11201
+ if (!checkpoints.diff) {
11202
+ err(dim(" (diff not supported by this checkpoint backend)\n"));
11203
+ return;
11204
+ }
11205
+ await checkpoints.refresh?.();
11206
+ if (!checkpoints.size) {
11207
+ err(dim(" (no checkpoints yet \u2014 make a turn first)\n"));
11208
+ return;
11209
+ }
11210
+ const d = (await checkpoints.diff()).trim();
11211
+ if (!d) {
11212
+ err(dim(" (no file changes this session)\n"));
11213
+ return;
11214
+ }
11215
+ err(d.split("\n").map((l) => l.startsWith("+") && !l.startsWith("+++") ? green(l) : l.startsWith("-") && !l.startsWith("---") ? red(l) : dim(l)).join("\n") + "\n");
11216
+ }
11217
+ },
10748
11218
  memory: {
10749
11219
  desc: "open the memory index in $EDITOR (.agent/memory/MEMORY.md)",
10750
11220
  run: async () => {
@@ -10775,8 +11245,8 @@ ${extra}` : body);
10775
11245
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10776
11246
  if (wasRaw) process.stdin.setRawMode(false);
10777
11247
  try {
10778
- const { spawnSync: spawnSync3 } = await import("child_process");
10779
- spawnSync3(ed, [idx], { stdio: "inherit" });
11248
+ const { spawnSync: spawnSync5 } = await import("child_process");
11249
+ spawnSync5(ed, [idx], { stdio: "inherit" });
10780
11250
  } finally {
10781
11251
  if (wasRaw) process.stdin.setRawMode(true);
10782
11252
  }
@@ -10893,7 +11363,7 @@ ${extra}` : body);
10893
11363
  try {
10894
11364
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
10895
11365
  } catch (e) {
10896
- log17.debug(`loadAgents(${d}) failed`, e);
11366
+ log20.debug(`loadAgents(${d}) failed`, e);
10897
11367
  }
10898
11368
  }
10899
11369
  if (!seen.size) {
@@ -10985,7 +11455,7 @@ ${extra}` : body);
10985
11455
  if (idx >= 0) {
10986
11456
  const old = mounted.splice(idx, 1)[0];
10987
11457
  removeWorkTools(old.tools.map((t) => t.name));
10988
- await old.client.close().catch((e) => log17.debug("mcp close failed", e));
11458
+ await old.client.close().catch((e) => log20.debug("mcp close failed", e));
10989
11459
  }
10990
11460
  try {
10991
11461
  const m = await mountMcpServer(name, conf);
@@ -11013,7 +11483,7 @@ ${extra}` : body);
11013
11483
  }
11014
11484
  const m = mounted.splice(idx, 1)[0];
11015
11485
  removeWorkTools(m.tools.map((t) => t.name));
11016
- await m.client.close().catch((e) => log17.debug("mcp close failed", e));
11486
+ await m.client.close().catch((e) => log20.debug("mcp close failed", e));
11017
11487
  err(dim(` removed "${name}"
11018
11488
  `));
11019
11489
  return;
@@ -11145,11 +11615,11 @@ ${extra}` : body);
11145
11615
  return;
11146
11616
  }
11147
11617
  const md = exportMarkdown(session.meta, shown);
11148
- const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join9(".agent", "exports", `${session.meta.id}.md`);
11618
+ const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join11(".agent", "exports", `${session.meta.id}.md`);
11149
11619
  const path = resolve3(cwd, name);
11150
11620
  try {
11151
- mkdirSync7(dirname4(path), { recursive: true });
11152
- writeFileSync6(path, md);
11621
+ mkdirSync8(dirname4(path), { recursive: true });
11622
+ writeFileSync8(path, md);
11153
11623
  err(green(` \u2713 exported \u2192 ${path}
11154
11624
  `) + dim(` ${shown.length} message(s) \xB7 ${md.length} chars
11155
11625
  `));
@@ -11212,9 +11682,9 @@ ${extra}` : body);
11212
11682
  `));
11213
11683
  const listDir = (absDir) => {
11214
11684
  try {
11215
- return readdirSync2(join9(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
11685
+ return readdirSync3(join11(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
11216
11686
  } catch (e) {
11217
- log17.debug("completion readdir failed", absDir, e);
11687
+ log20.debug("completion readdir failed", absDir, e);
11218
11688
  return null;
11219
11689
  }
11220
11690
  };
@@ -11704,7 +12174,7 @@ async function main() {
11704
12174
  }
11705
12175
  });
11706
12176
  if (args.task) {
11707
- const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") }));
12177
+ const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join11(cwd, ".agent", "mcp-auth.json") }));
11708
12178
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
11709
12179
  const store = new SessionStore(cwd);
11710
12180
  const session = startSession(args, store, agent, cwd);
@@ -11743,6 +12213,7 @@ export {
11743
12213
  formatTranscriptFull,
11744
12214
  jsonResult,
11745
12215
  mcpMentionResolver,
12216
+ mentionRefs,
11746
12217
  parseArgs,
11747
12218
  pastePathClassifier,
11748
12219
  readImageParts,