@livx.cc/agentx 0.99.31 → 0.99.33

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.
@@ -55,6 +55,9 @@ interface RealShellOptions {
55
55
  spawn?: SpawnFn;
56
56
  /** Per-command wall-clock cap (kill on overrun). Default 120s. */
57
57
  timeoutMs?: number;
58
+ /** Ceiling for the model-supplied `timeoutMs` argument (a long build can ask for more than the
59
+ * default, but not for an unbounded turn). Default 10min. */
60
+ maxTimeoutMs?: number;
58
61
  /** Extra env merged over the (optionally scrubbed) base env for the child. */
59
62
  env?: Record<string, string>;
60
63
  /** Strip likely-secret vars (API keys, tokens, cloud creds) from the child's env. Default ON.
@@ -15,6 +15,7 @@ var log2 = forComponent("llm");
15
15
 
16
16
  // src/redact.ts
17
17
  var REDACTED = "\u2039redacted\u203A";
18
+ var CONFIG_FILE_RE = /(^|\/)\.(agent|claude)\/(settings(\.[\w-]+)?\.json|config\.(json|js|mjs|cjs|ts))$/i;
18
19
  var SECRET_PAIR = /((?:^|[\s,{[])(?:export\s+)?["']?[\w.\-]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_?KEY|AUTH(?:_?TOKEN)?|BEARER)[\w.\-]*["']?\s*[:=]\s*)(["']?)([^\s"',{}\]]+)/gi;
19
20
  var SECRET_TOKEN = /\b(sk-ant-[\w-]{12,}|sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[\w]{20,}|xox[baprs]-[\w-]{10,}|AKIA[0-9A-Z]{12,}|AIza[\w-]{20,}|eyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,})\b/g;
20
21
  function redactSecrets(text) {
@@ -22,6 +23,196 @@ function redactSecrets(text) {
22
23
  return text.replace(SECRET_PAIR, (_m, head, quote, _val) => `${head}${quote}${REDACTED}`).replace(SECRET_TOKEN, REDACTED);
23
24
  }
24
25
 
26
+ // src/tools.structured.ts
27
+ function ckAbort(signal) {
28
+ if (signal?.aborted) throw new Error("aborted");
29
+ }
30
+ var DEFAULT_SKIP_DIRS = ["node_modules", ".git", "dist", ".tmp"];
31
+ var DEFAULT_MAX_DEPTH = 24;
32
+ function realOf(fs, p) {
33
+ const rp = fs.canonicalPath;
34
+ if (typeof rp !== "function") return p;
35
+ try {
36
+ return rp.call(fs, p) || p;
37
+ } catch {
38
+ return p;
39
+ }
40
+ }
41
+ async function walkFiles(fs, dir, signal, out = [], st, depth = 0) {
42
+ const state = st ?? { skip: new Set(DEFAULT_SKIP_DIRS), maxDepth: DEFAULT_MAX_DEPTH, seen: /* @__PURE__ */ new Set(), prunedDirs: /* @__PURE__ */ new Set(), depthCuts: 0 };
43
+ if (depth > state.maxDepth) {
44
+ state.depthCuts++;
45
+ return out;
46
+ }
47
+ const real = realOf(fs, dir);
48
+ if (state.seen.has(real)) return out;
49
+ state.seen.add(real);
50
+ let entries;
51
+ try {
52
+ entries = await fs.readDir(dir);
53
+ } catch {
54
+ return out;
55
+ }
56
+ for (const name of entries.sort()) {
57
+ ckAbort(signal);
58
+ if (state.skip.has(name)) {
59
+ state.prunedDirs.add(name);
60
+ continue;
61
+ }
62
+ const p = dir === "/" ? `/${name}` : `${dir}/${name}`;
63
+ if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out, state, depth + 1);
64
+ else out.push(p);
65
+ }
66
+ return out;
67
+ }
68
+ function scanRoots(fs, path, opts) {
69
+ const asked = path == null ? [] : (Array.isArray(path) ? path : [path]).map((p) => String(p)).filter(Boolean);
70
+ const roots = (asked.length ? asked : opts?.roots?.length ? opts.roots : [fsCwd(fs)]).map((r) => r.length > 1 && r.endsWith("/") ? r.slice(0, -1) : r);
71
+ return [...new Set(roots)].filter((r, _i, all) => !all.some((other) => other !== r && r.startsWith(`${other}/`)));
72
+ }
73
+ function scanNote(states) {
74
+ const pruned = [...new Set(states.flatMap((st) => [...st.prunedDirs]))].sort();
75
+ const cuts = states.reduce((n, st) => n + st.depthCuts, 0);
76
+ const parts = [];
77
+ if (pruned.length) parts.push(`skipped ${pruned.join(", ")} (pass skipDirs to include them)`);
78
+ if (cuts) parts.push(`stopped at depth ${states[0]?.maxDepth} in ${cuts} place${cuts === 1 ? "" : "s"} (pass maxDepth to go deeper)`);
79
+ return parts.length ? `
80
+ [scan: ${parts.join("; ")}]` : "";
81
+ }
82
+ async function walkRoots(fs, roots, signal, opts, call) {
83
+ const skip = new Set(call?.skipDirs ?? opts?.skipDirs ?? DEFAULT_SKIP_DIRS);
84
+ const maxDepth = call?.maxDepth ?? opts?.maxDepth ?? DEFAULT_MAX_DEPTH;
85
+ const out = [];
86
+ const states = [];
87
+ for (const root of roots) {
88
+ const state = { skip, maxDepth, seen: /* @__PURE__ */ new Set(), prunedDirs: /* @__PURE__ */ new Set(), depthCuts: 0 };
89
+ states.push(state);
90
+ await walkFiles(fs, root, signal, out, state, 0);
91
+ }
92
+ return { files: out, note: scanNote(states) };
93
+ }
94
+ function callScan(args) {
95
+ const sd = args.skipDirs;
96
+ return {
97
+ skipDirs: Array.isArray(sd) ? sd.map(String) : typeof sd === "string" ? sd.split(",").map((x) => x.trim()).filter(Boolean) : void 0,
98
+ maxDepth: args.maxDepth == null ? void 0 : Number(args.maxDepth)
99
+ };
100
+ }
101
+ var SCAN_PARAMS = {
102
+ path: { type: "string", description: "optional directory (or list) to search instead of the default roots; absolute VFS path" },
103
+ skipDirs: { type: "string", description: `comma-separated directory names to prune (default: ${DEFAULT_SKIP_DIRS.join(", ")}); pass an empty string to prune nothing` },
104
+ maxDepth: { type: "number", description: `max directory depth below each root (default ${DEFAULT_MAX_DEPTH})` }
105
+ };
106
+ function anchoredGlobAt(root, glob) {
107
+ if (glob.startsWith("/")) return globToRegExp(glob);
108
+ return globToRegExp(`${root === "/" ? "" : root}/${glob}`);
109
+ }
110
+ function globMatcher(roots, glob) {
111
+ const res = roots.map((r) => anchoredGlobAt(r, glob));
112
+ return (p) => res.some((re) => re.test(p));
113
+ }
114
+ function globToRegExp(glob, caseInsensitive = false) {
115
+ const g = glob.startsWith("/") ? glob : `/${glob}`;
116
+ let re = "";
117
+ for (let i = 0; i < g.length; i++) {
118
+ const c = g[i];
119
+ if (c === "*") {
120
+ if (g[i + 1] === "*") {
121
+ re += ".*";
122
+ i++;
123
+ if (g[i + 1] === "/") i++;
124
+ } else re += "[^/]*";
125
+ } else if (c === "?") re += "[^/]";
126
+ else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
127
+ }
128
+ return new RegExp(`^${re}$`, caseInsensitive ? "i" : "");
129
+ }
130
+ var fsCwd = (fs) => fs.getCwd();
131
+ function makeGlobTool(opts) {
132
+ return {
133
+ name: "Glob",
134
+ description: 'Find files by glob pattern (e.g. "**/*.ts", "src/**/*.test.ts"). Returns sorted paths, one per line. Space-separated patterns combine; `!`-prefix excludes (e.g. "**/*.ts !**/*.test.ts"). Optional `path` to search a specific directory. Prefer over `bash find` for locating files \u2014 one call, structured output.',
135
+ parameters: {
136
+ type: "object",
137
+ required: ["pattern"],
138
+ properties: {
139
+ pattern: { type: "string", description: "glob pattern(s); ** matches across directories; prefix a pattern with ! to exclude" },
140
+ ...SCAN_PARAMS
141
+ }
142
+ },
143
+ async run({ pattern, path, skipDirs, maxDepth }, ctx) {
144
+ const roots = scanRoots(ctx.fs, path, opts);
145
+ const pats = String(pattern ?? "").trim().split(/\s+/).filter(Boolean);
146
+ const include = pats.filter((p) => !p.startsWith("!")).map((p) => globMatcher(roots, p));
147
+ const exclude = pats.filter((p) => p.startsWith("!")).map((p) => globMatcher(roots, p.slice(1)));
148
+ const includes = include.length ? include : [globMatcher(roots, "**")];
149
+ const { files, note } = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));
150
+ const hits = files.filter((p) => includes.some((m) => m(p)) && !exclude.some((m) => m(p)));
151
+ return (hits.length ? [...new Set(hits)].sort().join("\n") : "(no matches)") + note;
152
+ }
153
+ };
154
+ }
155
+ function makeGrepTool(opts) {
156
+ return {
157
+ name: "Grep",
158
+ description: "Search file contents by regex. Returns `path:line: text` hits. Optional `glob` to scope files, `path` to search a specific directory, `context` for surrounding lines, `filesOnly` for matching paths only. Prefer over `bash grep` for file content search \u2014 structured results, no re-parse needed. Use `bash` instead for running commands, tests, or piped workflows.",
159
+ parameters: {
160
+ type: "object",
161
+ required: ["pattern"],
162
+ properties: {
163
+ pattern: { type: "string", description: "JS regular expression" },
164
+ glob: { type: "string", description: "optional file glob to restrict the search" },
165
+ ...SCAN_PARAMS,
166
+ context: { type: "number", description: "lines of context before/after each hit" },
167
+ filesOnly: { type: "boolean", description: "only list matching file paths" }
168
+ }
169
+ },
170
+ async run({ pattern, glob, path, skipDirs, maxDepth, context, filesOnly }, ctx) {
171
+ let re;
172
+ try {
173
+ re = new RegExp(String(pattern ?? ""));
174
+ } catch (e) {
175
+ throw new Error(`invalid regex: ${String(e)}`);
176
+ }
177
+ const roots = scanRoots(ctx.fs, path, opts);
178
+ const scope = glob ? globMatcher(roots, String(glob)) : null;
179
+ const walked = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));
180
+ const files = [...new Set(walked.files)].sort().filter((p) => !scope || scope(p));
181
+ const ctxN = Math.max(0, Number(context ?? 0));
182
+ const out = [];
183
+ const matched = [];
184
+ let skipped = 0;
185
+ for (const path2 of files) {
186
+ ckAbort(ctx.signal);
187
+ let content;
188
+ try {
189
+ content = await ctx.fs.readFile(path2);
190
+ } catch {
191
+ skipped++;
192
+ continue;
193
+ }
194
+ const lines = content.split("\n");
195
+ const mask = CONFIG_FILE_RE.test(path2);
196
+ let fileHit = false;
197
+ for (let i = 0; i < lines.length; i++) {
198
+ if (!re.test(lines[i])) continue;
199
+ fileHit = true;
200
+ if (filesOnly) break;
201
+ const lo = Math.max(0, i - ctxN), hi = Math.min(lines.length - 1, i + ctxN);
202
+ for (let j = lo; j <= hi; j++) out.push(`${path2}:${j + 1}: ${mask ? redactSecrets(lines[j]) : lines[j]}`);
203
+ }
204
+ if (fileHit) matched.push(path2);
205
+ }
206
+ const note = (skipped ? `
207
+ [skipped ${skipped} unreadable file${skipped === 1 ? "" : "s"}]` : "") + walked.note;
208
+ if (filesOnly) return (matched.length ? matched.join("\n") : "(no matches)") + note;
209
+ return (out.length ? out.join("\n") : "(no matches)") + note;
210
+ }
211
+ };
212
+ }
213
+ var globTool = makeGlobTool();
214
+ var grepTool = makeGrepTool();
215
+
25
216
  // src/tools.web.ts
