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/README.md +3 -3
- package/dist/{Agent-DmsB5hzp.d.ts → Agent-BA-rueWn.d.ts} +7 -2
- package/dist/cli.d.ts +7 -3
- package/dist/cli.js +657 -200
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +36 -6
- package/dist/index.js +96 -29
- package/dist/index.js.map +1 -1
- package/dist/{mcp-D00OuccC.d.ts → mcp-CnzmQ8JE.d.ts} +1 -1
- package/dist/mcp.client.d.ts +2 -2
- package/dist/{tools-9AUK6SG2.d.ts → tools-DtpN8Agv.d.ts} +2 -0
- package/dist/tools.shell.d.ts +1 -1
- package/dist/tools.shell.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
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:
|
|
1422
|
-
if (platform2 === "darwin") return
|
|
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 &&
|
|
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
|
|
1693
|
-
import { homedir as
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3359
|
-
while (
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
6322
|
+
const dot = (sub) => existsSync3(`${cwd}/.agent/${sub}`) ? `${cwd}/.agent/${sub}` : void 0;
|
|
6159
6323
|
const dots = (sub) => {
|
|
6160
|
-
const
|
|
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 =
|
|
6328
|
+
const home = homedir2();
|
|
6171
6329
|
const projectDir = `${cwd}/.agent/memory`;
|
|
6172
6330
|
const readDirs = [
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
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 =
|
|
6339
|
+
const home = homedir2();
|
|
6182
6340
|
return [
|
|
6183
|
-
|
|
6184
|
-
|
|
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
|
|
6295
|
-
import { homedir as
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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 (!
|
|
6355
|
-
const cacheDir = join4(
|
|
6515
|
+
if (!existsSync4(src)) return null;
|
|
6516
|
+
const cacheDir = join4(homedir3(), ".agent", "cache");
|
|
6356
6517
|
const bin = join4(cacheDir, "mic-aec");
|
|
6357
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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)
|
|
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)
|
|
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())
|
|
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
|
|
6551
|
-
import { existsSync as
|
|
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 (!
|
|
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 (!
|
|
6732
|
+
if (!existsSync5(p)) return {};
|
|
6572
6733
|
try {
|
|
6573
|
-
const raw = JSON.parse(
|
|
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(
|
|
6595
|
-
const user = await loadFrom(
|
|
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
|
|
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
|
-
|
|
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
|
|
6729
|
-
import { homedir as
|
|
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
|
|
6732
|
-
var globalDir = () => join6(
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
6929
|
+
if (!existsSync6(gd)) mkdirSync4(gd, { recursive: true });
|
|
6769
6930
|
const link2 = join6(gd, `${data.meta.id}.json`);
|
|
6770
|
-
if (!
|
|
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
|
-
|
|
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 (!
|
|
6941
|
+
if (!existsSync6(path)) return void 0;
|
|
6781
6942
|
try {
|
|
6782
|
-
return JSON.parse(
|
|
6943
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
6783
6944
|
} catch (e) {
|
|
6784
|
-
|
|
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 (!
|
|
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(
|
|
6956
|
+
metas.push(JSON.parse(readFileSync4(join6(this.dir, f), "utf8")).meta);
|
|
6796
6957
|
} catch (e) {
|
|
6797
|
-
|
|
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 (!
|
|
6974
|
+
if (!existsSync6(gd)) return void 0;
|
|
6814
6975
|
const exact = join6(gd, `${idOrPrefix}.json`);
|
|
6815
|
-
if (
|
|
6976
|
+
if (existsSync6(exact)) {
|
|
6816
6977
|
try {
|
|
6817
6978
|
const target = readlinkSync(exact);
|
|
6818
|
-
return JSON.parse(
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
6938
|
-
var exec = promisify(
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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 {
|
|
7160
|
-
import { homedir as
|
|
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
|
|
7186
|
-
|
|
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 =
|
|
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
|
-
|
|
7199
|
-
|
|
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(
|
|
7413
|
+
var TRUST_FILE = join8(homedir6(), ".agent", "trusted.json");
|
|
7227
7414
|
function isTrusted(cwd, file = TRUST_FILE) {
|
|
7228
|
-
|
|
7229
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
9105
|
+
var log20 = forComponent("cli");
|
|
8705
9106
|
var VERSION = (() => {
|
|
8706
9107
|
try {
|
|
8707
|
-
return JSON.parse(
|
|
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 && !
|
|
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 =
|
|
8931
|
-
if (!
|
|
8932
|
-
for (const line of
|
|
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,
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 =
|
|
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,${
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
9680
|
-
|
|
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 =
|
|
10088
|
+
const path = join11(cwd, ".agent", "settings.json");
|
|
9686
10089
|
try {
|
|
9687
|
-
const obj =
|
|
10090
|
+
const obj = existsSync9(path) ? JSON.parse(readFileSync6(path, "utf8")) : {};
|
|
9688
10091
|
if (obj[key] === value) return;
|
|
9689
10092
|
obj[key] = value;
|
|
9690
|
-
|
|
9691
|
-
|
|
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
|
-
|
|
10110
|
+
log20.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
9708
10111
|
return;
|
|
9709
10112
|
}
|
|
9710
|
-
|
|
10113
|
+
log20.error("unhandledRejection", e);
|
|
9711
10114
|
});
|
|
9712
10115
|
process.on("uncaughtException", (e) => {
|
|
9713
10116
|
if (isCancelTeardown(e)) {
|
|
9714
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
10442
|
+
const dir = join11(tmpdir3(), "agentx-pasted");
|
|
10028
10443
|
try {
|
|
10029
|
-
|
|
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:
|
|
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) =>
|
|
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 =
|
|
10129
|
-
const history =
|
|
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
|
-
|
|
10547
|
+
mkdirSync8(join11(cwd, ".agent"), { recursive: true });
|
|
10133
10548
|
appendFileSync(histPath, line + "\n");
|
|
10134
10549
|
} catch (e) {
|
|
10135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
10429
|
-
const r =
|
|
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}`, `${
|
|
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
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
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
|
-
|
|
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:
|
|
10779
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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" :
|
|
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
|
-
|
|
11152
|
-
|
|
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
|
|
11671
|
+
return readdirSync3(join11(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
11216
11672
|
} catch (e) {
|
|
11217
|
-
|
|
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:
|
|
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,
|