@livx.cc/agentx 0.99.78 → 0.99.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -405,6 +405,40 @@ var init_redact = __esm({
405
405
  }
406
406
  });
407
407
 
408
+ // src/hostPath.ts
409
+ function toVfsPath(p, platform = currentPlatform()) {
410
+ if (!isWinPaths(platform) || typeof p !== "string") return p;
411
+ let s = p.replace(/\\/g, "/");
412
+ s = s.replace(/^\/\/\?\/UNC\//i, "//").replace(/^\/\/\?\//, "");
413
+ const drive = /^([A-Za-z]):(?=\/|$)/.exec(s);
414
+ if (drive) return `/${drive[1].toLowerCase()}${s.slice(2) || "/"}`.replace(/\/+$/, "") || "/";
415
+ if (/^\/\/[^/]/.test(s)) return `/unc/${s.slice(2)}`;
416
+ return s;
417
+ }
418
+ function toHostPath(vpath, platform = currentPlatform()) {
419
+ if (!isWinPaths(platform)) return vpath;
420
+ const segs = vpath.split("/").filter(Boolean);
421
+ if (segs.length >= 1 && /^[A-Za-z]$/.test(segs[0])) return `${segs[0].toUpperCase()}:\\${segs.slice(1).join("\\")}`;
422
+ if (segs.length >= 3 && segs[0].toLowerCase() === "unc") return `\\\\${segs.slice(1).join("\\")}`;
423
+ return null;
424
+ }
425
+ function winSegmentProblem(seg) {
426
+ if (seg.includes(":")) return "alternate data stream / drive-relative name";
427
+ if (/[. ]$/.test(seg) && seg !== "." && seg !== "..") return "trailing dot or space";
428
+ if (WIN_DEVICE.test(seg)) return "reserved device name";
429
+ if (/[<>"|?*\x00-\x1f]/.test(seg)) return "invalid character";
430
+ return null;
431
+ }
432
+ var currentPlatform, isWinPaths, WIN_DEVICE;
433
+ var init_hostPath = __esm({
434
+ "src/hostPath.ts"() {
435
+ "use strict";
436
+ currentPlatform = () => typeof process !== "undefined" && process.platform || "browser";
437
+ isWinPaths = (platform = currentPlatform()) => platform === "win32";
438
+ WIN_DEVICE = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³]|conin\$|conout\$)(\..*)?$/i;
439
+ }
440
+ });
441
+
408
442
  // src/tools.structured.ts
409
443
  function ckAbort(signal) {
410
444
  if (signal?.aborted) throw new Error("aborted");
@@ -447,7 +481,7 @@ async function walkFiles(fs, dir, signal, out = [], st, depth = 0) {
447
481
  }
448
482
  function scanRoots(fs, path, opts) {
449
483
  const asked = path == null ? [] : (Array.isArray(path) ? path : [path]).map((p) => String(p)).filter(Boolean);
450
- const roots = (asked.length ? asked : opts?.roots?.length ? opts.roots : [fsCwd(fs)]).map((r) => r.length > 1 && r.endsWith("/") ? r.slice(0, -1) : r);
484
+ const roots = (asked.length ? asked : opts?.roots?.length ? opts.roots : [fsCwd(fs)]).map((r) => toVfsPath(r)).map((r) => r.length > 1 && r.endsWith("/") ? r.slice(0, -1) : r);
451
485
  return [...new Set(roots)].filter((r, _i, all) => !all.some((other) => other !== r && r.startsWith(`${other}/`)));
452
486
  }
453
487
  function scanNote(states) {
@@ -676,6 +710,7 @@ function parentDir(abs) {
676
710
  return i <= 0 ? "/" : abs.slice(0, i);
677
711
  }
678
712
  async function mkdirp(fs, dir) {
713
+ dir = toVfsPath(dir);
679
714
  if (dir === "/" || await fs.exists(dir)) return;
680
715
  await mkdirp(fs, parentDir(dir));
681
716
  if (!await fs.exists(dir)) await fs.createDir(dir);
@@ -736,6 +771,7 @@ var init_tools_structured = __esm({
736
771
  "src/tools.structured.ts"() {
737
772
  "use strict";
738
773
  init_redact();
774
+ init_hostPath();
739
775
  DEFAULT_SKIP_DIRS = ["node_modules", ".git", "dist", ".tmp"];
740
776
  DEFAULT_MAX_DEPTH = 24;
741
777
  SCAN_PARAMS = {
@@ -2019,20 +2055,58 @@ var NodeDiskFilesystem;
2019
2055
  var init_NodeDiskFilesystem = __esm({
2020
2056
  "src/NodeDiskFilesystem.ts"() {
2021
2057
  "use strict";
2058
+ init_hostPath();
2022
2059
  NodeDiskFilesystem = class _NodeDiskFilesystem {
2023
2060
  constructor(baseDir, opts = {}) {
2024
2061
  this.baseDir = baseDir;
2025
2062
  this.opts = { denySymlinks: true, ...opts };
2063
+ this.drives = this.win && (baseDir === "/" || baseDir === "\\");
2026
2064
  }
2027
2065
  baseDir;
2028
2066
  cwd = "/";
2029
2067
  opts;
2068
+ win = isWinPaths();
2069
+ /** Windows + baseDir '/': the VFS root is the machine (drives as first segment), not a directory. */
2070
+ drives;
2030
2071
  /** Ensure the root dir exists. */
2031
2072
  async init() {
2073
+ if (this.drives) return;
2032
2074
  await fsp.mkdir(this.baseDir, { recursive: true });
2033
2075
  }
2034
2076
  real(vpath) {
2035
- return np.join(this.baseDir, "." + vpath);
2077
+ if (!this.win) return np.join(this.baseDir, "." + vpath);
2078
+ for (const seg of vpath.split("/")) {
2079
+ const why = seg && winSegmentProblem(seg);
2080
+ if (why) throw new Error(`File not found: ${vpath} (${why})`);
2081
+ }
2082
+ if (!this.drives) return np.join(this.baseDir, "." + vpath);
2083
+ const host = toHostPath(vpath);
2084
+ if (!host) throw new Error(`File not found: ${vpath}`);
2085
+ return host;
2086
+ }
2087
+ /** Windows machine root ('/' when baseDir is '/'): a synthetic directory listing the drives. */
2088
+ isDriveRoot(vpath) {
2089
+ return this.drives && vpath === "/";
2090
+ }
2091
+ /** Windows: refuse an 8.3 short name (`ENV~1` opens `.env`) — the only alias that needs the disk to
2092
+ * detect. Only segments that look like one (`~<digit>`) cost a lookup: the OS's long name for that
2093
+ * component must be the name that was asked for. */
2094
+ assertLongNames(real) {
2095
+ if (!this.win || !/~\d/.test(real)) return;
2096
+ const root = np.parse(real).root;
2097
+ const parts = real.slice(root.length).split(/[\\/]/).filter(Boolean);
2098
+ let cur = root;
2099
+ for (const part of parts) {
2100
+ cur = np.join(cur, part);
2101
+ if (!/~\d/.test(part)) continue;
2102
+ let long;
2103
+ try {
2104
+ long = np.basename(realpathSync2.native(cur));
2105
+ } catch {
2106
+ return;
2107
+ }
2108
+ if (long.toLowerCase() !== part.toLowerCase()) throw new Error("File not found: short (8.3) name not permitted");
2109
+ }
2036
2110
  }
2037
2111
  // Verified non-symlink DIRECTORY components, with a short TTL: tree walks (Glob/Grep) hit the same
2038
2112
  // parents thousands of times; re-lstat'ing each per op is the dominant syscall cost. The 1s window
@@ -2042,11 +2116,13 @@ var init_NodeDiskFilesystem = __esm({
2042
2116
  static VERIFY_TTL_MS = 1e3;
2043
2117
  /** Throw if any existing component of `real` is a symlink (escape vector). */
2044
2118
  async assertNoSymlink(real) {
2119
+ this.assertLongNames(real);
2045
2120
  if (!this.opts.denySymlinks) return;
2046
- const rel = np.relative(this.baseDir, real);
2047
- if (rel === "" || rel.startsWith("..")) return;
2121
+ const base = this.drives ? np.parse(real).root : this.baseDir;
2122
+ const rel = np.relative(base, real);
2123
+ if (rel === "" || rel.startsWith("..") || np.isAbsolute(rel)) return;
2048
2124
  const parts = rel.split(np.sep);
2049
- let cur = this.baseDir;
2125
+ let cur = base;
2050
2126
  const now3 = Date.now();
2051
2127
  for (let i = 0; i < parts.length; i++) {
2052
2128
  cur = np.join(cur, parts[i]);
@@ -2066,13 +2142,13 @@ var init_NodeDiskFilesystem = __esm({
2066
2142
  }
2067
2143
  }
2068
2144
  resolvePath(path, cwd) {
2069
- return PathResolver.resolve(path, cwd || this.cwd);
2145
+ return PathResolver.resolve(toVfsPath(path), toVfsPath(cwd || this.cwd));
2070
2146
  }
2071
2147
  getCwd() {
2072
2148
  return this.cwd;
2073
2149
  }
2074
2150
  setCwd(path) {
2075
- this.cwd = PathResolver.normalize(path);
2151
+ this.cwd = PathResolver.normalize(toVfsPath(path));
2076
2152
  }
2077
2153
  async readFile(path) {
2078
2154
  const r = this.real(this.resolvePath(path));
@@ -2146,8 +2222,10 @@ var init_NodeDiskFilesystem = __esm({
2146
2222
  }
2147
2223
  }
2148
2224
  async readDir(path) {
2149
- const r = this.real(this.resolvePath(path));
2225
+ const v = this.resolvePath(path);
2226
+ if (this.isDriveRoot(v)) return this.listDrives();
2150
2227
  try {
2228
+ const r = this.real(v);
2151
2229
  await this.assertNoSymlink(r);
2152
2230
  return await fsp.readdir(r);
2153
2231
  } catch {
@@ -2167,8 +2245,10 @@ var init_NodeDiskFilesystem = __esm({
2167
2245
  await fsp.mkdir(r);
2168
2246
  }
2169
2247
  async exists(path) {
2170
- const r = this.real(this.resolvePath(path));
2248
+ const v = this.resolvePath(path);
2249
+ if (this.isDriveRoot(v)) return true;
2171
2250
  try {
2251
+ const r = this.real(v);
2172
2252
  await this.assertNoSymlink(r);
2173
2253
  await fsp.stat(r);
2174
2254
  return true;
@@ -2178,8 +2258,10 @@ var init_NodeDiskFilesystem = __esm({
2178
2258
  }
2179
2259
  async stat(path) {
2180
2260
  let s;
2181
- const r = this.real(this.resolvePath(path));
2261
+ const v = this.resolvePath(path);
2262
+ if (this.isDriveRoot(v)) return { created: /* @__PURE__ */ new Date(0), modified: /* @__PURE__ */ new Date(0), size: 0, permissions: "drwxr-xr-x", isExecutable: false };
2182
2263
  try {
2264
+ const r = this.real(v);
2183
2265
  await this.assertNoSymlink(r);
2184
2266
  s = await fsp.stat(r);
2185
2267
  } catch {
@@ -2194,8 +2276,10 @@ var init_NodeDiskFilesystem = __esm({
2194
2276
  };
2195
2277
  }
2196
2278
  async isDirectory(path) {
2197
- const r = this.real(this.resolvePath(path));
2279
+ const v = this.resolvePath(path);
2280
+ if (this.isDriveRoot(v)) return true;
2198
2281
  try {
2282
+ const r = this.real(v);
2199
2283
  await this.assertNoSymlink(r);
2200
2284
  return (await fsp.stat(r)).isDirectory();
2201
2285
  } catch {
@@ -2203,14 +2287,20 @@ var init_NodeDiskFilesystem = __esm({
2203
2287
  }
2204
2288
  }
2205
2289
  async isFile(path) {
2206
- const r = this.real(this.resolvePath(path));
2207
2290
  try {
2291
+ const r = this.real(this.resolvePath(path));
2208
2292
  await this.assertNoSymlink(r);
2209
2293
  return (await fsp.stat(r)).isFile();
2210
2294
  } catch {
2211
2295
  return false;
2212
2296
  }
2213
2297
  }
2298
+ /** Windows machine root: the drive letters that are mounted (`/c`, `/d`, …). */
2299
+ async listDrives() {
2300
+ const letters = "abcdefghijklmnopqrstuvwxyz".split("");
2301
+ const up = await Promise.all(letters.map((l) => fsp.stat(`${l.toUpperCase()}:\\`).then(() => true, () => false)));
2302
+ return letters.filter((_, i) => up[i]);
2303
+ }
2214
2304
  };
2215
2305
  }
2216
2306
  });
@@ -2227,6 +2317,7 @@ var init_JailedFilesystem = __esm({
2227
2317
  "src/JailedFilesystem.ts"() {
2228
2318
  "use strict";
2229
2319
  init_tools_structured();
2320
+ init_hostPath();
2230
2321
  JailedFilesystem = class {
2231
2322
  constructor(inner, options) {
2232
2323
  this.inner = inner;
@@ -2240,8 +2331,10 @@ var init_JailedFilesystem = __esm({
2240
2331
  denyRe;
2241
2332
  roRe;
2242
2333
  confineRe;
2334
+ /** Resolved VFS path — host-path syntax (Windows `C:\…`, backslashes) is normalized FIRST, so the
2335
+ * deny/readonly/confine globs always match the path that is actually opened. */
2243
2336
  abs(path) {
2244
- return this.inner.resolvePath(path, this.inner.getCwd());
2337
+ return this.inner.resolvePath(toVfsPath(path), this.inner.getCwd());
2245
2338
  }
2246
2339
  /** Throw if reading `path` is not permitted (denied or out of confinement). */
2247
2340
  guardRead(op, path) {
@@ -2326,7 +2419,7 @@ var init_JailedFilesystem = __esm({
2326
2419
  return entries.filter((name) => this.visible(abs === "/" ? `/${name}` : `${abs}/${name}`));
2327
2420
  }
2328
2421
  resolvePath(path, cwd) {
2329
- return this.inner.resolvePath(path, cwd ?? this.inner.getCwd());
2422
+ return this.inner.resolvePath(toVfsPath(path), cwd ?? this.inner.getCwd());
2330
2423
  }
2331
2424
  getCwd() {
2332
2425
  return this.inner.getCwd();
@@ -2444,6 +2537,125 @@ var init_shell_sandbox = __esm({
2444
2537
  }
2445
2538
  });
2446
2539
 
2540
+ // src/platform.ts
2541
+ import { spawnSync as spawnSync2, execFile } from "child_process";
2542
+ import { statSync as statSync4 } from "fs";
2543
+ import * as np2 from "path";
2544
+ function specFor(bin) {
2545
+ const name = np2.basename(bin).toLowerCase().replace(/\.exe$/, "");
2546
+ if (name === "powershell" || name === "pwsh") {
2547
+ const pre = "$ProgressPreference='SilentlyContinue';[Console]::OutputEncoding=[Text.Encoding]::UTF8;$OutputEncoding=[Text.Encoding]::UTF8;";
2548
+ return {
2549
+ bin,
2550
+ kind: "powershell",
2551
+ label: name === "pwsh" ? "PowerShell (pwsh)" : "Windows PowerShell",
2552
+ args: (c) => ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(pre + c, "utf16le").toString("base64")]
2553
+ };
2554
+ }
2555
+ if (name === "cmd") return { bin, kind: "cmd", label: "cmd.exe", args: (c) => ["/d", "/s", "/c", `"${c}"`], spawnOptions: { windowsVerbatimArguments: true } };
2556
+ return { bin, kind: "sh", label: /bash/.test(name) && /git/i.test(bin) ? "Git Bash" : name, args: (c) => ["-c", c] };
2557
+ }
2558
+ function findGitBash(env = process.env) {
2559
+ const roots = [];
2560
+ const git2 = findOnPath("git", env);
2561
+ if (git2) {
2562
+ const dir = np2.dirname(git2);
2563
+ const up = (p, n) => {
2564
+ for (let i = 0; i < n; i++) p = np2.dirname(p);
2565
+ return p;
2566
+ };
2567
+ roots.push(/[\\/](mingw(32|64)|ucrt64|clang(32|64|arm64))[\\/]bin$/i.test(dir) ? up(dir, 2) : up(dir, 1));
2568
+ }
2569
+ for (const base of [env.ProgramFiles, env["ProgramFiles(x86)"], env.ProgramW6432, env.LOCALAPPDATA && np2.join(env.LOCALAPPDATA, "Programs")])
2570
+ if (base) roots.push(np2.join(base, "Git"));
2571
+ for (const r of roots) {
2572
+ const b = np2.join(r, "bin", "bash.exe");
2573
+ if (isFile(b)) return b;
2574
+ }
2575
+ return void 0;
2576
+ }
2577
+ function defaultShell() {
2578
+ if (!IS_WIN) return POSIX_SH;
2579
+ if (_shell) return _shell;
2580
+ const override = process.env.AGENTX_SHELL;
2581
+ if (override && (isFile(override) || findOnPath(override))) return _shell = specFor(isFile(override) ? override : findOnPath(override));
2582
+ const bash = findGitBash();
2583
+ if (bash) return _shell = specFor(bash);
2584
+ const sysRoot = process.env.SystemRoot || "C:\\Windows";
2585
+ const ps = np2.join(sysRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
2586
+ if (isFile(ps)) return _shell = specFor(ps);
2587
+ return _shell = specFor(process.env.ComSpec || "cmd.exe");
2588
+ }
2589
+ function shellFor(shell) {
2590
+ if (!IS_WIN) return shell ? { ...POSIX_SH, bin: shell } : POSIX_SH;
2591
+ return shell && isFile(shell) ? specFor(shell) : defaultShell();
2592
+ }
2593
+ function killTree(pid, signal) {
2594
+ if (!pid) return false;
2595
+ if (!IS_WIN) {
2596
+ try {
2597
+ process.kill(-pid, signal);
2598
+ return true;
2599
+ } catch {
2600
+ return false;
2601
+ }
2602
+ }
2603
+ try {
2604
+ const r = spawnSync2("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore", timeout: 1e4 });
2605
+ return r.status === 0;
2606
+ } catch {
2607
+ return false;
2608
+ }
2609
+ }
2610
+ async function winTreeCpuSeconds(root) {
2611
+ const script = 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $([uint64]$_.UserModeTime + [uint64]$_.KernelModeTime)" }';
2612
+ const out = await new Promise((res, rej) => execFile(
2613
+ "powershell.exe",
2614
+ ["-NoProfile", "-NonInteractive", "-Command", script],
2615
+ { timeout: 15e3, windowsHide: true },
2616
+ (e, so) => e ? rej(e) : res(String(so))
2617
+ ));
2618
+ const rows = out.split(/\r?\n/).map((l) => l.trim().split(/\s+/)).filter((r) => r.length === 3).map(([pid, ppid, ticks]) => ({ pid: Number(pid), ppid: Number(ppid), sec: Number(ticks) / 1e7 }));
2619
+ const inTree = new Set(rows.filter((r) => r.pid === root).map((r) => r.pid));
2620
+ for (let grew = true; grew; ) {
2621
+ grew = false;
2622
+ for (const r of rows) if (!inTree.has(r.pid) && inTree.has(r.ppid) && r.pid !== r.ppid) {
2623
+ inTree.add(r.pid);
2624
+ grew = true;
2625
+ }
2626
+ }
2627
+ let total;
2628
+ for (const r of rows) if (inTree.has(r.pid) && Number.isFinite(r.sec)) total = (total ?? 0) + r.sec;
2629
+ return total;
2630
+ }
2631
+ function findOnPath(cmd, env = process.env) {
2632
+ const exts = IS_WIN && !/\.[^\\/.]+$/.test(cmd) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
2633
+ const cands = (base) => exts.map((e) => base + e);
2634
+ if (/[\\/]/.test(cmd)) return cands(cmd).find(isFile);
2635
+ const PATH = env.PATH ?? env.Path ?? "";
2636
+ for (const dir of PATH.split(np2.delimiter)) {
2637
+ if (!dir) continue;
2638
+ for (const p of cands(np2.join(dir, cmd))) if (isFile(p)) return p;
2639
+ }
2640
+ return void 0;
2641
+ }
2642
+ var IS_WIN, POSIX_SH, isFile, _shell, DETACH_OPTIONS;
2643
+ var init_platform = __esm({
2644
+ "src/platform.ts"() {
2645
+ "use strict";
2646
+ IS_WIN = process.platform === "win32";
2647
+ POSIX_SH = { bin: "/bin/sh", args: (c) => ["-c", c], label: "/bin/sh", kind: "sh" };
2648
+ isFile = (p) => {
2649
+ try {
2650
+ return statSync4(p).isFile();
2651
+ } catch {
2652
+ return false;
2653
+ }
2654
+ };
2655
+ DETACH_OPTIONS = IS_WIN ? { windowsHide: true } : { detached: true };
2656
+ }
2657
+ });
2658
+
2447
2659
  // src/tools.shell.ts
2448
2660
  var tools_shell_exports = {};
2449
2661
  __export(tools_shell_exports, {
@@ -2453,19 +2665,14 @@ __export(tools_shell_exports, {
2453
2665
  makeShellJobTools: () => makeShellJobTools,
2454
2666
  parsePsTime: () => parsePsTime,
2455
2667
  psGroupCpuSeconds: () => psGroupCpuSeconds,
2456
- sumTreeCpu: () => sumTreeCpu
2668
+ sumTreeCpu: () => sumTreeCpu,
2669
+ windowsShellNote: () => windowsShellNote
2457
2670
  });
2458
2671
  function killGroup(proc, signal) {
2459
- if (!proc?.pid) return false;
2460
- try {
2461
- process.kill(-proc.pid, signal);
2462
- return true;
2463
- } catch {
2464
- return false;
2465
- }
2672
+ return killTree(proc?.pid, signal);
2466
2673
  }
2467
2674
  async function spawnArgvFor(command, cwd, osSandbox) {
2468
- if (!osSandbox) return { bin: "/bin/sh", args: ["-c", command] };
2675
+ if (!osSandbox) return shellArgv(defaultShell(), command);
2469
2676
  const opts = osSandbox === true ? {} : osSandbox;
2470
2677
  const wrapper = await findSandboxWrapper();
2471
2678
  const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;
@@ -2493,9 +2700,17 @@ function parsePsTime(t) {
2493
2700
  return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
2494
2701
  }
2495
2702
  async function psGroupCpuSeconds(pgid) {
2703
+ if (IS_WIN) {
2704
+ try {
2705
+ return await winTreeCpuSeconds(pgid);
2706
+ } catch (e) {
2707
+ log9.debug(`cpu probe failed for ${pgid}: ${e}`);
2708
+ return void 0;
2709
+ }
2710
+ }
2496
2711
  try {
2497
- const { execFile } = await import("child_process");
2498
- const out = await new Promise((res, rej) => execFile("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
2712
+ const { execFile: execFile2 } = await import("child_process");
2713
+ const out = await new Promise((res, rej) => execFile2("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
2499
2714
  return sumTreeCpu(out, pgid);
2500
2715
  } catch (e) {
2501
2716
  log9.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
@@ -2546,7 +2761,7 @@ function makeRealShellTool(options) {
2546
2761
  return { ...this, description: rebound.description, run: rebound.run, withCwd: rebound.withCwd };
2547
2762
  },
2548
2763
  get description() {
2549
- return "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. " + (options.registry?.adopt ? `A command still running after ${defaultTimeoutMs}ms is NOT killed \u2014 it is handed to a background job and you get its id, so you can keep checking on it (\`[still running]\`). Pass \`timeoutMs\` (max ${maxTimeoutMs}) to wait longer in the foreground, and always bound network commands yourself (e.g. \`curl -m 10\`). ` : `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\`). `) + backgroundDoc();
2764
+ return `Run a shell command via ${shellFor(options.shell).label} in the working directory. ` + windowsShellNote(shellFor(options.shell)) + "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. " + (options.registry?.adopt ? `A command still running after ${defaultTimeoutMs}ms is NOT killed \u2014 it is handed to a background job and you get its id, so you can keep checking on it (\`[still running]\`). Pass \`timeoutMs\` (max ${maxTimeoutMs}) to wait longer in the foreground, and always bound network commands yourself (e.g. \`curl -m 10\`). ` : `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\`). `) + backgroundDoc();
2550
2765
  },
2551
2766
  // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).
2552
2767
  maxDurationMs: maxTimeoutMs,
@@ -2570,7 +2785,7 @@ function makeRealShellTool(options) {
2570
2785
  return `Started background job ${id}. Poll output with ShellOutput({id:"${id}"}), check ShellStatus({id:"${id}"}), stop with ShellKill({id:"${id}"}).`;
2571
2786
  }
2572
2787
  const spawn = options.spawn ?? await nodeSpawn();
2573
- let argv = { bin: options.shell || "/bin/sh", args: ["-c", cmd] };
2788
+ let argv = shellArgv(shellFor(options.shell), cmd);
2574
2789
  if (options.osSandbox) {
2575
2790
  try {
2576
2791
  argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);
@@ -2605,22 +2820,23 @@ function makeRealShellTool(options) {
2605
2820
  }
2606
2821
  };
2607
2822
  try {
2608
- return await new Promise((resolve2) => {
2823
+ return await new Promise((resolve3) => {
2609
2824
  let out = "";
2610
2825
  let settled = false;
2611
2826
  const finish = (s) => {
2612
2827
  if (settled) return;
2613
2828
  settled = true;
2614
- resolve2(s);
2829
+ resolve3(s);
2615
2830
  };
2616
2831
  let proc;
2617
2832
  try {
2618
- proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });
2833
+ if (IS_WIN) ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
2834
+ proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED, ...argv.spawnOptions });
2619
2835
  } catch (e) {
2620
2836
  return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
2621
2837
  }
2622
2838
  if (ctl.signal.aborted) killGroup(proc, "SIGKILL");
2623
- else ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
2839
+ else if (!IS_WIN) ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
2624
2840
  const registry = options.registry;
2625
2841
  if (registry?.adopt) {
2626
2842
  onDeadline = () => {
@@ -2665,6 +2881,12 @@ function makeRealShellTool(options) {
2665
2881
  }
2666
2882
  };
2667
2883
  }
2884
+ function windowsShellNote(sh) {
2885
+ if (!IS_WIN) return "";
2886
+ if (sh.kind === "sh") return "This is Windows: the shell is bash (POSIX syntax); drive paths are written /c/Users/\u2026 or C:/Users/\u2026, and Windows programs (.exe/.cmd) run directly. ";
2887
+ if (sh.kind === "powershell") return "This is Windows and the shell is PowerShell \u2014 use PowerShell syntax (`;` not `&&`, `$env:VAR`, Get-ChildItem/Select-String), not bash. ";
2888
+ return "This is Windows and the shell is cmd.exe \u2014 use cmd syntax (`&&`, `%VAR%`, dir/type/findstr), not bash. ";
2889
+ }
2668
2890
  function handoffFor(timeoutMs, id, body, notifies) {
2669
2891
  const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} \u2014 NOT killed, it is still going (it is only stopped if it goes silent with no CPU activity for a long time, and then you are told why). Check on it with ShellOutput({id:"${id}"}) / ShellStatus({id:"${id}"}), stop it with ShellKill({id:"${id}"}). ` + (notifies ? "Its completion will be reported to you, so you may continue with other work meanwhile." : "Nothing will tell you when it finishes \u2014 poll it yourself before you rely on its result.");
2670
2892
  return body ? `${head}
@@ -2719,7 +2941,7 @@ ${clean(out) || "(no output yet)"}`;
2719
2941
  }
2720
2942
  ];
2721
2943
  }
2722
- var log9, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2944
+ var log9, clean, DETACHED, shellArgv, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2723
2945
  var init_tools_shell = __esm({
2724
2946
  "src/tools.shell.ts"() {
2725
2947
  "use strict";
@@ -2728,9 +2950,11 @@ var init_tools_shell = __esm({
2728
2950
  init_logging();
2729
2951
  init_shell_sandbox();
2730
2952
  init_tools_jobs();
2953
+ init_platform();
2731
2954
  log9 = forComponent("shell");
2732
2955
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
2733
- DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
2956
+ DETACHED = { stdio: ["ignore", "pipe", "pipe"], ...DETACH_OPTIONS };
2957
+ shellArgv = (sh, command) => ({ bin: sh.bin, args: sh.args(command), ...sh.spawnOptions ? { spawnOptions: sh.spawnOptions } : {} });
2734
2958
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
2735
2959
  ShellJobRegistry = class {
2736
2960
  constructor(cfg) {
@@ -2745,8 +2969,8 @@ var init_tools_shell = __esm({
2745
2969
  const h = this.track(command, { idleMs: 0 });
2746
2970
  try {
2747
2971
  const spawn = this.cfg.spawn ?? await nodeSpawn();
2748
- const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
2749
- this.attach(h, spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED }));
2972
+ const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : shellArgv(defaultShell(), command);
2973
+ this.attach(h, spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED, ...argv.spawnOptions }));
2750
2974
  } catch (e) {
2751
2975
  h.chunk(`failed to spawn: ${e?.message ?? e}`);
2752
2976
  h.finish("error", void 0, e?.message ?? String(e));
@@ -3705,7 +3929,7 @@ init_OverlayFilesystem();
3705
3929
  // src/worktree.ts
3706
3930
  import { spawnSync } from "child_process";
3707
3931
  import { symlinkSync, mkdirSync, statSync as statSync3, readFileSync as readFileSync3, rmSync } from "fs";
3708
- import { join } from "path";
3932
+ import { join, resolve as resolve2 } from "path";
3709
3933
  var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
3710
3934
  var MAX_SLUG_LENGTH = 64;
3711
3935
  var GIT_NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
@@ -3739,7 +3963,7 @@ function git(args, cwd) {
3739
3963
  }
3740
3964
  function findGitRoot(from) {
3741
3965
  const r = git(["rev-parse", "--show-toplevel"], from);
3742
- return r.ok ? r.stdout : null;
3966
+ return r.ok ? process.platform === "win32" ? resolve2(r.stdout) : r.stdout : null;
3743
3967
  }
3744
3968
  function readWorktreeHead(worktreePath) {
3745
3969
  try {
@@ -3793,7 +4017,7 @@ function symlinkDirs(repoRoot, worktreePath, dirs) {
3793
4017
  continue;
3794
4018
  }
3795
4019
  try {
3796
- symlinkSync(src, dst, "dir");
4020
+ symlinkSync(src, dst, process.platform === "win32" ? "junction" : "dir");
3797
4021
  } catch (e) {
3798
4022
  if (e?.code !== "EEXIST") console.error(`[worktree] symlink ${dir}: ${e?.message ?? e}`);
3799
4023
  }
@@ -4331,6 +4555,10 @@ var AgentOptions = class {
4331
4555
  model = "anthropic/claude-sonnet-4-6";
4332
4556
  systemPrompt = "You are a coding agent operating on a virtual filesystem. Use the `bash` tool to explore (ls, grep, find, cat) and the `Read`/`Edit` tools to view and modify files. Always Read a file before Editing it \u2014 and before overwriting it with `Write` or a whole-file `ApplyEdits`: Write replaces the ENTIRE file, so blindly writing over an existing one destroys its unseen contents. If a file already exists, Read it first, then merge your changes (or Edit the specific part). When multiple tool calls are independent (e.g. several Reads or Greps with no dependency between them), issue them together in a single turn rather than one at a time. When the task is complete, reply with a short summary and make no further tool calls.\nRecovery: every turn's file changes are checkpointed. If the user asks to undo/revert/recover edits (including a file you overwrote), tell them to run `/rewind` (or `/undo` for the last turn) to restore the working tree \u2014 checkpoints cover files you edited even in .gitignored dirs. You cannot run `/rewind` yourself; surface it as the recovery path.\nWhen working with a knowledge base or document workspace: use `RepoMap` with scope \"docs\" to see all document headings and summaries (orient first); use `MemorySearch` to find memories by content when you don't know the exact slug; use `Recall` with multiple slugs to batch-load related facts in one call. Prefer targeted loads over reading everything \u2014 orient \u2192 search \u2192 load \u2192 act.";
4333
4557
  tools = defaultTools();
4558
+ /** claude-code/* only: which agentx tools reach it as the in-process `agentx` MCP server (ccBridge.ts). 'memory' =
4559
+ * the ones Claude Code has no native twin for (CC_BRIDGED); 'all' = every tool this agent has — for an agent whose
4560
+ * tools ARE its job (the duplex reflex: Act/Think/Hold…), run with Claude Code's own tools off (providerOptions.tools: []). */
4561
+ ccBridge = "memory";
4334
4562
  maxSteps = 25;
4335
4563
  // --- automatic kill-switches (always on; protect the API budget against runaway loops/abuse) ---
4336
4564
  /** Hard ceiling on accumulated tokens (prompt+completion) across the run; cache READS count at 0.1×
@@ -4926,8 +5154,9 @@ var Agent = class _Agent {
4926
5154
  };
4927
5155
  const cursorPo = isCursorWithTools ? { toolTimeoutMs: effectiveToolTimeoutMs, toolExecutor: hostExec } : void 0;
4928
5156
  const claudeSteerPo = o.model.startsWith("claude-code/") ? { steer: () => this.drainPendingSteers() } : void 0;
4929
- if (claudeSteerPo && o.providerOptions?.claudeCodeSession && this.activeTools.some((t) => CC_BRIDGED.has(t.name))) {
4930
- this.ccServer ??= ccToolServer("agentx", () => this.activeTools.filter((t) => CC_BRIDGED.has(t.name)), (n, a) => this.ccExec(n, a));
5157
+ const bridged = (t) => o.ccBridge === "all" || CC_BRIDGED.has(t.name);
5158
+ if (claudeSteerPo && o.providerOptions?.claudeCodeSession && this.activeTools.some(bridged)) {
5159
+ this.ccServer ??= ccToolServer("agentx", () => this.activeTools.filter(bridged), (n, a) => this.ccExec(n, a));
4931
5160
  claudeSteerPo.mcpServers = { ...o.providerOptions?.mcpServers, agentx: this.ccServer };
4932
5161
  }
4933
5162
  if (this.ccServer) this.ccExec = hostExec;
@@ -9134,6 +9363,10 @@ function describeCall(call) {
9134
9363
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
9135
9364
  return `${call.name}${hint}`;
9136
9365
  }
9366
+ function refusedCall(e) {
9367
+ const code = Number(e?.statusCode ?? e?.status);
9368
+ return code === 401 || code === 403 || /API key|unauthori[sz]ed|authentication|invalid x-api-key|permission denied/i.test(String(e?.message ?? e ?? ""));
9369
+ }
9137
9370
  var DuplexAgentOptions = class {
9138
9371
  /** Any ai.libx.js AIClient — shared by all tiers (routed by model). */
9139
9372
  ai;
@@ -9423,12 +9656,26 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9423
9656
  timeoutMs: 3e4,
9424
9657
  failEmptyTurns: false,
9425
9658
  // a silent reflex turn after a dispatch is legitimate — the voice layer repairs it
9659
+ ...this.claudeCodeReflex(),
9426
9660
  ...o.reflexOptions,
9427
9661
  tools,
9428
9662
  // Composed AFTER the spread so the dispatch guard can't be dropped by reflexOptions.
9429
9663
  hooks: composeHooks(this.dispatchGuard(), o.reflexOptions?.hooks)
9430
9664
  });
9431
9665
  }
9666
+ /** A claude-code/* reflex (the keyless stand-in when the fast model's provider has no key): Claude Code runs its own
9667
+ * tool loop and takes no host tool schemas, so the reflex's tools (Act/Think/Hold…) reach it through the in-process
9668
+ * `agentx` MCP bridge (ccBridge 'all', which needs a warm session), with Claude Code's OWN tools off (`tools: []`) and
9669
+ * no other MCP server — the reflex only talks and routes; real work still goes through Act and its policy. */
9670
+ claudeCodeReflex() {
9671
+ const o = this.options;
9672
+ if (!o.reflexModel.startsWith("claude-code/")) return {};
9673
+ const po = o.providerOptionsFor?.(o.reflexModel) ?? {};
9674
+ return {
9675
+ ccBridge: "all",
9676
+ providerOptions: { ...po, claudeCodeSession: po.claudeCodeSession ?? crypto.randomUUID(), tools: [], mcpServers: {}, strictMcpConfig: true }
9677
+ };
9678
+ }
9432
9679
  /** Resolve memory tools + inject index into voice system prompt (once). */
9433
9680
  async initMemory() {
9434
9681
  if (!this.memoryReady) return;
@@ -9593,7 +9840,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9593
9840
  await this.drainReflex();
9594
9841
  this.scrubLeakFromTranscript();
9595
9842
  this.scrubHeldText();
9596
- if (this.silentTurn) await this.ackIfSilent(fallback);
9843
+ if (this.silentTurn && res?.finishReason === "error" && refusedCall(res.error)) this.emitHost({ kind: "text_delta", message: fallback ?? _DuplexAgent.FALLBACK_ERROR });
9844
+ else if (this.silentTurn) await this.ackIfSilent(fallback);
9597
9845
  }
9598
9846
  /** Remove complete stage-direction parentheticals from the UNFORWARDED reflex text (STAGE_DIRECTION_RE).
9599
9847
  * Only the unforwarded region is touched — already-spoken audio can't be unsent, and splicing before
@@ -9699,6 +9947,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9699
9947
  * hosts/tests key on it. */
9700
9948
  static FALLBACK_ACKS = ["Okay, on it.", "On it.", "Alright, working on it."];
9701
9949
  static FALLBACK_RETRY = ["Sorry, could you say that again?", "Hm, I missed that \u2014 could you say that again?"];
9950
+ static FALLBACK_ERROR = "Sorry, I couldn't answer that \u2014 something went wrong on my side.";
9702
9951
  static FALLBACK_OWN = ["Sorry, I lost my train of thought there \u2014 give me that one more time?", "Hm, I got tangled up \u2014 ask me that once more?"];
9703
9952
  lastFallback = "";
9704
9953
  /** One user turn: the voice agent streams the reply (and may Act/Think). Serialized with re-voice turns.
@@ -10357,14 +10606,14 @@ Another agent just implemented the above. Independently check the CURRENT state
10357
10606
  * '[task <id> asks] …' and resolves with the user's answer via AnswerTask — or '' on timeout/
10358
10607
  * task settle (callers map '' to deny / best-judgment). Workers never block forever. */
10359
10608
  parkQuestion(askId, question) {
10360
- return new Promise((resolve2) => {
10609
+ return new Promise((resolve3) => {
10361
10610
  let settled = false;
10362
10611
  const finish = (answer) => {
10363
10612
  if (settled) return;
10364
10613
  settled = true;
10365
10614
  clearTimeout(timer);
10366
10615
  this.pendingAsks.delete(askId);
10367
- resolve2(answer);
10616
+ resolve3(answer);
10368
10617
  };
10369
10618
  const timer = setTimeout(() => {
10370
10619
  this.notify("task_ask_timeout", `task ${askId}: question timed out \u2014 proceeding without an answer`);
@@ -11111,11 +11360,11 @@ function buildMcpCatalog(servers) {
11111
11360
  }
11112
11361
  return { specs, routes };
11113
11362
  }
11114
- function searchOverCatalog(servers, specs, routes, resolve2, options) {
11363
+ function searchOverCatalog(servers, specs, routes, resolve3, options) {
11115
11364
  const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
11116
11365
  const r = routes.get(name);
11117
11366
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
11118
- return resolve2(r.server, r.rawName, args ?? {}, o);
11367
+ return resolve3(r.server, r.rawName, args ?? {}, o);
11119
11368
  }, options) : [];
11120
11369
  return { tools, serverNames: servers, toolCount: specs.length };
11121
11370
  }
@@ -11124,9 +11373,9 @@ function makeMcpToolSearchFromMounted(mounted, options) {
11124
11373
  const byName = new Map(mounted.map((m) => [m.name, m]));
11125
11374
  return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args, o) => byName.get(server).client.callTool(rawName, args, o), options);
11126
11375
  }
11127
- function makeLazyMcpToolSearch(servers, resolve2, options) {
11376
+ function makeLazyMcpToolSearch(servers, resolve3, options) {
11128
11377
  const { specs, routes } = buildMcpCatalog(servers);
11129
- return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve2, options);
11378
+ return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve3, options);
11130
11379
  }
11131
11380
 
11132
11381
  // src/hooks.ts