agent.libx.js 0.94.23 → 0.94.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -254,12 +254,14 @@ var init_tools_structured = __esm({
254
254
  const ctxN = Math.max(0, Number(context ?? 0));
255
255
  const out = [];
256
256
  const matched = [];
257
+ let skipped = 0;
257
258
  for (const path of files) {
258
259
  ckAbort(ctx.signal);
259
260
  let content;
260
261
  try {
261
262
  content = await ctx.fs.readFile(path);
262
263
  } catch {
264
+ skipped++;
263
265
  continue;
264
266
  }
265
267
  const lines = content.split("\n");
@@ -274,8 +276,10 @@ var init_tools_structured = __esm({
274
276
  }
275
277
  if (fileHit) matched.push(path);
276
278
  }
277
- if (filesOnly) return matched.length ? matched.join("\n") : "(no matches)";
278
- return out.length ? out.join("\n") : "(no matches)";
279
+ const note = skipped ? `
280
+ [skipped ${skipped} unreadable file${skipped === 1 ? "" : "s"}]` : "";
281
+ if (filesOnly) return (matched.length ? matched.join("\n") : "(no matches)") + note;
282
+ return (out.length ? out.join("\n") : "(no matches)") + note;
279
283
  }
280
284
  };
281
285
  SIG_RE = /^\s*(export\b|(?:export\s+)?(?:async\s+)?function\s+\*?\w|(?:export\s+)?(?:abstract\s+)?class\s+\w|(?:export\s+)?interface\s+\w|(?:export\s+)?type\s+\w|(?:export\s+)?enum\s+\w)/;
@@ -964,7 +968,7 @@ var init_tools = __esm({
964
968
  IMG_MIME = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
965
969
  readTool = {
966
970
  name: "Read",
967
- description: "Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. Always Read a file before Editing it.",
971
+ description: "Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.",
968
972
  parameters: {
969
973
  type: "object",
970
974
  required: ["path"],
@@ -976,6 +980,12 @@ var init_tools = __esm({
976
980
  },
977
981
  async run({ path, offset, limit }, ctx) {
978
982
  const ext = String(path).toLowerCase().split(".").pop() ?? "";
983
+ if (ext === "pdf") {
984
+ if (!ctx.pdfText) return `[${path} is a PDF \u2014 text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;
985
+ if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
986
+ const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();
987
+ return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;
988
+ }
979
989
  if (IMG_MIME[ext]) {
980
990
  const fs = ctx.fs;
981
991
  if (typeof fs.readFileBytes !== "function") {
@@ -1053,7 +1063,7 @@ var NodeDiskFilesystem;
1053
1063
  var init_NodeDiskFilesystem = __esm({
1054
1064
  "src/NodeDiskFilesystem.ts"() {
1055
1065
  "use strict";
1056
- NodeDiskFilesystem = class {
1066
+ NodeDiskFilesystem = class _NodeDiskFilesystem {
1057
1067
  constructor(baseDir, opts = {}) {
1058
1068
  this.baseDir = baseDir;
1059
1069
  this.opts = { denySymlinks: true, ...opts };
@@ -1068,14 +1078,24 @@ var init_NodeDiskFilesystem = __esm({
1068
1078
  real(vpath) {
1069
1079
  return np.join(this.baseDir, "." + vpath);
1070
1080
  }
1081
+ // Verified non-symlink DIRECTORY components, with a short TTL: tree walks (Glob/Grep) hit the same
1082
+ // parents thousands of times; re-lstat'ing each per op is the dominant syscall cost. The 1s window
1083
+ // is an accepted race only against an out-of-band symlink swap mid-walk (this FS can't create
1084
+ // symlinks itself); leaf components are never cached.
1085
+ verified = /* @__PURE__ */ new Map();
1086
+ static VERIFY_TTL_MS = 1e3;
1071
1087
  /** Throw if any existing component of `real` is a symlink (escape vector). */
1072
1088
  async assertNoSymlink(real) {
1073
1089
  if (!this.opts.denySymlinks) return;
1074
1090
  const rel = np.relative(this.baseDir, real);
1075
1091
  if (rel === "" || rel.startsWith("..")) return;
1092
+ const parts = rel.split(np.sep);
1076
1093
  let cur = this.baseDir;
1077
- for (const part of rel.split(np.sep)) {
1078
- cur = np.join(cur, part);
1094
+ const now5 = Date.now();
1095
+ for (let i = 0; i < parts.length; i++) {
1096
+ cur = np.join(cur, parts[i]);
1097
+ const isLeaf = i === parts.length - 1;
1098
+ if (!isLeaf && (this.verified.get(cur) ?? 0) > now5) continue;
1079
1099
  let st;
1080
1100
  try {
1081
1101
  st = await fsp.lstat(cur);
@@ -1083,6 +1103,10 @@ var init_NodeDiskFilesystem = __esm({
1083
1103
  return;
1084
1104
  }
1085
1105
  if (st.isSymbolicLink()) throw new Error("File not found: symlink not permitted");
1106
+ if (!isLeaf) {
1107
+ if (this.verified.size > 1e4) this.verified.clear();
1108
+ this.verified.set(cur, now5 + _NodeDiskFilesystem.VERIFY_TTL_MS);
1109
+ }
1086
1110
  }
1087
1111
  }
1088
1112
  resolvePath(path, cwd) {
@@ -1418,10 +1442,10 @@ function sandboxArgv(command, cwd, opts = {}, platform2 = process.platform, tmpD
1418
1442
  return null;
1419
1443
  }
1420
1444
  async function findSandboxWrapper(platform2 = process.platform) {
1421
- const { existsSync: existsSync9 } = await import("fs");
1422
- if (platform2 === "darwin") return existsSync9("/usr/bin/sandbox-exec") ? "/usr/bin/sandbox-exec" : null;
1445
+ const { existsSync: existsSync10 } = await import("fs");
1446
+ if (platform2 === "darwin") return existsSync10("/usr/bin/sandbox-exec") ? "/usr/bin/sandbox-exec" : null;
1423
1447
  if (platform2 === "linux") {
1424
- for (const dir of (process.env.PATH ?? "/usr/bin:/bin").split(":")) if (dir && existsSync9(`${dir}/bwrap`)) return `${dir}/bwrap`;
1448
+ for (const dir of (process.env.PATH ?? "/usr/bin:/bin").split(":")) if (dir && existsSync10(`${dir}/bwrap`)) return `${dir}/bwrap`;
1425
1449
  return null;
1426
1450
  }
1427
1451
  return null;
@@ -1689,8 +1713,8 @@ var init_tools_shell = __esm({
1689
1713
 
1690
1714
  // cli/cli.ts
1691
1715
  import { createInterface } from "readline/promises";
1692
- import { existsSync as existsSync8, readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
1693
- import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
1716
+ import { existsSync as existsSync9, readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, readdirSync as readdirSync3, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
1717
+ import { homedir as homedir8, tmpdir as tmpdir3 } from "os";
1694
1718
 
1695
1719
  // cli/clipboard.ts
1696
1720
  import { execFileSync } from "child_process";
@@ -1744,9 +1768,20 @@ close access f`;
1744
1768
  }
1745
1769
  return null;
1746
1770
  }
1771
+ function copyTextToClipboard(text, platform2 = process.platform) {
1772
+ const candidates = platform2 === "darwin" ? [["pbcopy", []]] : platform2 === "linux" ? [["wl-copy", []], ["xclip", ["-selection", "clipboard"]]] : [];
1773
+ for (const [cmd, args] of candidates) {
1774
+ try {
1775
+ execFileSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
1776
+ return true;
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ return false;
1781
+ }
1747
1782
 
1748
1783
  // cli/cli.ts
1749
- import { join as join9, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
1784
+ import { join as join11, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
1750
1785
  import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions } from "ai.libx.js";
1751
1786
 
1752
1787
  // src/llm.ts
@@ -2839,6 +2874,8 @@ var AgentOptions = class {
2839
2874
  permissions;
2840
2875
  /** Opt-in syntax guardrail: refuse to persist a syntactically-broken code-file write/edit. Default off. */
2841
2876
  lintOnWrite;
2877
+ /** Optional PDF text extraction for Read on .pdf files (node hosts wire pdftotext); absent => Read explains. */
2878
+ pdfText;
2842
2879
  /** Opt-in: after a write-class tool runs, run `command` over the VFS and append any failure to the tool result.
2843
2880
  * `tools` defaults to ['Write','Edit','MultiEdit','ApplyEdits']. */
2844
2881
  autoTest;
@@ -2889,8 +2926,12 @@ var Agent = class _Agent {
2889
2926
  reprepare() {
2890
2927
  this.prepared = false;
2891
2928
  }
2892
- /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn. */
2929
+ /** Tools injected via addTools(); kept separate from options.tools so prepare() rebuilds don't drop them. */
2930
+ injectedTools = [];
2931
+ /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn
2932
+ * and survives prepare() rebuilds (reprepare(), new conversations). */
2893
2933
  addTools(tools) {
2934
+ this.injectedTools.push(...tools);
2894
2935
  this.activeTools.push(...tools);
2895
2936
  }
2896
2937
  /** Remove tools by name from a running agent. Returns the count removed. */
@@ -2898,6 +2939,7 @@ var Agent = class _Agent {
2898
2939
  const s = names instanceof Set ? names : new Set(names);
2899
2940
  const before = this.activeTools.length;
2900
2941
  this.activeTools = this.activeTools.filter((t) => !s.has(t.name));
2942
+ this.injectedTools = this.injectedTools.filter((t) => !s.has(t.name));
2901
2943
  return before - this.activeTools.length;
2902
2944
  }
2903
2945
  constructor(options) {
@@ -2909,6 +2951,7 @@ var Agent = class _Agent {
2909
2951
  this.ctx = makeContext(this.options.fs, this.options.host);
2910
2952
  this.ctx.signal = this.options.signal;
2911
2953
  if (this.options.lintOnWrite) this.ctx.lint = checkSyntax;
2954
+ if (this.options.pdfText) this.ctx.pdfText = this.options.pdfText;
2912
2955
  this.ctx.ai = this.options.ai;
2913
2956
  this.ctx.model = this.options.model;
2914
2957
  this.ctx.parkHuman = (p) => this.park(p);
@@ -2981,7 +3024,7 @@ var Agent = class _Agent {
2981
3024
  const plan = o.planMode ? planMode({ host: o.host }) : void 0;
2982
3025
  if (plan) tools = [...tools, plan.tool];
2983
3026
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
2984
- this.activeTools = tools;
3027
+ this.activeTools = [...tools, ...this.injectedTools];
2985
3028
  this.systemPromptCache = systemPrompt;
2986
3029
  this.prepared = true;
2987
3030
  return systemPrompt;
@@ -3116,7 +3159,15 @@ var Agent = class _Agent {
3116
3159
  } catch (err2) {
3117
3160
  if (err2?.code === "budget") return kill("budget");
3118
3161
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
3119
- log3.error(`chat() failed: ${err2?.message ?? err2}`, err2);
3162
+ const body = err2?.body ?? err2?.response?.data ?? err2?.error;
3163
+ let bodyStr;
3164
+ try {
3165
+ bodyStr = body && typeof body !== "string" ? JSON.stringify(body).slice(0, 2e3) : body;
3166
+ } catch {
3167
+ bodyStr = void 0;
3168
+ }
3169
+ if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
3170
+ log3.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err2);
3120
3171
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
3121
3172
  }
3122
3173
  if (o.signal?.aborted) return kill("aborted");
@@ -3347,20 +3398,28 @@ function stubOldToolResults(messages, keep) {
3347
3398
  return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
3348
3399
  });
3349
3400
  }
3350
- var hasCallFor = (msgs, id) => msgs.some((m) => m.role === "assistant" && m.tool_calls?.some((tc) => tc.id === id));
3401
+ var callIdSet = (msgs) => {
3402
+ const ids = /* @__PURE__ */ new Set();
3403
+ for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
3404
+ return ids;
3405
+ };
3351
3406
  function dropOrphanToolResults(messages) {
3352
- const ok = (m) => m.role !== "tool" || hasCallFor(messages, m.tool_call_id);
3407
+ const ids = callIdSet(messages);
3408
+ const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
3353
3409
  return messages.every(ok) ? messages : messages.filter(ok);
3354
3410
  }
3355
3411
  function fitTokenBudget(messages, maxTokens) {
3356
- if (estimateTokens(messages) <= maxTokens) return messages;
3412
+ const per = messages.map((x) => estimateTokens([x]));
3413
+ let total = per.reduce((a, b) => a + b, 0);
3414
+ if (total <= maxTokens) return messages;
3357
3415
  const head = messages[0]?.role === "system" ? [messages[0]] : [];
3358
- let body = messages.slice(head.length);
3359
- while (body.length && estimateTokens([...head, ...body]) > maxTokens) body = body.slice(1);
3360
- while (body.length && body[0].role === "tool" && !hasCallFor(body, body[0].tool_call_id)) body = body.slice(1);
3361
- if (estimateTokens([...head, ...body]) > maxTokens)
3362
- log3.warn(`context ~${estimateTokens([...head, ...body])} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3363
- return [...head, ...body];
3416
+ let from = head.length;
3417
+ while (from < messages.length && total > maxTokens) total -= per[from++];
3418
+ const ids = callIdSet(messages.slice(from));
3419
+ while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
3420
+ if (total > maxTokens)
3421
+ log3.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3422
+ return [...head, ...messages.slice(from)];
3364
3423
  }
3365
3424
  function compact(m, max, focus) {
3366
3425
  const hasSystem = m[0]?.role === "system";
@@ -3812,11 +3871,11 @@ var Scheduler = class {
3812
3871
  this.jobs.clear();
3813
3872
  }
3814
3873
  };
3815
- function makeScheduleTools(scheduler) {
3874
+ function makeScheduleTools(scheduler, os) {
3816
3875
  return [
3817
3876
  {
3818
3877
  name: "ScheduleTask",
3819
- description: 'Schedule a prompt to fire automatically while this session is alive.\nModes:\n \u2022 One-off: {at: <epoch_ms>} \u2014 fires once at that time, then done.\n \u2022 Interval: {everyMs: <ms>} \u2014 fires repeatedly (\u22651s).\n \u2022 Cron: {cron: "min hr dom mon dow"} \u2014 standard 5-field cron.\nReturns the job id. Jobs only fire while the CLI session is running \u2014 they do NOT survive quitting.',
3878
+ description: 'Schedule a prompt to fire automatically.\nModes:\n \u2022 One-off: {at: <epoch_ms>} \u2014 fires once at that time, then done.\n \u2022 Interval: {everyMs: <ms>} \u2014 fires repeatedly (\u22651s).\n \u2022 Cron: {cron: "min hr dom mon dow"} \u2014 standard 5-field cron.\nBackend: "session" fires while this CLI session is alive (default for recurring + near one-offs); "os" registers with the OS scheduler so the job survives quitting \u2014 it headless-resumes this session when it fires (auto-chosen for one-offs \u226530min out when available). Pass backend:"os" explicitly for recurring jobs that must outlive the session.',
3820
3879
  parameters: {
3821
3880
  type: "object",
3822
3881
  required: ["prompt", "trigger"],
@@ -3831,15 +3890,22 @@ function makeScheduleTools(scheduler) {
3831
3890
  cron: { type: "string" }
3832
3891
  }
3833
3892
  },
3834
- label: { type: "string", description: "Short label for display (optional)." }
3893
+ label: { type: "string", description: "Short label for display (optional)." },
3894
+ backend: { type: "string", enum: ["auto", "session", "os"], description: "Where the job lives (default auto)." }
3835
3895
  }
3836
3896
  },
3837
- async run({ prompt, trigger, label }) {
3897
+ async run({ prompt, trigger, label, backend }) {
3838
3898
  try {
3899
+ if (os && os.route(trigger, backend) === "os") {
3900
+ const id2 = `os-${Date.now().toString(36)}`;
3901
+ const mechanism = os.schedule({ id: id2, prompt, sessionId: os.sessionId, cwd: os.cwd, trigger, label });
3902
+ return `Scheduled ${id2}${label ? ` (${label})` : ""} on the OS scheduler (${mechanism}) \u2014 survives quitting; fires \`agentx --resume ${os.sessionId}\` headless.`;
3903
+ }
3904
+ if (backend === "os") return "Error: no OS scheduler available on this platform \u2014 job not created (use the default in-session backend).";
3839
3905
  const id = scheduler.add({ prompt, trigger, label });
3840
3906
  const job = scheduler.get(id);
3841
3907
  const next = scheduler.nextFire(job);
3842
- return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}.`;
3908
+ return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}. (In-session: does not survive quitting.)`;
3843
3909
  } catch (e) {
3844
3910
  return `Error: ${e?.message ?? e}`;
3845
3911
  }
@@ -3847,24 +3913,28 @@ function makeScheduleTools(scheduler) {
3847
3913
  },
3848
3914
  {
3849
3915
  name: "ScheduleList",
3850
- description: "List all scheduled jobs and their next fire time.",
3916
+ description: "List all scheduled jobs (in-session + OS-backed) and their next fire time.",
3851
3917
  parameters: { type: "object", properties: {} },
3852
3918
  async run() {
3919
+ const osJobs = os?.list() ?? [];
3920
+ const osLines = osJobs.map((j) => `${j.id} os ${j.mechanism}${j.label ? " " + j.label : ""}`);
3853
3921
  const jobs = scheduler.list();
3854
- if (!jobs.length) return "(no scheduled jobs)";
3855
- return jobs.map((j) => {
3922
+ if (!jobs.length && !osLines.length) return "(no scheduled jobs)";
3923
+ return [...osLines, ...jobs.map((j) => {
3856
3924
  const next = scheduler.nextFire(j);
3857
3925
  const trig = "at" in j.trigger ? `once @ ${new Date(j.trigger.at).toLocaleString()}` : "everyMs" in j.trigger ? `every ${(j.trigger.everyMs / 1e3).toFixed(0)}s` : `cron: ${j.trigger.cron}`;
3858
3926
  return `${j.id} ${j.status} ${trig} runs:${j.runs} next:${next ? new Date(next).toLocaleTimeString() : "\u2014"}${j.label ? " " + j.label : ""}`;
3859
- }).join("\n");
3927
+ })].join("\n");
3860
3928
  }
3861
3929
  },
3862
3930
  {
3863
3931
  name: "ScheduleCancel",
3864
- description: "Cancel a scheduled job by id.",
3932
+ description: "Cancel a scheduled job by id (in-session or OS-backed).",
3865
3933
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
3866
3934
  async run({ id }) {
3867
- return scheduler.cancel(String(id)) ? `Cancelled ${id}.` : `Error: no scheduled job '${id}'. Use ScheduleList to see jobs.`;
3935
+ const key = String(id);
3936
+ if (key.startsWith("os-")) return os?.cancel(key) ? `Cancelled ${key} (OS job removed).` : `Error: no OS job '${key}'.`;
3937
+ return scheduler.cancel(key) ? `Cancelled ${key}.` : `Error: no scheduled job '${key}'. Use ScheduleList to see jobs.`;
3868
3938
  }
3869
3939
  },
3870
3940
  {
@@ -4162,6 +4232,9 @@ var DuplexAgentOptions = class {
4162
4232
  askRelay = false;
4163
4233
  /** Parked questions auto-resolve empty after this long (callers map '' to deny/best-judgment). */
4164
4234
  askTimeoutMs = 12e4;
4235
+ /** Max retained task records: oldest SETTLED tasks (and their activity tails) are evicted past this,
4236
+ * bounding memory over a long-lived session. Running tasks are never evicted. */
4237
+ maxTaskRecords = 50;
4165
4238
  /** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
4166
4239
  * (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
4167
4240
  quickLook;
@@ -4452,6 +4525,11 @@ ${recent}` : brief) + verify;
4452
4525
  };
4453
4526
  const promise = new Agent(agentOpts).run(briefText).then((res) => this.maybeVerify(id, briefText, res, tier, agentOpts)).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
4454
4527
  this.tasks.set(id, { id, label, status: "running", controller, promise, tail });
4528
+ if (this.tasks.size > this.options.maxTaskRecords)
4529
+ for (const [tid, rec] of this.tasks) {
4530
+ if (this.tasks.size <= this.options.maxTaskRecords) break;
4531
+ if (rec.status !== "running") this.tasks.delete(tid);
4532
+ }
4455
4533
  }
4456
4534
  /** Fresh-context check of a successful Act task: a NEW agent (same model/fs/tools, but NO shared
4457
4535
  * conversation context) re-reads the file state against the brief and fixes any gap. The fix lands
@@ -6052,15 +6130,101 @@ function defaultOpenBrowser(url) {
6052
6130
 
6053
6131
  // cli/core.ts
6054
6132
  import { randomUUID } from "crypto";
6133
+ import { execFile as execFile2 } from "child_process";
6055
6134
  import { resolve, basename, join as join3 } from "path";
6056
- import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
6057
- import { platform, arch, release, userInfo, homedir, tmpdir } from "os";
6135
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
6136
+ import { platform, arch, release, userInfo, homedir as homedir2, tmpdir } from "os";
6058
6137
  init_tools_shell();
6138
+
6139
+ // src/tools.notify.ts
6140
+ init_logging();
6141
+ import { execFile } from "child_process";
6142
+ var log13 = forComponent("notify");
6143
+ function makeNotifyTool(opts = {}) {
6144
+ const platform2 = opts.platform ?? process.platform;
6145
+ const run = opts.exec ?? execFile;
6146
+ return {
6147
+ name: "PushNotification",
6148
+ description: "Show an OS desktop notification to the user (out-of-band alert \u2014 e.g. a long task finished, input is needed). Use sparingly: only for events the user would want to be interrupted for.",
6149
+ parameters: {
6150
+ type: "object",
6151
+ required: ["message"],
6152
+ properties: {
6153
+ message: { type: "string", description: "notification body" },
6154
+ title: { type: "string", description: 'notification title (default "agentx")' }
6155
+ }
6156
+ },
6157
+ async run({ message, title }) {
6158
+ const msg = String(message ?? "").slice(0, 256);
6159
+ const head = String(title ?? "agentx").slice(0, 64);
6160
+ if (!msg) return "Error: empty message";
6161
+ const argv = platform2 === "darwin" ? ["osascript", ["-e", `display notification ${JSON.stringify(msg)} with title ${JSON.stringify(head)}`]] : platform2 === "linux" ? ["notify-send", [head, msg]] : null;
6162
+ if (!argv) return `Notifications unavailable on ${platform2}.`;
6163
+ return new Promise((resolve4) => {
6164
+ run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
6165
+ if (e) {
6166
+ log13.debug("notification failed", e);
6167
+ resolve4(`Notification failed: ${e.message}`);
6168
+ } else resolve4("Notification shown.");
6169
+ });
6170
+ });
6171
+ }
6172
+ };
6173
+ }
6174
+
6175
+ // cli/core.ts
6059
6176
  import { BodDB as BodDB2 } from "@bod.ee/db";
6177
+
6178
+ // cli/util.ts
6179
+ init_logging();
6180
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
6181
+ import { homedir } from "os";
6182
+ var log14 = forComponent("cli-util");
6183
+ function dotDirs(base, sub, opts = {}) {
6184
+ const home = opts.home ?? homedir();
6185
+ const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
6186
+ return opts.existing ? dirs.filter((d) => existsSync2(d)) : dirs;
6187
+ }
6188
+ function truncate(s, n, suffix = "\u2026") {
6189
+ const t = s ?? "";
6190
+ return t.length > n ? t.slice(0, n) + suffix : t;
6191
+ }
6192
+ function sanitizeLabel(s, max = 60) {
6193
+ return truncate(s.replace(/\s+/g, " ").trim(), max, "");
6194
+ }
6195
+ function parseJson(text, fallback, what = "json") {
6196
+ try {
6197
+ return JSON.parse(text);
6198
+ } catch (e) {
6199
+ log14.debug(`parseJson(${what}) failed: ${e.message}`);
6200
+ return fallback;
6201
+ }
6202
+ }
6203
+ function readJsonFile(path, fallback) {
6204
+ if (!existsSync2(path)) return fallback;
6205
+ let text;
6206
+ try {
6207
+ text = readFileSync2(path, "utf8");
6208
+ } catch (e) {
6209
+ log14.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
6210
+ return fallback;
6211
+ }
6212
+ return parseJson(text, fallback, path);
6213
+ }
6214
+
6215
+ // cli/core.ts
6060
6216
  var DEFAULT_TOOLS = ["bash", "Read", "Edit", "Write", "Grep", "Glob", "MultiEdit", "ApplyEdits", "RepoMap", "TodoWrite"];
6061
6217
  function autoWebTools() {
6062
6218
  return ["WebFetch", "WebSearch"];
6063
6219
  }
6220
+ function pdfTextViaPoppler(path) {
6221
+ return new Promise((res, rej) => {
6222
+ execFile2("pdftotext", [path, "-"], { maxBuffer: 32 * 1024 * 1024, timeout: 3e4 }, (e, stdout) => {
6223
+ if (e) rej(new Error(/ENOENT/.test(String(e.code ?? e.message)) ? "pdftotext not installed (brew/apt install poppler)" : e.message));
6224
+ else res(stdout);
6225
+ });
6226
+ });
6227
+ }
6064
6228
  var SANDBOX_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"]);
6065
6229
  async function hydrate(from, to, dir = "/") {
6066
6230
  let n = 0;
@@ -6155,33 +6319,27 @@ async function buildAgent(o) {
6155
6319
  ${notes.join("\n")}
6156
6320
  Reference files in them by their mount path (the left side).`;
6157
6321
  }
6158
- const dot = (sub) => existsSync2(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
6322
+ const dot = (sub) => existsSync3(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
6159
6323
  const dots = (sub) => {
6160
- const home = homedir();
6161
- const dirs = [
6162
- dot(sub),
6163
- existsSync2(`${cwd}/.claude/${sub}`) ? `${cwd}/.claude/${sub}` : void 0,
6164
- existsSync2(`${home}/.agent/${sub}`) ? `${home}/.agent/${sub}` : void 0,
6165
- existsSync2(`${home}/.claude/${sub}`) ? `${home}/.claude/${sub}` : void 0
6166
- ].filter(Boolean);
6324
+ const dirs = dotDirs(cwd, sub, { existing: true });
6167
6325
  return dirs.length ? dirs : void 0;
6168
6326
  };
6169
6327
  const memoryDir = (() => {
6170
- const home = homedir();
6328
+ const home = homedir2();
6171
6329
  const projectDir = `${cwd}/.agent/memory`;
6172
6330
  const readDirs = [
6173
- existsSync2(projectDir) ? projectDir : void 0,
6174
- existsSync2(`${cwd}/.claude/memory`) ? `${cwd}/.claude/memory` : void 0,
6175
- existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6176
- existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6331
+ existsSync3(projectDir) ? projectDir : void 0,
6332
+ existsSync3(`${cwd}/.claude/memory`) ? `${cwd}/.claude/memory` : void 0,
6333
+ existsSync3(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6334
+ existsSync3(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6177
6335
  ].filter(Boolean);
6178
6336
  return readDirs[0] === projectDir ? readDirs : [projectDir, ...readDirs];
6179
6337
  })();
6180
6338
  const memoryUserDir = (() => {
6181
- const home = homedir();
6339
+ const home = homedir2();
6182
6340
  return [
6183
- existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6184
- existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6341
+ existsSync3(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
6342
+ existsSync3(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
6185
6343
  ].find(Boolean) ?? `${home}/.agent/memory`;
6186
6344
  })();
6187
6345
  const memoryWriteDir = memoryDir[0];
@@ -6198,6 +6356,8 @@ Reference files in them by their mount path (the left side).`;
6198
6356
  ai: o.ai,
6199
6357
  fs,
6200
6358
  model: o.model ?? "anthropic/claude-sonnet-4-6",
6359
+ // PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
6360
+ ...!virtual ? { pdfText: pdfTextViaPoppler } : {},
6201
6361
  // Anchor cursor to the launch dir (its adapter defaults to TMPDIR otherwise) and forward the
6202
6362
  // host's MCP servers so the delegated cursor agent runs in the same environment. Gated to cursor:
6203
6363
  // openai/google adapters Object.assign providerOptions into the request body, so a blanket cwd
@@ -6240,6 +6400,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
6240
6400
  const base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
6241
6401
  const tail = [...o.extraTools ?? []];
6242
6402
  if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "anthropic/claude-sonnet-4-6", dir: scratchDir }));
6403
+ tail.push(makeNotifyTool());
6243
6404
  if (!realShell.length) return [...base, ...tail];
6244
6405
  const filtered = base.filter((t) => t.name !== "bash");
6245
6406
  return [...filtered, ...realShell, ...tail];
@@ -6291,11 +6452,11 @@ var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0
6291
6452
  // cli/voice.ts
6292
6453
  init_logging();
6293
6454
  import { spawn as spawn2, spawnSync } from "child_process";
6294
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
6295
- import { homedir as homedir2 } from "os";
6455
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
6456
+ import { homedir as homedir3 } from "os";
6296
6457
  import { dirname as dirname3, join as join4 } from "path";
6297
6458
  import { fileURLToPath } from "url";
6298
- var log13 = forComponent("VoiceIO");
6459
+ var log15 = forComponent("VoiceIO");
6299
6460
  var now4 = () => performance.now();
6300
6461
  var Player = class {
6301
6462
  proc = null;
@@ -6309,7 +6470,7 @@ var Player = class {
6309
6470
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
6310
6471
  { stdio: ["pipe", "ignore", "ignore"] }
6311
6472
  );
6312
- this.proc.on("error", (e) => log13.warn(`ffplay error: ${e.message}`));
6473
+ this.proc.on("error", (e) => log15.warn(`ffplay error: ${e.message}`));
6313
6474
  this.proc.stdin.on("error", () => {
6314
6475
  });
6315
6476
  this.bytesWritten = 0;
@@ -6344,28 +6505,28 @@ function detectFfmpegMic() {
6344
6505
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
6345
6506
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
6346
6507
  if (!mic) throw new Error("no audio input device found");
6347
- log13.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
6508
+ log15.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
6348
6509
  return `:${mic.idx}`;
6349
6510
  }
6350
6511
  function resolveAecBinary() {
6351
6512
  if (process.env.MIC_AEC === "0" || process.platform !== "darwin") return null;
6352
6513
  const src = join4(nativeDir(), "mic-aec.swift");
6353
6514
  const plist = join4(nativeDir(), "Info.plist");
6354
- if (!existsSync3(src)) return null;
6355
- const cacheDir = join4(homedir2(), ".agent", "cache");
6515
+ if (!existsSync4(src)) return null;
6516
+ const cacheDir = join4(homedir3(), ".agent", "cache");
6356
6517
  const bin = join4(cacheDir, "mic-aec");
6357
- if (existsSync3(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
6518
+ if (existsSync4(bin) && statSync2(bin).mtimeMs >= statSync2(src).mtimeMs) return bin;
6358
6519
  if (spawnSync("which", ["swiftc"]).status !== 0) return null;
6359
6520
  mkdirSync3(cacheDir, { recursive: true });
6360
- log13.info("compiling AEC mic helper (first run)\u2026");
6521
+ log15.info("compiling AEC mic helper (first run)\u2026");
6361
6522
  const build = spawnSync("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
6362
6523
  if (build.status !== 0) {
6363
- log13.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
6524
+ log15.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
6364
6525
  return null;
6365
6526
  }
6366
6527
  const sign = spawnSync("codesign", ["-fs", "-", bin], { encoding: "utf8" });
6367
6528
  if (sign.status !== 0) {
6368
- log13.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
6529
+ log15.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
6369
6530
  return null;
6370
6531
  }
6371
6532
  return bin;
@@ -6384,16 +6545,16 @@ var NodeMicSource = class {
6384
6545
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
6385
6546
  } else {
6386
6547
  if (spawnSync("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
6387
- log13.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
6548
+ log15.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
6388
6549
  this.proc = spawn2(
6389
6550
  "ffmpeg",
6390
6551
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
6391
6552
  { stdio: ["ignore", "pipe", "pipe"] }
6392
6553
  );
6393
- this.proc.stderr.on("data", (d) => log13.warn(`ffmpeg: ${String(d).trim()}`));
6554
+ this.proc.stderr.on("data", (d) => log15.warn(`ffmpeg: ${String(d).trim()}`));
6394
6555
  }
6395
6556
  this.proc.on("exit", (c) => {
6396
- if (c && !this.stopped) log13.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
6557
+ if (c && !this.stopped) log15.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
6397
6558
  });
6398
6559
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
6399
6560
  }
@@ -6427,11 +6588,11 @@ var AecDuplexAudio = class {
6427
6588
  this.proc.stdin.on("error", () => {
6428
6589
  });
6429
6590
  this.proc.on("exit", (c) => {
6430
- if (c && !this.stopped) log13.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
6591
+ if (c && !this.stopped) log15.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
6431
6592
  });
6432
6593
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
6433
6594
  this.proc.stderr.on("data", (d) => {
6434
- for (const ln of String(d).split("\n")) if (ln.trim()) log13.debug(`mic-aec: ${ln.trim()}`);
6595
+ for (const ln of String(d).split("\n")) if (ln.trim()) log15.debug(`mic-aec: ${ln.trim()}`);
6435
6596
  });
6436
6597
  }
6437
6598
  stop() {
@@ -6547,15 +6708,15 @@ var VoiceIO = class extends VoiceEngine {
6547
6708
  };
6548
6709
 
6549
6710
  // cli/config.ts
6550
- import { homedir as homedir3 } from "os";
6551
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
6711
+ import { homedir as homedir4 } from "os";
6712
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
6552
6713
  import { join as join5 } from "path";
6553
6714
  import { pathToFileURL } from "url";
6554
6715
  var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
6555
6716
  async function loadFrom(dir) {
6556
6717
  for (const f of FILES) {
6557
6718
  const p = join5(dir, ".agent", f);
6558
- if (!existsSync4(p)) continue;
6719
+ if (!existsSync5(p)) continue;
6559
6720
  try {
6560
6721
  const mod = await import(pathToFileURL(p).href, f.endsWith(".json") ? { with: { type: "json" } } : void 0);
6561
6722
  return mod.default ?? mod.config ?? mod;
@@ -6568,9 +6729,9 @@ async function loadFrom(dir) {
6568
6729
  }
6569
6730
  function loadSettings(dir) {
6570
6731
  const p = join5(dir, ".agent", "settings.json");
6571
- if (!existsSync4(p)) return {};
6732
+ if (!existsSync5(p)) return {};
6572
6733
  try {
6573
- const raw = JSON.parse(readFileSync2(p, "utf8"));
6734
+ const raw = JSON.parse(readFileSync3(p, "utf8"));
6574
6735
  const cfg = {};
6575
6736
  if (raw.mcpServers && typeof raw.mcpServers === "object") cfg.mcpServers = raw.mcpServers;
6576
6737
  if (raw.permissions && typeof raw.permissions === "object") cfg.permissions = raw.permissions;
@@ -6591,8 +6752,8 @@ function loadSettings(dir) {
6591
6752
  }
6592
6753
  }
6593
6754
  async function loadConfig(cwd) {
6594
- const userSettings = loadSettings(homedir3());
6595
- const user = await loadFrom(homedir3());
6755
+ const userSettings = loadSettings(homedir4());
6756
+ const user = await loadFrom(homedir4());
6596
6757
  const projectSettings = loadSettings(cwd);
6597
6758
  const project = await loadFrom(cwd);
6598
6759
  const merged = { ...userSettings, ...user, ...projectSettings, ...project };
@@ -6604,7 +6765,7 @@ async function loadConfig(cwd) {
6604
6765
 
6605
6766
  // cli/hooks-config.ts
6606
6767
  import { spawnSync as spawnSync2 } from "child_process";
6607
- var log14 = forComponent("hooks");
6768
+ var log16 = forComponent("hooks");
6608
6769
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6609
6770
  function ruleMatches(rule, toolName) {
6610
6771
  if (!rule.tool || rule.tool === "*") return true;
@@ -6621,7 +6782,7 @@ function runCmd(rule, env) {
6621
6782
  });
6622
6783
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
6623
6784
  } catch (e) {
6624
- log14.debug(`hook command failed: ${rule.command}`, e);
6785
+ log16.debug(`hook command failed: ${rule.command}`, e);
6625
6786
  return { code: 1, out: String(e?.message ?? e) };
6626
6787
  }
6627
6788
  }
@@ -6725,11 +6886,11 @@ function formatDiff(ops, opts = {}) {
6725
6886
  }
6726
6887
 
6727
6888
  // cli/session.ts
6728
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync, symlinkSync, unlinkSync, readlinkSync } from "fs";
6729
- import { homedir as homedir4 } from "os";
6889
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync, renameSync, symlinkSync, unlinkSync, readlinkSync } from "fs";
6890
+ import { homedir as homedir5 } from "os";
6730
6891
  import { join as join6 } from "path";
6731
- var log15 = forComponent("session");
6732
- var globalDir = () => join6(homedir4(), ".agent", "sessions");
6892
+ var log17 = forComponent("session");
6893
+ var globalDir = () => join6(homedir5(), ".agent", "sessions");
6733
6894
  var SessionStore = class {
6734
6895
  dir;
6735
6896
  constructor(cwd) {
@@ -6741,10 +6902,10 @@ var SessionStore = class {
6741
6902
  const p = (n, w = 2) => String(n).padStart(w, "0");
6742
6903
  const slug2 = (cwd ?? process.cwd()).split("/").pop()?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
6743
6904
  let id = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
6744
- if (existsSync5(this.dir) && existsSync5(join6(this.dir, `${id}.json`))) {
6905
+ if (existsSync6(this.dir) && existsSync6(join6(this.dir, `${id}.json`))) {
6745
6906
  for (let i = 2; i <= 99; i++) {
6746
6907
  const c = `${id}-${i}`;
6747
- if (!existsSync5(join6(this.dir, `${c}.json`))) {
6908
+ if (!existsSync6(join6(this.dir, `${c}.json`))) {
6748
6909
  id = c;
6749
6910
  break;
6750
6911
  }
@@ -6758,43 +6919,43 @@ var SessionStore = class {
6758
6919
  }
6759
6920
  save(data) {
6760
6921
  if (!this.safeId(data.meta.id)) throw new Error(`unsafe session id: ${data.meta.id}`);
6761
- if (!existsSync5(this.dir)) mkdirSync4(this.dir, { recursive: true });
6922
+ if (!existsSync6(this.dir)) mkdirSync4(this.dir, { recursive: true });
6762
6923
  const path = join6(this.dir, `${data.meta.id}.json`);
6763
6924
  const tmp = `${path}.${process.pid}.tmp`;
6764
6925
  writeFileSync3(tmp, JSON.stringify(data));
6765
6926
  renameSync(tmp, path);
6766
6927
  try {
6767
6928
  const gd = globalDir();
6768
- if (!existsSync5(gd)) mkdirSync4(gd, { recursive: true });
6929
+ if (!existsSync6(gd)) mkdirSync4(gd, { recursive: true });
6769
6930
  const link2 = join6(gd, `${data.meta.id}.json`);
6770
- if (!existsSync5(link2)) symlinkSync(path, link2);
6931
+ if (!existsSync6(link2)) symlinkSync(path, link2);
6771
6932
  } catch {
6772
6933
  }
6773
6934
  }
6774
6935
  load(id) {
6775
6936
  if (!this.safeId(id)) {
6776
- log15.debug(`rejecting unsafe session id: ${id}`);
6937
+ log17.debug(`rejecting unsafe session id: ${id}`);
6777
6938
  return void 0;
6778
6939
  }
6779
6940
  const path = join6(this.dir, `${id}.json`);
6780
- if (!existsSync5(path)) return void 0;
6941
+ if (!existsSync6(path)) return void 0;
6781
6942
  try {
6782
- return JSON.parse(readFileSync3(path, "utf8"));
6943
+ return JSON.parse(readFileSync4(path, "utf8"));
6783
6944
  } catch (e) {
6784
- log15.debug(`unreadable session ${id} \u2014 ignoring`, e);
6945
+ log17.debug(`unreadable session ${id} \u2014 ignoring`, e);
6785
6946
  return void 0;
6786
6947
  }
6787
6948
  }
6788
6949
  /** All sessions' metadata, most-recently-updated first. */
6789
6950
  list() {
6790
- if (!existsSync5(this.dir)) return [];
6951
+ if (!existsSync6(this.dir)) return [];
6791
6952
  const metas = [];
6792
6953
  for (const f of readdirSync(this.dir)) {
6793
6954
  if (!f.endsWith(".json")) continue;
6794
6955
  try {
6795
- metas.push(JSON.parse(readFileSync3(join6(this.dir, f), "utf8")).meta);
6956
+ metas.push(JSON.parse(readFileSync4(join6(this.dir, f), "utf8")).meta);
6796
6957
  } catch (e) {
6797
- log15.debug(`skipping unreadable session file ${f}`, e);
6958
+ log17.debug(`skipping unreadable session file ${f}`, e);
6798
6959
  }
6799
6960
  }
6800
6961
  return metas.sort((a, b) => b.updated - a.updated);
@@ -6810,12 +6971,12 @@ var SessionStore = class {
6810
6971
  };
6811
6972
  function globalSessionLoad(idOrPrefix) {
6812
6973
  const gd = globalDir();
6813
- if (!existsSync5(gd)) return void 0;
6974
+ if (!existsSync6(gd)) return void 0;
6814
6975
  const exact = join6(gd, `${idOrPrefix}.json`);
6815
- if (existsSync5(exact)) {
6976
+ if (existsSync6(exact)) {
6816
6977
  try {
6817
6978
  const target = readlinkSync(exact);
6818
- return JSON.parse(readFileSync3(target, "utf8"));
6979
+ return JSON.parse(readFileSync4(target, "utf8"));
6819
6980
  } catch {
6820
6981
  return void 0;
6821
6982
  }
@@ -6826,7 +6987,7 @@ function globalSessionLoad(idOrPrefix) {
6826
6987
  const base = f.slice(0, -5);
6827
6988
  if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
6828
6989
  const target = readlinkSync(join6(gd, f));
6829
- return JSON.parse(readFileSync3(target, "utf8"));
6990
+ return JSON.parse(readFileSync4(target, "utf8"));
6830
6991
  }
6831
6992
  }
6832
6993
  } catch {
@@ -6835,20 +6996,20 @@ function globalSessionLoad(idOrPrefix) {
6835
6996
  }
6836
6997
  function globalSessionList() {
6837
6998
  const gd = globalDir();
6838
- if (!existsSync5(gd)) return [];
6999
+ if (!existsSync6(gd)) return [];
6839
7000
  const metas = [];
6840
7001
  for (const f of readdirSync(gd)) {
6841
7002
  if (!f.endsWith(".json")) continue;
6842
7003
  try {
6843
7004
  const target = readlinkSync(join6(gd, f));
6844
- if (!existsSync5(target)) {
7005
+ if (!existsSync6(target)) {
6845
7006
  try {
6846
7007
  unlinkSync(join6(gd, f));
6847
7008
  } catch {
6848
7009
  }
6849
7010
  continue;
6850
7011
  }
6851
- metas.push(JSON.parse(readFileSync3(target, "utf8")).meta);
7012
+ metas.push(JSON.parse(readFileSync4(target, "utf8")).meta);
6852
7013
  } catch {
6853
7014
  }
6854
7015
  }
@@ -6873,7 +7034,7 @@ var CheckpointStack = class {
6873
7034
  current;
6874
7035
  /** Open a new turn frame (call right before sending a user turn). */
6875
7036
  begin(label) {
6876
- this.current = { label: label.replace(/\s+/g, " ").trim().slice(0, 60) || "(turn)", at: Date.now(), saved: /* @__PURE__ */ new Map() };
7037
+ this.current = { label: sanitizeLabel(label) || "(turn)", at: Date.now(), saved: /* @__PURE__ */ new Map() };
6877
7038
  this.frames.push(this.current);
6878
7039
  if (this.frames.length > this.max) this.frames.shift();
6879
7040
  }
@@ -6901,6 +7062,24 @@ var CheckpointStack = class {
6901
7062
  list() {
6902
7063
  return this.frames.map((f, i) => ({ index: i, label: f.label, at: f.at, files: f.saved.size })).reverse();
6903
7064
  }
7065
+ /** Unified-style diff of all session edits: each file's OLDEST saved content vs its current content. */
7066
+ async diff() {
7067
+ const base = /* @__PURE__ */ new Map();
7068
+ for (const f of this.frames) for (const [path, prior] of f.saved) if (!base.has(path)) base.set(path, prior);
7069
+ const parts = [];
7070
+ for (const [path, prior] of base) {
7071
+ let now5 = null;
7072
+ try {
7073
+ now5 = await this.fs.readFile(path);
7074
+ } catch {
7075
+ }
7076
+ if ((prior ?? "") === (now5 ?? "")) continue;
7077
+ const ops = diffLines(prior ?? "", now5 ?? "");
7078
+ parts.push(`--- ${path}${prior == null ? " (new)" : now5 == null ? " (deleted)" : ""}
7079
+ ${formatDiff(ops)}`);
7080
+ }
7081
+ return parts.join("\n");
7082
+ }
6904
7083
  /**
6905
7084
  * Restore the working tree to BEFORE frame `index` — undo that frame and every later one.
6906
7085
  * Frames are replayed newest→oldest so the OLDEST saved content for a path wins (its true
@@ -6930,12 +7109,12 @@ var CheckpointStack = class {
6930
7109
  };
6931
7110
 
6932
7111
  // cli/gitCheckpoints.ts
6933
- import { execFile } from "child_process";
7112
+ import { execFile as execFile3 } from "child_process";
6934
7113
  import { promisify } from "util";
6935
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
7114
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
6936
7115
  import { join as join7, resolve as resolve2, sep as sep2 } from "path";
6937
- var log16 = forComponent("checkpoints");
6938
- var exec = promisify(execFile);
7116
+ var log18 = forComponent("checkpoints");
7117
+ var exec = promisify(execFile3);
6939
7118
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
6940
7119
  var ShadowRepo = class {
6941
7120
  // undefined = unprobed; false = git/this root unusable
@@ -6961,14 +7140,14 @@ var ShadowRepo = class {
6961
7140
  if (this.ready !== void 0) return this.ready;
6962
7141
  try {
6963
7142
  await exec(this.git, ["--version"]);
6964
- if (!existsSync6(this.gitDir)) {
7143
+ if (!existsSync7(this.gitDir)) {
6965
7144
  mkdirSync5(this.gitDir, { recursive: true });
6966
7145
  await this.run("init", "-q");
6967
7146
  }
6968
7147
  writeFileSync4(join7(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
6969
7148
  this.ready = true;
6970
7149
  } catch (e) {
6971
- log16.debug(`git checkpoints unavailable for ${this.workTree}`, e);
7150
+ log18.debug(`git checkpoints unavailable for ${this.workTree}`, e);
6972
7151
  this.ready = false;
6973
7152
  }
6974
7153
  return this.ready;
@@ -7001,6 +7180,14 @@ var ShadowRepo = class {
7001
7180
  return 0;
7002
7181
  }
7003
7182
  }
7183
+ /** Unified diff between `sha` and the current work-tree (for `/diff`). */
7184
+ async diffSince(sha) {
7185
+ try {
7186
+ return await this.run("diff", sha);
7187
+ } catch {
7188
+ return "";
7189
+ }
7190
+ }
7004
7191
  /** Restore the tree to `sha`; returns counts of reverted tracked files + removed untracked-new. */
7005
7192
  async resetTo(sha) {
7006
7193
  let restored = 0, deleted = 0;
@@ -7031,7 +7218,7 @@ var ShadowRepo = class {
7031
7218
  await this.run("gc", "--auto", "-q").catch(() => {
7032
7219
  });
7033
7220
  } catch (e) {
7034
- log16.debug("checkpoint prune failed", e);
7221
+ log18.debug("checkpoint prune failed", e);
7035
7222
  }
7036
7223
  }
7037
7224
  };
@@ -7088,18 +7275,18 @@ var GitCheckpoints = class {
7088
7275
  use(sessionId) {
7089
7276
  if (sessionId === this.session) return;
7090
7277
  this.session = sessionId;
7091
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log16.debug("re-point failed", e));
7278
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log18.debug("re-point failed", e));
7092
7279
  }
7093
7280
  async begin(label) {
7094
7281
  if (!await this.start()) return;
7095
- const msg = label.replace(/\s+/g, " ").trim().slice(0, 72) || "(turn)";
7282
+ const msg = sanitizeLabel(label, 72) || "(turn)";
7096
7283
  let slow;
7097
7284
  if (!this.snapshotted) slow = setTimeout(() => process.stderr.write("\x1B[2m checkpointing initial workspace snapshot\u2026\x1B[0m\n"), 1500);
7098
7285
  for (const r of this.repos) {
7099
7286
  try {
7100
7287
  await r.commit(msg);
7101
7288
  } catch (e) {
7102
- log16.debug("checkpoint commit failed", e);
7289
+ log18.debug("checkpoint commit failed", e);
7103
7290
  }
7104
7291
  }
7105
7292
  if (slow) clearTimeout(slow);
@@ -7123,6 +7310,16 @@ var GitCheckpoints = class {
7123
7310
  get size() {
7124
7311
  return this.caches[0]?.length ?? 0;
7125
7312
  }
7313
+ /** Unified diff of everything this session changed: oldest session checkpoint → current tree, across all roots. */
7314
+ async diff() {
7315
+ if (!this.caches[0]?.length) await this.refresh();
7316
+ const parts = [];
7317
+ for (let i = 0; i < this.repos.length; i++) {
7318
+ const base = this.caches[i]?.[0];
7319
+ if (base) parts.push(await this.repos[i].diffSince(base.sha));
7320
+ }
7321
+ return parts.filter(Boolean).join("\n");
7322
+ }
7126
7323
  async rewindTo(index) {
7127
7324
  if (!this.caches[0]?.length) await this.refresh();
7128
7325
  if (index < 0 || index >= (this.caches[0]?.length ?? 0)) throw new Error("no such checkpoint");
@@ -7156,8 +7353,8 @@ var GitCheckpointsOptions = class {
7156
7353
  };
7157
7354
 
7158
7355
  // cli/permissions.ts
7159
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
7160
- import { homedir as homedir5 } from "os";
7356
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
7357
+ import { homedir as homedir6 } from "os";
7161
7358
  import { join as join8 } from "path";
7162
7359
  var RULE_RE = /^(\w+)(?:\((.+)\))?$/;
7163
7360
  function parseOne(raw, decision) {
@@ -7182,25 +7379,15 @@ function describeRule(r) {
7182
7379
  }
7183
7380
  var PERM_FILE = (cwd) => join8(cwd, ".agent", "permissions.json");
7184
7381
  function loadPersistedRules(cwd) {
7185
- const p = PERM_FILE(cwd);
7186
- if (!existsSync7(p)) return {};
7187
- try {
7188
- const j = JSON.parse(readFileSync4(p, "utf8"));
7189
- return { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] };
7190
- } catch {
7191
- return {};
7192
- }
7382
+ const j = readJsonFile(PERM_FILE(cwd), null);
7383
+ return j ? { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] } : {};
7193
7384
  }
7194
- function loadClaudeSettings(cwd, home = homedir5()) {
7385
+ function loadClaudeSettings(cwd, home = homedir6()) {
7195
7386
  const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
7196
7387
  let out = {};
7197
7388
  for (const p of files) {
7198
- if (!existsSync7(p)) continue;
7199
- try {
7200
- const perms = JSON.parse(readFileSync4(p, "utf8"))?.permissions;
7201
- if (perms) out = mergePerms(out, { allow: perms.allow, ask: perms.ask, deny: perms.deny }) ?? out;
7202
- } catch {
7203
- }
7389
+ const perms = readJsonFile(p, null)?.permissions;
7390
+ if (perms) out = mergePerms(out, { allow: perms.allow, ask: perms.ask, deny: perms.deny }) ?? out;
7204
7391
  }
7205
7392
  return out;
7206
7393
  }
@@ -7223,20 +7410,14 @@ function mergePerms(a, b) {
7223
7410
  }
7224
7411
  return Object.keys(out).length ? out : void 0;
7225
7412
  }
7226
- var TRUST_FILE = join8(homedir5(), ".agent", "trusted.json");
7413
+ var TRUST_FILE = join8(homedir6(), ".agent", "trusted.json");
7227
7414
  function isTrusted(cwd, file = TRUST_FILE) {
7228
- try {
7229
- return existsSync7(file) && JSON.parse(readFileSync4(file, "utf8")).includes(cwd);
7230
- } catch {
7231
- return false;
7232
- }
7415
+ const list = readJsonFile(file, []);
7416
+ return Array.isArray(list) && list.includes(cwd);
7233
7417
  }
7234
7418
  function trustDir(cwd, file = TRUST_FILE) {
7235
- let list = [];
7236
- try {
7237
- if (existsSync7(file)) list = JSON.parse(readFileSync4(file, "utf8"));
7238
- } catch {
7239
- }
7419
+ let list = readJsonFile(file, []);
7420
+ if (!Array.isArray(list)) list = [];
7240
7421
  if (!list.includes(cwd)) list.push(cwd);
7241
7422
  try {
7242
7423
  mkdirSync6(join8(file, ".."), { recursive: true });
@@ -7291,14 +7472,18 @@ function completePath(listDir, ref) {
7291
7472
  for (const e of entries) {
7292
7473
  if (!fuzzy(base, e.name)) continue;
7293
7474
  if (e.name.startsWith(".") && !base.startsWith(".")) continue;
7294
- const rel = dir ? `${dir}/${e.name}` : e.name;
7295
- matched.push("@" + rel + (e.dir ? "/" : ""));
7475
+ const rel = (dir ? `${dir}/${e.name}` : e.name) + (e.dir ? "/" : "");
7476
+ matched.push(/\s/.test(rel) ? `@"${rel}"` : "@" + rel);
7296
7477
  }
7297
7478
  return rank(matched, "@" + (dir ? dir + "/" : "") + base);
7298
7479
  }
7299
7480
 
7300
7481
  // cli/lineEditor.ts
7301
7482
  import { emitKeypressEvents } from "readline";
7483
+ import { spawnSync as spawnSync3 } from "child_process";
7484
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
7485
+ import { tmpdir as tmpdir2 } from "os";
7486
+ import { join as join9 } from "path";
7302
7487
 
7303
7488
  // cli/bidi.ts
7304
7489
  var RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
@@ -8138,6 +8323,30 @@ function createLineEditor(out) {
8138
8323
  if (cursorCol > 0) out.write(`\x1B[${cursorCol}C`);
8139
8324
  curRow = cursorRow;
8140
8325
  }
8326
+ function externalEdit(s) {
8327
+ const spec = process.env.VISUAL || process.env.EDITOR || "vi";
8328
+ const [cmd, ...cargs] = spec.split(" ").filter(Boolean);
8329
+ const file = join9(tmpdir2(), `agentx-edit-${process.pid}-${Date.now()}.md`);
8330
+ try {
8331
+ writeFileSync6(file, s.buf);
8332
+ process.stdin.setRawMode(false);
8333
+ out.write("\x1B[?2004l");
8334
+ const r = spawnSync3(cmd, [...cargs, file], { stdio: "inherit" });
8335
+ if (r.status === 0) {
8336
+ const text = readFileSync5(file, "utf8").replace(/\n$/, "");
8337
+ s.reset();
8338
+ if (text) s.insert(text);
8339
+ }
8340
+ } catch {
8341
+ } finally {
8342
+ try {
8343
+ unlinkSync2(file);
8344
+ } catch {
8345
+ }
8346
+ process.stdin.setRawMode(true);
8347
+ out.write("\x1B[?2004h");
8348
+ }
8349
+ }
8141
8350
  async function readLine(opts) {
8142
8351
  const maxVisible = opts.maxVisible ?? 8;
8143
8352
  if (!isTTY) return readPlainLine();
@@ -8174,6 +8383,7 @@ function createLineEditor(out) {
8174
8383
  lastStatus = cur;
8175
8384
  redraw();
8176
8385
  }, opts.statusTickMs) : void 0;
8386
+ let chordCtrlX = false;
8177
8387
  const onKey = (str, key) => {
8178
8388
  if (key?.ctrl && key.name === "l") {
8179
8389
  out.write("\x1B[2J\x1B[3J\x1B[H");
@@ -8181,6 +8391,19 @@ function createLineEditor(out) {
8181
8391
  redraw();
8182
8392
  return;
8183
8393
  }
8394
+ if (key?.ctrl && key.name === "x" && !s.pasting) {
8395
+ chordCtrlX = true;
8396
+ return;
8397
+ }
8398
+ if (chordCtrlX) {
8399
+ chordCtrlX = false;
8400
+ if (key?.ctrl && key.name === "e") {
8401
+ externalEdit(s);
8402
+ curRow = 0;
8403
+ redraw();
8404
+ return;
8405
+ }
8406
+ }
8184
8407
  if (key?.name === "tab" && key.shift && opts.onCyclePosture) {
8185
8408
  opts.onCyclePosture();
8186
8409
  redraw();
@@ -8686,6 +8909,184 @@ var MarkdownStream = class {
8686
8909
  }
8687
8910
  };
8688
8911
 
8912
+ // cli/osScheduler.ts
8913
+ import { spawnSync as spawnSync4 } from "child_process";
8914
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync8 } from "fs";
8915
+ import { homedir as homedir7 } from "os";
8916
+ import { join as join10 } from "path";
8917
+ var log19 = forComponent("os-sched");
8918
+ var OsScheduler = class {
8919
+ options;
8920
+ constructor(options) {
8921
+ this.options = { ...new OsSchedulerOptions(), ...options };
8922
+ }
8923
+ get dir() {
8924
+ return join10(this.options.home, ".agent", "sched");
8925
+ }
8926
+ label(id) {
8927
+ return `cc.livx.agentx.sched-${id}`;
8928
+ }
8929
+ plistPath(id) {
8930
+ return join10(this.options.home, "Library", "LaunchAgents", `${this.label(id)}.plist`);
8931
+ }
8932
+ run(cmd, args, input) {
8933
+ return this.options.exec(cmd, args, input);
8934
+ }
8935
+ available() {
8936
+ return this.options.platform === "darwin" || this.options.platform === "linux";
8937
+ }
8938
+ /** Register the job with the OS. Returns a human description of the mechanism used. Throws on failure. */
8939
+ schedule(spec) {
8940
+ if (!this.available()) throw new Error(`no OS scheduler on ${this.options.platform}`);
8941
+ mkdirSync7(this.dir, { recursive: true });
8942
+ const oneOff = "at" in spec.trigger;
8943
+ const script = this.writeScript(spec, oneOff);
8944
+ const mechanism = this.options.platform === "darwin" ? this.scheduleDarwin(spec, script) : this.scheduleLinux(spec, script, oneOff);
8945
+ const meta = { ...spec, created: Date.now(), mechanism };
8946
+ writeFileSync7(join10(this.dir, `${spec.id}.json`), JSON.stringify(meta, null, 2));
8947
+ return mechanism;
8948
+ }
8949
+ cancel(id) {
8950
+ const meta = readJsonFile(join10(this.dir, `${id}.json`), null);
8951
+ if (!meta) return false;
8952
+ try {
8953
+ if (this.options.platform === "darwin") {
8954
+ try {
8955
+ this.run("launchctl", ["remove", this.label(id)]);
8956
+ } catch {
8957
+ }
8958
+ try {
8959
+ unlinkSync3(this.plistPath(id));
8960
+ } catch {
8961
+ }
8962
+ } else if (meta.mechanism.startsWith("crontab")) {
8963
+ const cur = (() => {
8964
+ try {
8965
+ return this.run("crontab", ["-l"]);
8966
+ } catch {
8967
+ return "";
8968
+ }
8969
+ })();
8970
+ const next = cur.split("\n").filter((l) => !l.includes(`# agentx-sched-${id}`)).join("\n");
8971
+ this.run("crontab", ["-"], next.trim() ? next.trimEnd() + "\n" : "");
8972
+ } else if (meta.mechanism.startsWith("at:")) {
8973
+ try {
8974
+ this.run("atrm", [meta.mechanism.slice(3)]);
8975
+ } catch {
8976
+ }
8977
+ }
8978
+ } catch (e) {
8979
+ log19.debug(`cancel ${id}`, e);
8980
+ }
8981
+ for (const f of [`${id}.json`, `${id}.sh`]) {
8982
+ try {
8983
+ unlinkSync3(join10(this.dir, f));
8984
+ } catch {
8985
+ }
8986
+ }
8987
+ return true;
8988
+ }
8989
+ list() {
8990
+ if (!existsSync8(this.dir)) return [];
8991
+ return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(join10(this.dir, f), null)).filter(Boolean);
8992
+ }
8993
+ /** The per-job runner script: cd to the project, headless-resume the session, log, notify. */
8994
+ writeScript(spec, oneOff) {
8995
+ const p = join10(this.dir, `${spec.id}.sh`);
8996
+ const q2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
8997
+ const cleanup = oneOff ? this.options.platform === "darwin" ? `launchctl remove ${this.label(spec.id)} 2>/dev/null; rm -f ${q2(this.plistPath(spec.id))} ${q2(join10(this.dir, `${spec.id}.json`))} ${q2(p)}
8998
+ ` : `rm -f ${q2(join10(this.dir, `${spec.id}.json`))} ${q2(p)}
8999
+ ` : "";
9000
+ writeFileSync7(p, `#!/bin/sh
9001
+ # agentx scheduled job ${spec.id}${spec.label ? ` \u2014 ${spec.label}` : ""}
9002
+ cd ${q2(spec.cwd)} || exit 1
9003
+ ${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(join10(this.dir, `${spec.id}.log`))} 2>&1
9004
+ ${cleanup}`);
9005
+ chmodSync(p, 493);
9006
+ return p;
9007
+ }
9008
+ scheduleDarwin(spec, script) {
9009
+ const t = spec.trigger;
9010
+ let trigger;
9011
+ if ("everyMs" in t) {
9012
+ trigger = `<key>StartInterval</key><integer>${Math.max(60, Math.round(t.everyMs / 1e3))}</integer>`;
9013
+ } else if ("at" in t) {
9014
+ const d = new Date(t.at);
9015
+ trigger = `<key>StartCalendarInterval</key><dict><key>Minute</key><integer>${d.getMinutes()}</integer><key>Hour</key><integer>${d.getHours()}</integer><key>Day</key><integer>${d.getDate()}</integer><key>Month</key><integer>${d.getMonth() + 1}</integer></dict>`;
9016
+ } else {
9017
+ const f = parseCron(t.cron);
9018
+ const dict = [];
9019
+ const put = (key, vals, full) => {
9020
+ if (vals.length === full) return;
9021
+ if (vals.length !== 1) throw new Error(`macOS launchd supports only single-value cron fields (got "${t.cron}") \u2014 use {everyMs} or a simpler cron`);
9022
+ dict.push(`<key>${key}</key><integer>${vals[0]}</integer>`);
9023
+ };
9024
+ put("Minute", f.minute, 60);
9025
+ put("Hour", f.hour, 24);
9026
+ put("Day", f.dom, 31);
9027
+ put("Month", f.month, 12);
9028
+ put("Weekday", f.dow, 7);
9029
+ trigger = `<key>StartCalendarInterval</key><dict>${dict.join("")}</dict>`;
9030
+ }
9031
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
9032
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9033
+ <plist version="1.0"><dict>
9034
+ <key>Label</key><string>${this.label(spec.id)}</string>
9035
+ <key>ProgramArguments</key><array><string>/bin/sh</string><string>${script}</string></array>
9036
+ ${trigger}
9037
+ <key>RunAtLoad</key><false/>
9038
+ </dict></plist>
9039
+ `;
9040
+ mkdirSync7(join10(this.options.home, "Library", "LaunchAgents"), { recursive: true });
9041
+ writeFileSync7(this.plistPath(spec.id), plist);
9042
+ this.run("launchctl", ["load", this.plistPath(spec.id)]);
9043
+ return `launchd:${this.label(spec.id)}`;
9044
+ }
9045
+ scheduleLinux(spec, script, oneOff) {
9046
+ const t = spec.trigger;
9047
+ if (oneOff) {
9048
+ const d = new Date("at" in t ? t.at : Date.now());
9049
+ const stamp = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")} ${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
9050
+ const out = this.run("at", [stamp], `/bin/sh ${script}
9051
+ `);
9052
+ const jobId = /job (\d+)/.exec(out)?.[1] ?? "";
9053
+ return `at:${jobId}`;
9054
+ }
9055
+ const expr = "cron" in t ? t.cron : `*/${Math.max(1, Math.round(t.everyMs / 6e4))} * * * *`;
9056
+ parseCron(expr);
9057
+ const cur = (() => {
9058
+ try {
9059
+ return this.run("crontab", ["-l"]);
9060
+ } catch {
9061
+ return "";
9062
+ }
9063
+ })();
9064
+ this.run("crontab", ["-"], `${cur.trimEnd()}
9065
+ ${expr} /bin/sh ${script} # agentx-sched-${spec.id}
9066
+ `.replace(/^\n/, ""));
9067
+ return `crontab:${expr}`;
9068
+ }
9069
+ };
9070
+ var OsSchedulerOptions = class {
9071
+ platform = process.platform;
9072
+ home = homedir7();
9073
+ /** How the fired job invokes the CLI — a RAW shell snippet (may be `bun /path/cli.ts`). Default: `agentx` on PATH. */
9074
+ agentx = "agentx";
9075
+ /** Injectable executor for tests. Returns stdout+stderr merged — `at` reports "job N" on STDERR,
9076
+ * and the job id is what atrm needs for cancel. Throws on non-zero exit. */
9077
+ exec = (cmd, args, input) => {
9078
+ const r = spawnSync4(cmd, args, { input, encoding: "utf8" });
9079
+ if (r.error) throw r.error;
9080
+ if (r.status !== 0) throw new Error(`${cmd} exited ${r.status}: ${r.stderr || r.stdout}`);
9081
+ return `${r.stdout ?? ""}${r.stderr ?? ""}`;
9082
+ };
9083
+ };
9084
+ function routeTrigger(trigger, backendHint, now5 = Date.now()) {
9085
+ if (backendHint === "os") return "os";
9086
+ if (backendHint === "session") return "session";
9087
+ return "at" in trigger && trigger.at - now5 >= 30 * 6e4 ? "os" : "session";
9088
+ }
9089
+
8689
9090
  // cli/cli.ts
8690
9091
  var forceColor = process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "" && process.env.FORCE_COLOR !== "0";
8691
9092
  var useColor = forceColor || !process.env.NO_COLOR && !!process.stdout.isTTY && !!process.stderr.isTTY;
@@ -8701,10 +9102,10 @@ var italic = C("3");
8701
9102
  var strike = C("9");
8702
9103
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
8703
9104
  var err = (s) => process.stderr.write(s);
8704
- var log17 = forComponent("cli");
9105
+ var log20 = forComponent("cli");
8705
9106
  var VERSION = (() => {
8706
9107
  try {
8707
- return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
9108
+ return JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
8708
9109
  } catch {
8709
9110
  return "?";
8710
9111
  }
@@ -8905,7 +9306,7 @@ REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \
8905
9306
  REPL slash commands: /help /version /tools /permissions /status /cost /context /transcript /doctor /cwd /model /reasoning /config /rename /compact /memory /rewind /undo /clear /sessions /resume /commands /skills /reload /mcp /init /export /paste /goal /exit (duplex: /act /think /tasks /voice /voice-model /think-model)
8906
9307
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8907
9308
  REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
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.
9309
+ REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 Ctrl+X Ctrl+E edits the buffer in $EDITOR \xB7 \u2192 or Tab accepts the dim history ghost-suggestion \xB7 Alt+S/Ctrl+S stash/unstash.
8909
9310
  REPL stash: type while a turn is running \u2192 Enter queues it (auto-submits when the turn finishes). Alt+S (or Ctrl+S) with text stashes it; on an empty prompt pops the next entry for editing.
8910
9311
  REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Ctrl-_ undo \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
8911
9312
  REPL paste: large/multi-line pastes collapse to a [Pasted text +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; Ctrl-V or /paste grabs a clipboard image (macOS).`;
@@ -8925,11 +9326,11 @@ function resolveModelOrNewest(model) {
8925
9326
  var ENV_KEY_ALIASES = { google: ["GEMINI_API_KEY"] };
8926
9327
  function loadInstallEnv() {
8927
9328
  let dir = dirname4(import.meta.path);
8928
- for (let i = 0; i < 5 && !existsSync8(join9(dir, "package.json")); i++) dir = dirname4(dir);
9329
+ for (let i = 0; i < 5 && !existsSync9(join11(dir, "package.json")); i++) dir = dirname4(dir);
8929
9330
  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")) {
9331
+ const file = join11(dir, name);
9332
+ if (!existsSync9(file)) continue;
9333
+ for (const line of readFileSync6(file, "utf8").split("\n")) {
8933
9334
  const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
8934
9335
  if (!m || m[1] in process.env) continue;
8935
9336
  let val = m[2].trim();
@@ -9237,7 +9638,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
9237
9638
  function turnCost(model, usage) {
9238
9639
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
9239
9640
  }
9240
- async function evaluateGoal(ai, condition, transcript, log18) {
9641
+ async function evaluateGoal(ai, condition, transcript, log21) {
9241
9642
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
9242
9643
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
9243
9644
  return text.slice(0, 600);
@@ -9257,7 +9658,7 @@ ${recent}` }
9257
9658
  const match = r.content.match(/\{[\s\S]*\}/);
9258
9659
  if (match) return JSON.parse(match[0]);
9259
9660
  } catch (e) {
9260
- log18(dim(` (goal evaluator error: ${e?.message ?? e})
9661
+ log21(dim(` (goal evaluator error: ${e?.message ?? e})
9261
9662
  `));
9262
9663
  }
9263
9664
  return { met: false, reason: "evaluation unclear" };
@@ -9444,19 +9845,22 @@ async function mountMcp(cfg, oauth) {
9444
9845
  return mounted;
9445
9846
  }
9446
9847
  async function closeMcp(mounted) {
9447
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log17.debug("mcp close failed", e))));
9848
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log20.debug("mcp close failed", e))));
9448
9849
  }
9449
9850
  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;
9851
+ function mentionRefs(line) {
9852
+ return [...line.matchAll(/(?:^|\s)@(?:"([^"]+)"|(\S+))/g)].map((m) => m[1] ?? m[2].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9853
+ }
9854
+ var untilde = (p) => p.startsWith("~/") ? join11(homedir8(), p.slice(2)) : p;
9451
9855
  function readImageParts(cwd, line) {
9452
- const refs = [...line.matchAll(/(?:^|\s)@(\S+)/g)].map((m) => m[1].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9856
+ const refs = mentionRefs(line);
9453
9857
  const parts = [];
9454
9858
  for (const ref of refs) {
9455
9859
  const mime = IMG_EXT[extname(ref).toLowerCase()];
9456
9860
  if (!mime) continue;
9457
9861
  const abs = ref.startsWith("~/") ? untilde(ref) : resolve3(cwd, ref);
9458
9862
  try {
9459
- parts.push(imagePart(`data:${mime};base64,${readFileSync5(abs).toString("base64")}`));
9863
+ parts.push(imagePart(`data:${mime};base64,${readFileSync6(abs).toString("base64")}`));
9460
9864
  } catch {
9461
9865
  }
9462
9866
  }
@@ -9467,7 +9871,6 @@ function pastePathClassifier(cwd) {
9467
9871
  let t = text.trim();
9468
9872
  if (!t || t.includes("\n")) return null;
9469
9873
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
9470
- if (/\s/.test(t)) return null;
9471
9874
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
9472
9875
  const abs = t.startsWith("~/") ? untilde(t) : resolve3(cwd, t);
9473
9876
  try {
@@ -9476,7 +9879,7 @@ function pastePathClassifier(cwd) {
9476
9879
  return null;
9477
9880
  }
9478
9881
  const isImg = !!IMG_EXT[extname(abs).toLowerCase()];
9479
- return { display: isImg ? "Image" : `File ${basename2(abs)}`, ref: "@" + abs };
9882
+ return { display: isImg ? "Image" : `File ${basename2(abs)}`, ref: /\s/.test(abs) ? `@"${abs}"` : "@" + abs };
9480
9883
  };
9481
9884
  }
9482
9885
  var mcpMentionResolver;
@@ -9484,7 +9887,7 @@ function setMcpMentionResolver(fn) {
9484
9887
  mcpMentionResolver = fn;
9485
9888
  }
9486
9889
  async function expandMentions(fs, line) {
9487
- const refs = [...line.matchAll(/(?:^|\s)@(\S+)/g)].map((m) => m[1].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
9890
+ const refs = mentionRefs(line);
9488
9891
  if (!refs.length) return { text: line, loaded: [], missing: [] };
9489
9892
  const loaded = [], missing = [], blocks = [];
9490
9893
  for (const ref of refs) {
@@ -9492,7 +9895,7 @@ async function expandMentions(fs, line) {
9492
9895
  if (loaded.includes(ref) || missing.includes(ref)) continue;
9493
9896
  if (ref.includes(":") && mcpMentionResolver) {
9494
9897
  const body = await mcpMentionResolver(ref).catch((e) => {
9495
- log17.debug("mcp mention resolve failed", e);
9898
+ log20.debug("mcp mention resolve failed", e);
9496
9899
  return null;
9497
9900
  });
9498
9901
  if (body != null) {
@@ -9670,25 +10073,25 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
9670
10073
  `;
9671
10074
  function initInstructions(cwd) {
9672
10075
  for (const f of ["AGENTS.md", "CLAUDE.md"]) {
9673
- if (existsSync8(join9(cwd, f))) {
10076
+ if (existsSync9(join11(cwd, f))) {
9674
10077
  err(yellow(` ${f} already exists \u2014 leaving it as-is
9675
10078
  `));
9676
10079
  return;
9677
10080
  }
9678
10081
  }
9679
- const path = join9(cwd, "AGENTS.md");
9680
- writeFileSync6(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
10082
+ const path = join11(cwd, "AGENTS.md");
10083
+ writeFileSync8(path, AGENTS_MD_TEMPLATE.replace("${name}", basename2(cwd)));
9681
10084
  err(green(` created ${path}
9682
10085
  `) + dim(" edit it, then it auto-loads into every run.\n"));
9683
10086
  }
9684
10087
  function persistSetting(cwd, key, value) {
9685
- const path = join9(cwd, ".agent", "settings.json");
10088
+ const path = join11(cwd, ".agent", "settings.json");
9686
10089
  try {
9687
- const obj = existsSync8(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
10090
+ const obj = existsSync9(path) ? JSON.parse(readFileSync6(path, "utf8")) : {};
9688
10091
  if (obj[key] === value) return;
9689
10092
  obj[key] = value;
9690
- mkdirSync7(dirname4(path), { recursive: true });
9691
- writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
10093
+ mkdirSync8(dirname4(path), { recursive: true });
10094
+ writeFileSync8(path, JSON.stringify(obj, null, 2) + "\n");
9692
10095
  } catch (e) {
9693
10096
  err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
9694
10097
  `));
@@ -9704,14 +10107,14 @@ var isCancelTeardown = (e) => {
9704
10107
  function installCancelGuards(mounted) {
9705
10108
  process.on("unhandledRejection", (e) => {
9706
10109
  if (isCancelTeardown(e)) {
9707
- log17.debug("suppressed unhandledRejection (cursor stream cancel)", e);
10110
+ log20.debug("suppressed unhandledRejection (cursor stream cancel)", e);
9708
10111
  return;
9709
10112
  }
9710
- log17.error("unhandledRejection", e);
10113
+ log20.error("unhandledRejection", e);
9711
10114
  });
9712
10115
  process.on("uncaughtException", (e) => {
9713
10116
  if (isCancelTeardown(e)) {
9714
- log17.debug("suppressed uncaughtException (cursor stream cancel)", e);
10117
+ log20.debug("suppressed uncaughtException (cursor stream cancel)", e);
9715
10118
  return;
9716
10119
  }
9717
10120
  console.error(e);
@@ -9720,7 +10123,7 @@ function installCancelGuards(mounted) {
9720
10123
  });
9721
10124
  }
9722
10125
  async function repl(args, ai, cfg, cwd) {
9723
- const oauth = new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") });
10126
+ const oauth = new McpOAuth({ storePath: join11(cwd, ".agent", "mcp-auth.json") });
9724
10127
  const mounted = await mountMcp(cfg, oauth);
9725
10128
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
9726
10129
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
@@ -9740,7 +10143,19 @@ async function repl(args, ai, cfg, cwd) {
9740
10143
  },
9741
10144
  tickMs: 15e3
9742
10145
  });
9743
- agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler)];
10146
+ const agentxInvocation = () => process.argv[1] ? `${process.execPath} ${resolve3(process.argv[1])}` : "agentx";
10147
+ const osSched = new OsScheduler({ agentx: agentxInvocation() });
10148
+ const osBackend = osSched.available() ? {
10149
+ get sessionId() {
10150
+ return session.meta.id;
10151
+ },
10152
+ cwd,
10153
+ route: (t, hint) => routeTrigger(t, hint),
10154
+ schedule: (spec) => osSched.schedule(spec),
10155
+ cancel: (id) => osSched.cancel(id),
10156
+ list: () => osSched.list()
10157
+ } : void 0;
10158
+ agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler, osBackend)];
9744
10159
  const duplex = args.duplex;
9745
10160
  let dx;
9746
10161
  let voiceIO;
@@ -9902,7 +10317,7 @@ async function repl(args, ai, cfg, cwd) {
9902
10317
  quickLook: {
9903
10318
  branch: () => {
9904
10319
  try {
9905
- const head = readFileSync5(join9(cwd, ".git", "HEAD"), "utf8").trim();
10320
+ const head = readFileSync6(join11(cwd, ".git", "HEAD"), "utf8").trim();
9906
10321
  return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
9907
10322
  } catch {
9908
10323
  return "not a git repository";
@@ -10024,9 +10439,9 @@ async function repl(args, ai, cfg, cwd) {
10024
10439
  };
10025
10440
  const pendingImages = [];
10026
10441
  const grabClipboardAttachment = () => {
10027
- const dir = join9(tmpdir2(), "agentx-pasted");
10442
+ const dir = join11(tmpdir3(), "agentx-pasted");
10028
10443
  try {
10029
- mkdirSync7(dir, { recursive: true });
10444
+ mkdirSync8(dir, { recursive: true });
10030
10445
  } catch {
10031
10446
  }
10032
10447
  const img = grabClipboardImage(dir, String(Date.now()));
@@ -10076,7 +10491,7 @@ async function repl(args, ai, cfg, cwd) {
10076
10491
  err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
10077
10492
  `));
10078
10493
  }
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 });
10494
+ const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join11(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
10080
10495
  const cpHooks = checkpoints.hooks?.();
10081
10496
  if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
10082
10497
  duplexPersist = () => {
@@ -10103,7 +10518,7 @@ async function repl(args, ai, cfg, cwd) {
10103
10518
  const fs = agent.options.fs;
10104
10519
  const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
10105
10520
  const adot = (sub) => `${fsBase}/.agent/${sub}`;
10106
- const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir6()}/.agent/${sub}`, `${homedir6()}/.claude/${sub}`];
10521
+ const adots = (sub) => dotDirs(fsBase, sub);
10107
10522
  const cmds = (await loadCommands(fs, adots("commands"))).commands;
10108
10523
  const skills = (await loadSkills(fs, adots("skills"))).skills;
10109
10524
  const refreshCatalogs = async () => {
@@ -10125,14 +10540,14 @@ ${lines.join("\n")}
10125
10540
  Added entries are loadable now via the Skill/SlashCommand tools; removed ones are gone even if still listed in the system prompt.
10126
10541
  </system-reminder>`;
10127
10542
  };
10128
- const histPath = join9(cwd, ".agent", "history");
10129
- const history = existsSync8(histPath) ? readFileSync5(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
10543
+ const histPath = join11(cwd, ".agent", "history");
10544
+ const history = existsSync9(histPath) ? readFileSync6(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
10130
10545
  const remember = (line) => {
10131
10546
  try {
10132
- mkdirSync7(join9(cwd, ".agent"), { recursive: true });
10547
+ mkdirSync8(join11(cwd, ".agent"), { recursive: true });
10133
10548
  appendFileSync(histPath, line + "\n");
10134
10549
  } catch (e) {
10135
- log17.debug("history write failed", e);
10550
+ log20.debug("history write failed", e);
10136
10551
  }
10137
10552
  };
10138
10553
  const ago = (t) => {
@@ -10202,7 +10617,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
10202
10617
  try {
10203
10618
  store.save(session);
10204
10619
  } catch (e) {
10205
- log17.debug("session save after rewind failed", e);
10620
+ log20.debug("session save after rewind failed", e);
10206
10621
  }
10207
10622
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
10208
10623
  `));
@@ -10230,7 +10645,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
10230
10645
  const announcedTasks = /* @__PURE__ */ new Set();
10231
10646
  const turn = async (task) => {
10232
10647
  const delta = await refreshCatalogs().catch((e) => {
10233
- log17.debug("catalog refresh failed", e);
10648
+ log20.debug("catalog refresh failed", e);
10234
10649
  return "";
10235
10650
  });
10236
10651
  if (delta) {
@@ -10425,8 +10840,8 @@ ${extra}` : body);
10425
10840
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10426
10841
  if (wasRaw) process.stdin.setRawMode(false);
10427
10842
  try {
10428
- const { spawnSync: spawnSync3 } = await import("child_process");
10429
- const r = spawnSync3("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
10843
+ const { spawnSync: spawnSync5 } = await import("child_process");
10844
+ const r = spawnSync5("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
10430
10845
  if (r.error) err(text);
10431
10846
  } finally {
10432
10847
  if (wasRaw) process.stdin.setRawMode(true);
@@ -10446,13 +10861,13 @@ ${extra}` : body);
10446
10861
  keys.length ? ok(`provider keys: ${keys.join(", ")}`) : bad("no provider keys set (ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY)");
10447
10862
  const info = getModelInfo(work.model);
10448
10863
  info?.pricing ? ok(`model ${work.model} \u2014 priced (${info.pricing.inputCostPer1K}/${info.pricing.outputCostPer1K} per 1k in/out)`) : warn(`model ${work.model} \u2014 no pricing in the catalog (costs will show ~$0; verify the id)`);
10449
- const cfgFiles = ["ts", "js", "json"].flatMap((e) => [`${cwd}/.agent/config.${e}`, `${homedir6()}/.agent/config.${e}`]).filter((p) => existsSync8(p));
10864
+ const cfgFiles = ["ts", "js", "json"].flatMap((e) => [`${cwd}/.agent/config.${e}`, `${homedir8()}/.agent/config.${e}`]).filter((p) => existsSync9(p));
10450
10865
  cfgFiles.length ? ok(`config: ${cfgFiles.join(", ")}`) : warn("no .agent/config.* found (project or ~) \u2014 running on defaults");
10451
10866
  try {
10452
10867
  const probe = `${cwd}/.agent/sessions/.doctor-probe`;
10453
- mkdirSync7(`${cwd}/.agent/sessions`, { recursive: true });
10454
- writeFileSync6(probe, "ok");
10455
- unlinkSync2(probe);
10868
+ mkdirSync8(`${cwd}/.agent/sessions`, { recursive: true });
10869
+ writeFileSync8(probe, "ok");
10870
+ unlinkSync4(probe);
10456
10871
  ok(`session store writable (${cwd}/.agent/sessions)`);
10457
10872
  } catch (e) {
10458
10873
  bad(`session store not writable: ${e?.message ?? e}`);
@@ -10479,7 +10894,7 @@ ${extra}` : body);
10479
10894
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
10480
10895
  run: async () => {
10481
10896
  await refreshCatalogs().catch((e) => {
10482
- log17.debug("catalog refresh failed", e);
10897
+ log20.debug("catalog refresh failed", e);
10483
10898
  });
10484
10899
  face.reprepare();
10485
10900
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -10745,6 +11160,47 @@ ${extra}` : body);
10745
11160
  `));
10746
11161
  }
10747
11162
  },
11163
+ copy: {
11164
+ desc: "copy the last reply to the clipboard \u2014 /copy code = last code block only",
11165
+ run: (a) => {
11166
+ const last = [...face.transcript].reverse().find((m) => m.role === "assistant" && contentText(m.content).trim());
11167
+ if (!last) {
11168
+ err(dim(" (nothing to copy yet)\n"));
11169
+ return;
11170
+ }
11171
+ let text = contentText(last.content).trim();
11172
+ if (a[0] === "code") {
11173
+ const fences = [...text.matchAll(/```[^\n]*\n([\s\S]*?)```/g)];
11174
+ if (!fences.length) {
11175
+ err(dim(" (no code block in the last reply)\n"));
11176
+ return;
11177
+ }
11178
+ text = fences[fences.length - 1][1].trimEnd();
11179
+ }
11180
+ err(dim(copyTextToClipboard(text) ? ` \u2713 copied ${text.length} chars
11181
+ ` : " no clipboard tool found (pbcopy/wl-copy/xclip)\n"));
11182
+ }
11183
+ },
11184
+ diff: {
11185
+ desc: "show all file changes this session (oldest checkpoint \u2192 now)",
11186
+ run: async () => {
11187
+ if (!checkpoints.diff) {
11188
+ err(dim(" (diff not supported by this checkpoint backend)\n"));
11189
+ return;
11190
+ }
11191
+ await checkpoints.refresh?.();
11192
+ if (!checkpoints.size) {
11193
+ err(dim(" (no checkpoints yet \u2014 make a turn first)\n"));
11194
+ return;
11195
+ }
11196
+ const d = (await checkpoints.diff()).trim();
11197
+ if (!d) {
11198
+ err(dim(" (no file changes this session)\n"));
11199
+ return;
11200
+ }
11201
+ err(d.split("\n").map((l) => l.startsWith("+") && !l.startsWith("+++") ? green(l) : l.startsWith("-") && !l.startsWith("---") ? red(l) : dim(l)).join("\n") + "\n");
11202
+ }
11203
+ },
10748
11204
  memory: {
10749
11205
  desc: "open the memory index in $EDITOR (.agent/memory/MEMORY.md)",
10750
11206
  run: async () => {
@@ -10775,8 +11231,8 @@ ${extra}` : body);
10775
11231
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10776
11232
  if (wasRaw) process.stdin.setRawMode(false);
10777
11233
  try {
10778
- const { spawnSync: spawnSync3 } = await import("child_process");
10779
- spawnSync3(ed, [idx], { stdio: "inherit" });
11234
+ const { spawnSync: spawnSync5 } = await import("child_process");
11235
+ spawnSync5(ed, [idx], { stdio: "inherit" });
10780
11236
  } finally {
10781
11237
  if (wasRaw) process.stdin.setRawMode(true);
10782
11238
  }
@@ -10893,7 +11349,7 @@ ${extra}` : body);
10893
11349
  try {
10894
11350
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
10895
11351
  } catch (e) {
10896
- log17.debug(`loadAgents(${d}) failed`, e);
11352
+ log20.debug(`loadAgents(${d}) failed`, e);
10897
11353
  }
10898
11354
  }
10899
11355
  if (!seen.size) {
@@ -10985,7 +11441,7 @@ ${extra}` : body);
10985
11441
  if (idx >= 0) {
10986
11442
  const old = mounted.splice(idx, 1)[0];
10987
11443
  removeWorkTools(old.tools.map((t) => t.name));
10988
- await old.client.close().catch((e) => log17.debug("mcp close failed", e));
11444
+ await old.client.close().catch((e) => log20.debug("mcp close failed", e));
10989
11445
  }
10990
11446
  try {
10991
11447
  const m = await mountMcpServer(name, conf);
@@ -11013,7 +11469,7 @@ ${extra}` : body);
11013
11469
  }
11014
11470
  const m = mounted.splice(idx, 1)[0];
11015
11471
  removeWorkTools(m.tools.map((t) => t.name));
11016
- await m.client.close().catch((e) => log17.debug("mcp close failed", e));
11472
+ await m.client.close().catch((e) => log20.debug("mcp close failed", e));
11017
11473
  err(dim(` removed "${name}"
11018
11474
  `));
11019
11475
  return;
@@ -11145,11 +11601,11 @@ ${extra}` : body);
11145
11601
  return;
11146
11602
  }
11147
11603
  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`);
11604
+ const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join11(".agent", "exports", `${session.meta.id}.md`);
11149
11605
  const path = resolve3(cwd, name);
11150
11606
  try {
11151
- mkdirSync7(dirname4(path), { recursive: true });
11152
- writeFileSync6(path, md);
11607
+ mkdirSync8(dirname4(path), { recursive: true });
11608
+ writeFileSync8(path, md);
11153
11609
  err(green(` \u2713 exported \u2192 ${path}
11154
11610
  `) + dim(` ${shown.length} message(s) \xB7 ${md.length} chars
11155
11611
  `));
@@ -11212,9 +11668,9 @@ ${extra}` : body);
11212
11668
  `));
11213
11669
  const listDir = (absDir) => {
11214
11670
  try {
11215
- return readdirSync2(join9(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
11671
+ return readdirSync3(join11(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
11216
11672
  } catch (e) {
11217
- log17.debug("completion readdir failed", absDir, e);
11673
+ log20.debug("completion readdir failed", absDir, e);
11218
11674
  return null;
11219
11675
  }
11220
11676
  };
@@ -11704,7 +12160,7 @@ async function main() {
11704
12160
  }
11705
12161
  });
11706
12162
  if (args.task) {
11707
- const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join9(cwd, ".agent", "mcp-auth.json") }));
12163
+ const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join11(cwd, ".agent", "mcp-auth.json") }));
11708
12164
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
11709
12165
  const store = new SessionStore(cwd);
11710
12166
  const session = startSession(args, store, agent, cwd);
@@ -11743,6 +12199,7 @@ export {
11743
12199
  formatTranscriptFull,
11744
12200
  jsonResult,
11745
12201
  mcpMentionResolver,
12202
+ mentionRefs,
11746
12203
  parseArgs,
11747
12204
  pastePathClassifier,
11748
12205
  readImageParts,