@livx.cc/agentx 0.94.38

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.
@@ -0,0 +1,539 @@
1
+ // src/tools.ts
2
+ import { CommandExecutor, registerHeadlessCommands } from "@livx.cc/wcli/core";
3
+
4
+ // src/redact.ts
5
+ var REDACTED = "\u2039redacted\u203A";
6
+ 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;
7
+ 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;
8
+ function redactSecrets(text) {
9
+ if (!text) return text;
10
+ return text.replace(SECRET_PAIR, (_m, head, quote, _val) => `${head}${quote}${REDACTED}`).replace(SECRET_TOKEN, REDACTED);
11
+ }
12
+
13
+ // src/logging.ts
14
+ import { log } from "libx.js/src/modules/log";
15
+ var forComponent = (name) => log.forComponent(name);
16
+
17
+ // src/tools.web.ts
18
+ var log2 = forComponent("web");
19
+ function htmlToText(html) {
20
+ let s = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<title[\s\S]*?<\/title>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<textarea[\s\S]*?<\/textarea>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ");
21
+ s = s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0?39;/g, "'").replace(/&#x27;/gi, "'");
22
+ return s.replace(/[ \t\f\v]+/g, " ").split("\n").map((l) => l.trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
23
+ }
24
+ function isPrivateHost(host) {
25
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
26
+ if (h === "" || h === "localhost" || h.endsWith(".localhost") || h.endsWith(".internal")) return true;
27
+ if (h === "::1" || h === "::" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd")) return true;
28
+ const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
29
+ if (m) {
30
+ const a = +m[1], b = +m[2];
31
+ return a === 0 || a === 127 || a === 10 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
32
+ }
33
+ return false;
34
+ }
35
+ var _dnsLookup;
36
+ async function resolveIps(host) {
37
+ if (_dnsLookup === void 0) {
38
+ try {
39
+ _dnsLookup = (await import("dns/promises")).lookup;
40
+ } catch {
41
+ _dnsLookup = null;
42
+ }
43
+ }
44
+ if (!_dnsLookup) return null;
45
+ try {
46
+ return (await _dnsLookup(host, { all: true })).map((a) => a.address);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ async function readCapped(res, maxBytes) {
52
+ const reader = res.body?.getReader?.();
53
+ if (!reader) {
54
+ const t = await res.text();
55
+ return t.length > maxBytes ? t.slice(0, maxBytes) : t;
56
+ }
57
+ const chunks = [];
58
+ let total = 0;
59
+ for (; ; ) {
60
+ const { done, value } = await reader.read();
61
+ if (done) break;
62
+ if (value) {
63
+ chunks.push(value);
64
+ total += value.length;
65
+ }
66
+ if (total >= maxBytes) {
67
+ try {
68
+ await reader.cancel();
69
+ } catch {
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ const out = new Uint8Array(Math.min(total, maxBytes));
75
+ let off = 0;
76
+ for (const c of chunks) {
77
+ if (off >= out.length) break;
78
+ const take = Math.min(c.length, out.length - off);
79
+ out.set(c.subarray(0, take), off);
80
+ off += take;
81
+ }
82
+ return new TextDecoder().decode(out);
83
+ }
84
+ function makeWebFetchTool(options = {}) {
85
+ const maxBytes = options.maxBytes ?? 2e6;
86
+ const maxChars = options.maxChars ?? 1e5;
87
+ const timeoutMs = options.timeoutMs ?? 15e3;
88
+ return {
89
+ name: "WebFetch",
90
+ description: "Fetch an http/https URL and return its readable text (HTML is stripped to text). Use to read docs or web pages. Returns the status line then up to ~100k chars of content.",
91
+ parameters: { type: "object", required: ["url"], properties: { url: { type: "string", description: "absolute http(s) URL" } } },
92
+ async run({ url }) {
93
+ const doFetch = options.fetch ?? globalThis.fetch;
94
+ const customFetch = !!options.fetch;
95
+ const u = String(url ?? "");
96
+ try {
97
+ new URL(u);
98
+ } catch {
99
+ return `Error: invalid URL: ${u}`;
100
+ }
101
+ if (!doFetch) return "Error: no network (fetch) available in this runtime";
102
+ const hostBlock = async (hostname) => {
103
+ if (options.allowPrivateHosts) return null;
104
+ if (isPrivateHost(hostname)) return hostname;
105
+ if (!customFetch) {
106
+ const ips = await resolveIps(hostname);
107
+ if (ips) {
108
+ for (const ip of ips) if (isPrivateHost(ip)) return `${hostname} \u2192 ${ip}`;
109
+ }
110
+ }
111
+ return null;
112
+ };
113
+ const ctl = new AbortController();
114
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
115
+ try {
116
+ let current = u;
117
+ let res;
118
+ for (let hop = 0; ; hop++) {
119
+ const pu = new URL(current);
120
+ if (pu.protocol !== "http:" && pu.protocol !== "https:") return `Error: only http/https URLs are allowed (got "${pu.protocol}")`;
121
+ const blocked = await hostBlock(pu.hostname);
122
+ if (blocked) return `Error: refusing to fetch a private/internal address (${blocked}) \u2014 set allowPrivateHosts to override`;
123
+ res = await doFetch(current, { signal: ctl.signal, redirect: "manual", headers: { "user-agent": "agentx (+https://github.com/Livshitz/agentx)" } });
124
+ if (res.status >= 300 && res.status < 400 && res.headers.get("location")) {
125
+ if (hop >= 5) return `Error fetching ${u}: too many redirects`;
126
+ current = new URL(res.headers.get("location"), current).toString();
127
+ continue;
128
+ }
129
+ break;
130
+ }
131
+ const type = res.headers.get("content-type") ?? "";
132
+ const body = await readCapped(res, maxBytes);
133
+ const text = /html/i.test(type) || /^\s*<(?:!doctype|html)/i.test(body) ? htmlToText(body) : body.trim();
134
+ const capped = text.length > maxChars ? text.slice(0, maxChars) + `
135
+ \u2026 [truncated at ${maxChars} chars]` : text;
136
+ return `${res.status} ${res.statusText} \xB7 ${new URL(current).host}
137
+
138
+ ${capped}`;
139
+ } catch (e) {
140
+ log2.debug(`WebFetch ${u} failed`, e);
141
+ return `Error fetching ${u}: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
142
+ } finally {
143
+ clearTimeout(timer);
144
+ }
145
+ }
146
+ };
147
+ }
148
+ function decodeDdgUrl(href) {
149
+ const m = href.match(/[?&]uddg=([^&]+)/);
150
+ if (m) {
151
+ try {
152
+ return decodeURIComponent(m[1]);
153
+ } catch {
154
+ }
155
+ }
156
+ return href.startsWith("//") ? "https:" + href : href;
157
+ }
158
+ function parseDdgHtml(html, max) {
159
+ const anchors = [...html.matchAll(/<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
160
+ const snippets = [...html.matchAll(/<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => htmlToText(m[1]));
161
+ const hits = [];
162
+ for (let i = 0; i < anchors.length && hits.length < max; i++) {
163
+ const url = decodeDdgUrl(anchors[i][1]);
164
+ try {
165
+ if (isPrivateHost(new URL(url).hostname)) continue;
166
+ } catch {
167
+ continue;
168
+ }
169
+ hits.push({ title: htmlToText(anchors[i][2]) || "(untitled)", url, snippet: snippets[i] ?? "" });
170
+ }
171
+ return hits;
172
+ }
173
+ function formatHits(hits) {
174
+ if (!hits.length) return "(no results)";
175
+ return hits.map((r, i) => `${i + 1}. ${r.title}
176
+ ${r.url}
177
+ ${r.snippet.replace(/\s+/g, " ").slice(0, 240)}`).join("\n\n");
178
+ }
179
+ function makeWebSearchTool(options = {}) {
180
+ const tavilyEndpoint = options.endpoint ?? "https://api.tavily.com/search";
181
+ const maxResults = options.maxResults ?? 5;
182
+ const timeoutMs = options.timeoutMs ?? 15e3;
183
+ return {
184
+ name: "WebSearch",
185
+ description: "Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic \u2014 then WebFetch a result URL to read it in full.",
186
+ parameters: { type: "object", required: ["query"], properties: { query: { type: "string" } } },
187
+ async run({ query }) {
188
+ const doFetch = options.fetch ?? globalThis.fetch;
189
+ if (!doFetch) return "Error: no network (fetch) available in this runtime";
190
+ const q = String(query ?? "").trim();
191
+ if (!q) return "Error: empty query";
192
+ const key = options.apiKey ?? process.env.TAVILY_API_KEY;
193
+ const provider = options.provider ?? "auto";
194
+ const useTavily = provider === "tavily" || provider === "auto" && !!key;
195
+ const ctl = new AbortController();
196
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
197
+ try {
198
+ if (useTavily) {
199
+ if (!key) return "Error: Tavily provider selected but TAVILY_API_KEY is not set";
200
+ const res2 = await doFetch(tavilyEndpoint, {
201
+ method: "POST",
202
+ signal: ctl.signal,
203
+ headers: { "content-type": "application/json" },
204
+ body: JSON.stringify({ api_key: key, query: q, max_results: maxResults })
205
+ });
206
+ if (!res2.ok) return `Error: search provider returned ${res2.status} ${res2.statusText}`;
207
+ const data = await res2.json();
208
+ const results = Array.isArray(data?.results) ? data.results.slice(0, maxResults) : [];
209
+ return formatHits(results.map((r) => ({ title: r.title ?? "(untitled)", url: r.url ?? "", snippet: String(r.content ?? "") })));
210
+ }
211
+ const res = await doFetch("https://html.duckduckgo.com/html/?q=" + encodeURIComponent(q), {
212
+ signal: ctl.signal,
213
+ headers: { "user-agent": "Mozilla/5.0 (compatible; agentx/1.0; +https://github.com/Livshitz/agentx)" }
214
+ });
215
+ if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;
216
+ return formatHits(parseDdgHtml(await res.text(), maxResults));
217
+ } catch (e) {
218
+ log2.debug("WebSearch failed", e);
219
+ return `Error searching: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
220
+ } finally {
221
+ clearTimeout(timer);
222
+ }
223
+ }
224
+ };
225
+ }
226
+ var webFetchTool = makeWebFetchTool();
227
+ var webSearchTool = makeWebSearchTool();
228
+
229
+ // src/tools.ts
230
+ function truncateOutput(s, headLines = 80, tailLines = 20) {
231
+ const lines = s.split("\n");
232
+ if (lines.length <= headLines + tailLines + 1) return s;
233
+ const omitted = lines.length - headLines - tailLines;
234
+ return [...lines.slice(0, headLines), `\u2026 (${omitted} lines omitted \u2014 narrow the command to see more) \u2026`, ...lines.slice(-tailLines)].join("\n");
235
+ }
236
+
237
+ // src/shell.sandbox.ts
238
+ var OsSandboxOptions = class {
239
+ /** Allow outbound network. Default OFF (Tier-1: no network unless granted). */
240
+ network = false;
241
+ /** Extra absolute paths writable beyond cwd + tmp (e.g. a build cache). */
242
+ writePaths = [];
243
+ };
244
+ function writable(cwd, o, tmpDir) {
245
+ const set = /* @__PURE__ */ new Set([cwd, "/tmp", "/private/tmp", "/private/var/folders", "/dev", ...tmpDir ? [tmpDir] : [], ...o.writePaths]);
246
+ return [...set];
247
+ }
248
+ var sbQuote = (p) => `"${p.replace(/(["\\])/g, "\\$1")}"`;
249
+ function seatbeltProfile(cwd, o, tmpDir) {
250
+ const allows = writable(cwd, o, tmpDir).map((p) => `(subpath ${sbQuote(p)})`).join(" ");
251
+ return [
252
+ "(version 1)",
253
+ "(allow default)",
254
+ ...o.network ? [] : ["(deny network*)"],
255
+ "(deny file-write*)",
256
+ `(allow file-write* ${allows})`
257
+ ].join("\n");
258
+ }
259
+ function sandboxArgv(command, cwd, opts = {}, platform = process.platform, tmpDir) {
260
+ const o = { ...new OsSandboxOptions(), ...opts };
261
+ if (platform === "darwin") {
262
+ return { bin: "/usr/bin/sandbox-exec", args: ["-p", seatbeltProfile(cwd, o, tmpDir), "/bin/sh", "-c", command] };
263
+ }
264
+ if (platform === "linux") {
265
+ const binds = writable(cwd, o, tmpDir).filter((p) => p !== "/dev" && !p.startsWith("/private")).flatMap((p) => ["--bind-try", p, p]);
266
+ return {
267
+ bin: "bwrap",
268
+ args: ["--ro-bind", "/", "/", ...binds, "--dev", "/dev", "--proc", "/proc", "--die-with-parent", ...o.network ? [] : ["--unshare-net"], "/bin/sh", "-c", command]
269
+ };
270
+ }
271
+ return null;
272
+ }
273
+ async function findSandboxWrapper(platform = process.platform) {
274
+ const { existsSync } = await import("fs");
275
+ if (platform === "darwin") return existsSync("/usr/bin/sandbox-exec") ? "/usr/bin/sandbox-exec" : null;
276
+ if (platform === "linux") {
277
+ for (const dir of (process.env.PATH ?? "/usr/bin:/bin").split(":")) if (dir && existsSync(`${dir}/bwrap`)) return `${dir}/bwrap`;
278
+ return null;
279
+ }
280
+ return null;
281
+ }
282
+
283
+ // src/tools.shell.ts
284
+ var log3 = forComponent("shell");
285
+ var clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
286
+ var DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
287
+ function killGroup(proc, signal) {
288
+ if (!proc?.pid) return false;
289
+ try {
290
+ process.kill(-proc.pid, signal);
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+ async function spawnArgvFor(command, cwd, osSandbox) {
297
+ if (!osSandbox) return { bin: "/bin/sh", args: ["-c", command] };
298
+ const opts = osSandbox === true ? {} : osSandbox;
299
+ const wrapper = await findSandboxWrapper();
300
+ const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;
301
+ if (!wrapped) throw new Error(`OS sandbox requested but no wrapper available on ${process.platform} (need sandbox-exec or bwrap)`);
302
+ return wrapped;
303
+ }
304
+ var SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
305
+ function childEnv(opts) {
306
+ const base = {};
307
+ const redact = opts.redactEnv !== false;
308
+ for (const [k, v] of Object.entries(process.env)) if (!(redact && SECRET_ENV_RE.test(k))) base[k] = v;
309
+ return { ...base, ...opts.env };
310
+ }
311
+ var _spawn;
312
+ async function nodeSpawn() {
313
+ if (!_spawn) _spawn = (await import("child_process")).spawn;
314
+ return _spawn;
315
+ }
316
+ var ShellJobRegistry = class {
317
+ constructor(cfg) {
318
+ this.cfg = cfg;
319
+ if (cfg.killOnExit && typeof process !== "undefined") process.once("exit", () => this.killAll());
320
+ }
321
+ cfg;
322
+ jobs = /* @__PURE__ */ new Map();
323
+ seq = 0;
324
+ async start(command) {
325
+ const id = `job-${++this.seq}`;
326
+ const max = this.cfg.maxBuffer ?? 256 * 1024;
327
+ const job = { command, buf: "", status: "running" };
328
+ const append = (chunk) => {
329
+ const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
330
+ job.buf = (job.buf + s).slice(-max);
331
+ };
332
+ try {
333
+ const spawn = this.cfg.spawn ?? await nodeSpawn();
334
+ const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
335
+ const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });
336
+ job.proc = proc;
337
+ proc.stdout?.on("data", append);
338
+ proc.stderr?.on("data", append);
339
+ proc.on("error", (err) => {
340
+ if (job.status === "running") {
341
+ job.status = "error";
342
+ append(`
343
+ [error] ${err?.message ?? err}`);
344
+ }
345
+ });
346
+ proc.on("close", (code) => {
347
+ if (job.status === "running") {
348
+ job.status = "exited";
349
+ job.exitCode = code ?? void 0;
350
+ }
351
+ });
352
+ } catch (e) {
353
+ job.status = "error";
354
+ job.buf = `failed to spawn: ${e?.message ?? e}`;
355
+ }
356
+ this.jobs.set(id, job);
357
+ return id;
358
+ }
359
+ /** Current tail output for a job (null = no such job). */
360
+ output(id) {
361
+ return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
362
+ }
363
+ status(id) {
364
+ const j = this.jobs.get(id);
365
+ return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;
366
+ }
367
+ list() {
368
+ return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));
369
+ }
370
+ kill(id) {
371
+ const j = this.jobs.get(id);
372
+ if (!j) return false;
373
+ if (j.status === "running") {
374
+ if (!killGroup(j.proc, "SIGTERM")) {
375
+ try {
376
+ j.proc?.kill("SIGTERM");
377
+ } catch {
378
+ }
379
+ }
380
+ j.status = "killed";
381
+ }
382
+ return true;
383
+ }
384
+ killAll() {
385
+ for (const id of this.jobs.keys()) this.kill(id);
386
+ }
387
+ };
388
+ function makeRealShellTool(options) {
389
+ const timeoutMs = options.timeoutMs ?? 12e4;
390
+ return {
391
+ name: "Shell",
392
+ 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.",
393
+ parameters: {
394
+ type: "object",
395
+ required: ["command"],
396
+ properties: {
397
+ command: { type: "string", description: "the shell command line to execute" },
398
+ background: { type: "boolean", description: "run detached and return a job id immediately (for servers/watchers/long builds)" }
399
+ }
400
+ },
401
+ async run({ command, background }, ctx) {
402
+ const cmd = String(command ?? "");
403
+ if (!cmd.trim()) return "[exit 1] empty command";
404
+ if (background) {
405
+ if (!options.registry) return "Error: background execution is not enabled in this host (no job registry).";
406
+ const id = await options.registry.start(cmd);
407
+ return `Started background job ${id}. Poll output with ShellOutput({id:"${id}"}), check ShellStatus({id:"${id}"}), stop with ShellKill({id:"${id}"}).`;
408
+ }
409
+ const spawn = options.spawn ?? await nodeSpawn();
410
+ let argv = { bin: options.shell || "/bin/sh", args: ["-c", cmd] };
411
+ if (options.osSandbox) {
412
+ try {
413
+ argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);
414
+ } catch (e) {
415
+ return `[exit 1] ${e?.message ?? e}`;
416
+ }
417
+ }
418
+ const ctl = new AbortController();
419
+ const onAbort = () => ctl.abort();
420
+ if (ctx.signal) {
421
+ if (ctx.signal.aborted) ctl.abort();
422
+ else ctx.signal.addEventListener("abort", onAbort, { once: true });
423
+ }
424
+ let timedOut = false;
425
+ const timer = setTimeout(() => {
426
+ timedOut = true;
427
+ ctl.abort();
428
+ }, timeoutMs);
429
+ let pend = "";
430
+ let flushTimer = null;
431
+ const flushEmit = (ctx2) => {
432
+ if (flushTimer) {
433
+ clearTimeout(flushTimer);
434
+ flushTimer = null;
435
+ }
436
+ if (pend) {
437
+ ctx2.emit?.(redactSecrets(pend));
438
+ pend = "";
439
+ }
440
+ };
441
+ try {
442
+ return await new Promise((resolve) => {
443
+ let out = "";
444
+ let settled = false;
445
+ const finish = (s) => {
446
+ if (settled) return;
447
+ settled = true;
448
+ resolve(s);
449
+ };
450
+ let proc;
451
+ try {
452
+ proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });
453
+ } catch (e) {
454
+ return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
455
+ }
456
+ if (ctl.signal.aborted) killGroup(proc, "SIGKILL");
457
+ else ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
458
+ const collect = (chunk) => {
459
+ const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
460
+ out += s;
461
+ if (ctx.emit && !settled) {
462
+ pend += s;
463
+ if (pend.length >= 1024) flushEmit(ctx);
464
+ else flushTimer ??= setTimeout(() => flushEmit(ctx), 250);
465
+ }
466
+ };
467
+ proc.stdout?.on("data", collect);
468
+ proc.stderr?.on("data", collect);
469
+ proc.on("error", (err) => {
470
+ if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
471
+ log3.debug("shell spawn error", err);
472
+ finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
473
+ });
474
+ proc.on("close", (code) => {
475
+ flushEmit(ctx);
476
+ if (ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
477
+ const body = clean(out);
478
+ if (code && code !== 0) return finish(`[exit ${code}]${body ? "\n" + body : ""}`);
479
+ finish(body || "(command succeeded, no output)");
480
+ });
481
+ });
482
+ } finally {
483
+ clearTimeout(timer);
484
+ if (flushTimer) clearTimeout(flushTimer);
485
+ ctx.signal?.removeEventListener("abort", onAbort);
486
+ }
487
+ }
488
+ };
489
+ }
490
+ function reasonFor(timedOut, timeoutMs, body) {
491
+ const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)` : "[exit 130] cancelled (killed)";
492
+ return body ? `${head}
493
+ ${body}` : head;
494
+ }
495
+ var NO_JOB = (id) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;
496
+ function makeShellJobTools(registry) {
497
+ const idParam = { type: "object", properties: { id: { type: "string", description: "the job id from Shell({background:true})" } } };
498
+ return [
499
+ {
500
+ name: "ShellOutput",
501
+ description: "Read the accumulated output (tail) of a background Shell job by id.",
502
+ parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
503
+ async run({ id }) {
504
+ const out = registry.output(String(id));
505
+ if (out == null) return NO_JOB(String(id));
506
+ const st = registry.status(String(id));
507
+ return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}]
508
+ ${clean(out) || "(no output yet)"}`;
509
+ }
510
+ },
511
+ {
512
+ name: "ShellStatus",
513
+ description: "Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.",
514
+ parameters: idParam,
515
+ async run({ id }) {
516
+ if (!id) {
517
+ const jobs = registry.list();
518
+ return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join("\n") : "(no background jobs)";
519
+ }
520
+ const st = registry.status(String(id));
521
+ return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ""} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB(String(id));
522
+ }
523
+ },
524
+ {
525
+ name: "ShellKill",
526
+ description: "Stop a running background Shell job by id (SIGTERM).",
527
+ parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
528
+ async run({ id }) {
529
+ return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));
530
+ }
531
+ }
532
+ ];
533
+ }
534
+ export {
535
+ ShellJobRegistry,
536
+ makeRealShellTool,
537
+ makeShellJobTools
538
+ };
539
+ //# sourceMappingURL=tools.shell.js.map