26
217
  var log3 = forComponent("web");
27
218
  function htmlToText(html) {
@@ -446,20 +637,23 @@ var ShellJobRegistry = class {
446
637
  }
447
638
  };
448
639
  function makeRealShellTool(options) {
449
- const timeoutMs = options.timeoutMs ?? 12e4;
640
+ const defaultTimeoutMs = options.timeoutMs ?? 12e4;
641
+ const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
450
642
  return {
451
643
  name: "Shell",
452
- description: "Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input fail fast rather than hang \u2014 for privileged actions use a non-interactive flag (e.g. `sudo -n`), or ask the user to run the command themselves. Set `background:true` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.",
644
+ description: `Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed \`[exit N]\`. Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input fail fast rather than hang \u2014 for privileged actions use a non-interactive flag (e.g. \`sudo -n\`), or ask the user to run the command themselves. Each command is killed after ${defaultTimeoutMs}ms (result \`[exit 124]\` with whatever output it produced) \u2014 pass \`timeoutMs\` (max ${maxTimeoutMs}) for a legitimately slower command, and always bound network commands yourself (e.g. \`curl -m 10\`). Set \`background:true\` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.`,
453
645
  parameters: {
454
646
  type: "object",
455
647
  required: ["command"],
456
648
  properties: {
457
649
  command: { type: "string", description: "the shell command line to execute" },
458
- background: { type: "boolean", description: "run detached and return a job id immediately (for servers/watchers/long builds)" }
650
+ background: { type: "boolean", description: "run detached and return a job id immediately (for servers/watchers/long builds)" },
651
+ timeoutMs: { type: "number", description: "wall-clock cap for THIS command, overriding the default (clamped to the tool max)" }
459
652
  }
460
653
  },
461
- async run({ command, background }, ctx) {
654
+ async run({ command, background, timeoutMs: askedTimeoutMs }, ctx) {
462
655
  const cmd = String(command ?? "");
656
+ const timeoutMs = Number.isFinite(askedTimeoutMs) && Number(askedTimeoutMs) > 0 ? Math.min(Number(askedTimeoutMs), maxTimeoutMs) : defaultTimeoutMs;
463
657
  if (!cmd.trim()) return "[exit 1] empty command";
464
658
  if (background) {
465
659
  if (!options.registry) return "Error: background execution is not enabled in this host (no job registry).";