@wrongstack/tools 0.272.2 → 0.273.1

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
@@ -7,11 +7,12 @@ import { spawn, execFileSync } from 'node:child_process';
7
7
  import * as os2 from 'node:os';
8
8
  import * as fs8 from 'node:fs';
9
9
  import { statSync, mkdirSync, createWriteStream } from 'node:fs';
10
+ import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils/error';
10
11
  import * as dns from 'node:dns/promises';
11
12
  import * as net from 'node:net';
12
13
  import { Agent } from 'undici';
13
14
  import TurndownService from 'turndown';
14
- import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils';
15
+ import { toErrorMessage as toErrorMessage$2 } from '@wrongstack/core/utils';
15
16
  import { randomUUID } from 'node:crypto';
16
17
  import { createRequire } from 'node:module';
17
18
  import { fileURLToPath } from 'node:url';
@@ -626,7 +627,7 @@ var replaceTool = {
626
627
  if (err.code === "ENOENT") return null;
627
628
  throw err;
628
629
  });
629
- if (!lstat2 || !lstat2.isFile()) continue;
630
+ if (!lstat2?.isFile()) continue;
630
631
  if (lstat2.isSymbolicLink()) continue;
631
632
  let realPath;
632
633
  try {
@@ -637,7 +638,7 @@ var replaceTool = {
637
638
  const rel = path.relative(realRoot, realPath);
638
639
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
639
640
  const stat11 = await fs7.stat(realPath).catch(() => null);
640
- if (!stat11 || !stat11.isFile()) continue;
641
+ if (!stat11?.isFile()) continue;
641
642
  let content;
642
643
  try {
643
644
  const buf = await fs7.readFile(realPath);
@@ -1798,6 +1799,21 @@ function _resetProcessRegistry() {
1798
1799
  _registry = void 0;
1799
1800
  }
1800
1801
  var REGISTRY_FILE = ".wrongstack/process-registry.json";
1802
+ function toErrorMessage2(err) {
1803
+ return err instanceof Error ? err.message : String(err);
1804
+ }
1805
+ function emitStructuredLog(level, event, message, error) {
1806
+ const payload = {
1807
+ level,
1808
+ event,
1809
+ message,
1810
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1811
+ };
1812
+ if (error !== void 0) {
1813
+ payload.error = toErrorMessage2(error);
1814
+ }
1815
+ console.log(JSON.stringify(payload));
1816
+ }
1801
1817
  var HEARTBEAT_INTERVAL_MS = 5e3;
1802
1818
  var STALE_THRESHOLD_MS = 3e4;
1803
1819
  var LOCKFILE = ".wrongstack/.process-registry.lock";
@@ -1807,6 +1823,9 @@ function generateInstanceId() {
1807
1823
  const random = Math.random().toString(36).slice(2, 8);
1808
1824
  return `${hostname4}:${pid}:${random}`;
1809
1825
  }
1826
+ function isNodeError(err) {
1827
+ return typeof err === "object" && err !== null && "code" in err;
1828
+ }
1810
1829
  async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1811
1830
  const start = Date.now();
1812
1831
  const pidStr = String(process.pid);
@@ -1821,7 +1840,7 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1821
1840
  }
1822
1841
  };
1823
1842
  } catch (err) {
1824
- if (err.code === "EEXIST") {
1843
+ if (isNodeError(err) && err.code === "EEXIST") {
1825
1844
  try {
1826
1845
  const content = await fs7.readFile(lockfilePath, "utf-8");
1827
1846
  const parts = content.split(":");
@@ -1858,7 +1877,7 @@ async function readRegistryFile(filePath) {
1858
1877
  }
1859
1878
  return parsed;
1860
1879
  } catch (err) {
1861
- if (err.code === "ENOENT") {
1880
+ if (isNodeError(err) && err.code === "ENOENT") {
1862
1881
  return {
1863
1882
  version: 1,
1864
1883
  instances: /* @__PURE__ */ new Map(),
@@ -1894,7 +1913,7 @@ var PersistentProcessRegistry = class {
1894
1913
  this.lockPath = path.join(homeDir, LOCKFILE);
1895
1914
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
1896
1915
  this.ensureDirectory().catch((err) => {
1897
- console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
1916
+ emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
1898
1917
  });
1899
1918
  }
1900
1919
  async ensureDirectory() {
@@ -1902,7 +1921,7 @@ var PersistentProcessRegistry = class {
1902
1921
  try {
1903
1922
  await fs7.mkdir(dir, { recursive: true });
1904
1923
  } catch (err) {
1905
- if (err.code !== "EEXIST") throw err;
1924
+ if (!isNodeError(err) || err.code !== "EEXIST") throw err;
1906
1925
  }
1907
1926
  }
1908
1927
  /**
@@ -1982,6 +2001,7 @@ var PersistentProcessRegistry = class {
1982
2001
  try {
1983
2002
  const data = await readRegistryFile(this.registryPath);
1984
2003
  data.instances.set(String(entry.pid), entry);
2004
+ const child = null;
1985
2005
  this.baseRegistry.register({
1986
2006
  pid: entry.pid,
1987
2007
  name: entry.name,
@@ -1989,8 +2009,7 @@ var PersistentProcessRegistry = class {
1989
2009
  startedAt: entry.startedAt,
1990
2010
  sessionId: entry.sessionId,
1991
2011
  protected: entry.protected,
1992
- child: null
1993
- // Main process has no child handle
2012
+ child
1994
2013
  });
1995
2014
  await writeRegistryFile(this.registryPath, data);
1996
2015
  } finally {
@@ -2038,7 +2057,7 @@ var PersistentProcessRegistry = class {
2038
2057
  data.lastCleanup = now2;
2039
2058
  await writeRegistryFile(this.registryPath, data);
2040
2059
  } catch (err) {
2041
- console.error("PersistentProcessRegistry: sync failed", err);
2060
+ emitStructuredLog("warn", "process_registry.sync_failed", "PersistentProcessRegistry: sync failed", err);
2042
2061
  } finally {
2043
2062
  await release();
2044
2063
  }
@@ -2059,7 +2078,11 @@ var PersistentProcessRegistry = class {
2059
2078
  if (process.platform !== "win32") {
2060
2079
  process.kill(entry.pid, 0);
2061
2080
  } else {
2062
- console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
2081
+ emitStructuredLog(
2082
+ "debug",
2083
+ "process_registry.stale_pid_check",
2084
+ `PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`
2085
+ );
2063
2086
  }
2064
2087
  } catch {
2065
2088
  stalePids.push(_pidStr);
@@ -2073,7 +2096,7 @@ var PersistentProcessRegistry = class {
2073
2096
  await writeRegistryFile(this.registryPath, data);
2074
2097
  }
2075
2098
  } catch (err) {
2076
- console.error("PersistentProcessRegistry: cleanup failed", err);
2099
+ emitStructuredLog("warn", "process_registry.cleanup_failed", "PersistentProcessRegistry: cleanup failed", err);
2077
2100
  } finally {
2078
2101
  await release();
2079
2102
  }
@@ -2302,7 +2325,7 @@ async function isKillProtected(kill) {
2302
2325
  const entries = await getProtectedEntries();
2303
2326
  const killNameLower = kill.name.toLowerCase();
2304
2327
  for (const entry of entries) {
2305
- if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
2328
+ if (entry.name?.toLowerCase().includes(killNameLower)) {
2306
2329
  return true;
2307
2330
  }
2308
2331
  }
@@ -2357,6 +2380,155 @@ async function checkAndBlockKillCommand(command) {
2357
2380
  }
2358
2381
  return { blocked: false };
2359
2382
  }
2383
+ function pickShell(platform4, command, env) {
2384
+ const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
2385
+ if (override === "cmd" || override === "cmd.exe") return "cmd";
2386
+ if (override === "powershell" || override === "powershell.exe") return "powershell";
2387
+ if (override === "pwsh" || override === "pwsh.exe") return "pwsh";
2388
+ if (looksLikePowerShell(command)) return "pwsh";
2389
+ return "cmd";
2390
+ }
2391
+ function looksLikePowerShell(command) {
2392
+ if (!command) return false;
2393
+ const trimmed = command.trimStart();
2394
+ if (/\.ps1\b/i.test(trimmed)) return true;
2395
+ if (/^\s*#requires\s/i.test(trimmed)) return true;
2396
+ if (/^\s*param\s*\(/i.test(trimmed)) return true;
2397
+ if (/\$[\w:{]/i.test(trimmed)) return true;
2398
+ if (/\$\(/.test(trimmed)) return true;
2399
+ if (/@\s*['"]/.test(trimmed)) return true;
2400
+ if (/&\s+\$/.test(trimmed)) return true;
2401
+ if (/(^|\s)@\s*\(/.test(trimmed)) return true;
2402
+ if (/(^|\s)@\{/.test(trimmed)) return true;
2403
+ if (/(?:^|[\s\[\(\{,;])(?:-eq|-ne|-lt|-gt|-le|-ge|-like|-notlike|-match|-notmatch|-contains|-notcontains|-in|-notin|-and|-or|-not|-band|-bor|-bxor|-replace|-isplit|-csplit|-osplit|-join|-is|-as|-f)(?:$|[\s\]\)\},;])/i.test(trimmed)) {
2404
+ return true;
2405
+ }
2406
+ if (PS_VERB_RE.test(trimmed)) return true;
2407
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
2408
+ return true;
2409
+ }
2410
+ if (looksLikePowerShellExtended(command)) return true;
2411
+ return false;
2412
+ }
2413
+ function looksLikePowerShellExtended(command) {
2414
+ if (!command) return false;
2415
+ const trimmed = command.trimStart();
2416
+ if (/(?:^|\s)[-/](?:WhatIf|Confirm|ErrorAction)(?::[^\s]+|\s|=|$)/i.test(trimmed)) {
2417
+ return true;
2418
+ }
2419
+ if (/(?:^|[\s;&|])(Where-Object|ForEach-Object|Select-Object|Sort-Object|Group-Object|Measure-Object|Compare-Object|Tee-Object)(?:\s|$)/i.test(trimmed)) {
2420
+ return true;
2421
+ }
2422
+ if (/\bWrite-(?:Host|Output|Error|Warning|Verbose|Debug|Information)(?:\s|$)/i.test(trimmed)) {
2423
+ return true;
2424
+ }
2425
+ if (/HK(?:LM|CU|CR|U|CC|DD|PD):\\/i.test(trimmed)) return true;
2426
+ if (/\[(?:string|int|bool|xml|double|float|decimal|char|byte|long|System\.)/i.test(trimmed)) {
2427
+ return true;
2428
+ }
2429
+ if (/^\s*<#|#>\s*$/m.test(trimmed)) return true;
2430
+ if (/(?:^|\s)[-/\/](?:AsPlainText|PipelineVariable|pv|FilterHashtable|OutVariable|ov)(?:\s|=|$)/i.test(trimmed)) {
2431
+ return true;
2432
+ }
2433
+ return false;
2434
+ }
2435
+ function wrapPowerShellScript(command) {
2436
+ const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
2437
+ return "\uFEFF" + bootstrap + "\ntry {\n" + command + "\n} finally { exit $LASTEXITCODE }";
2438
+ }
2439
+ var PS_VERB_RE = new RegExp(
2440
+ // Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
2441
+ "(?:^|[\\s;&|\\(\\{,])(?:Get|Set|New|Remove|Add|Clear|Copy|Move|Rename|Test|Update|Write|Read|Push|Pop|Invoke|Start|Stop|Wait|Out|Format|Group|Measure|Compare|Resolve|ConvertTo|ConvertFrom|Convert|Import|Export|Select|Where|ForEach|Sort|Tee|Split|Join|Limit|Skip|Step|Trace|Debug|Register|Unregister|Enable|Disable|Restart|Suspend|Resume|Save|Open|Close|Lock|Unlock|Mount|Dismount|Enter|Exit|Use|Show|Hide|Find|Search|Watch|Initialize|Optimize|Compress|Expand|Merge|Checkpoint|Undo|Redo|Approve|Deny|Block|Grant|Revoke|Assert|Confirm|Receive|Send|Connect|Disconnect|Reset|Backup|Restore|Publish|Unpublish|Install|Uninstall|Build|Rebuild|Deploy|Submit|Process|Complete|Approve|Revoke|Pay|Refund|Decline|Receive|Send)-[A-Za-z][A-Za-z0-9]+(?:[\\-\\+][A-Za-z][A-Za-z0-9]+)*(?:$|[\\s\\-\\;\\&\\|\\(\\)\\{\\},])",
2442
+ "i"
2443
+ );
2444
+ function shellArgs(shell) {
2445
+ if (shell === "powershell" || shell === "pwsh") {
2446
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "-"];
2447
+ }
2448
+ return ["/c"];
2449
+ }
2450
+ function diagnoseBashism(command, shell) {
2451
+ if (!command) return void 0;
2452
+ const isCmd = shell === "cmd";
2453
+ const hints = [];
2454
+ const add = (h) => {
2455
+ if (!hints.includes(h)) hints.push(h);
2456
+ };
2457
+ if (/\/dev\/null/.test(command)) {
2458
+ add(
2459
+ isCmd ? "use `nul` instead of `/dev/null` (e.g. `2>nul`)" : "use `$null` instead of `/dev/null` (e.g. `2>$null`)"
2460
+ );
2461
+ }
2462
+ if (/(^|[;&|]\s*)export\s+[A-Za-z_]\w*=/.test(command)) {
2463
+ add(
2464
+ isCmd ? "set env vars with `set NAME=value`, not `export`" : "set env vars with `$env:NAME = 'value'`, not `export`"
2465
+ );
2466
+ }
2467
+ if (/<<-?\s*['"]?[A-Za-z_]\w*/.test(command)) {
2468
+ add(
2469
+ isCmd ? "cmd has no heredocs \u2014 write the content to a file or use multiple `echo` lines" : "PowerShell has no heredocs \u2014 use a single-quoted here-string `@'\u2026'@` (closing `'@` at column 0)"
2470
+ );
2471
+ }
2472
+ if (shell === "powershell" && /(&&|\|\|)/.test(command)) {
2473
+ add("Windows PowerShell 5.1 has no `&&`/`||` \u2014 separate commands with `;` (check `$LASTEXITCODE`)");
2474
+ }
2475
+ if (/\brm\s+-[A-Za-z]*[rf]/.test(command)) {
2476
+ add(
2477
+ isCmd ? "`rm` is not a cmd builtin \u2014 use `del` (files) or `rmdir /s /q` (dirs)" : "use `Remove-Item -Recurse -Force` \u2014 the `rm -rf` bash flags don't exist in PowerShell"
2478
+ );
2479
+ }
2480
+ if (/\bwhich\s+\S/.test(command)) {
2481
+ add(isCmd ? "use `where <cmd>` instead of `which`" : "use `Get-Command <cmd>` instead of `which`");
2482
+ }
2483
+ if (hints.length === 0) return void 0;
2484
+ const label = isCmd ? "cmd.exe" : shell === "pwsh" ? "PowerShell 7" : "Windows PowerShell";
2485
+ return `[wrongstack] This command failed and contains bash/POSIX syntax that ${label} does not accept \u2014 ${hints.join("; ")}. Rewrite it in ${isCmd ? "cmd" : "PowerShell"} syntax and retry.`;
2486
+ }
2487
+ function resolveWin32Command(cmd) {
2488
+ if (process.platform !== "win32") return cmd;
2489
+ if (cmd.includes("/") || cmd.includes("\\") || path.extname(cmd.replace(/\//g, "\\"))) {
2490
+ return cmd;
2491
+ }
2492
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
2493
+ const pathDirs = (process.env["PATH"] ?? "").split(path.delimiter);
2494
+ for (const dir of pathDirs) {
2495
+ const base = path.join(dir, cmd);
2496
+ for (const ext of pathext) {
2497
+ const full = `${base}${ext}`;
2498
+ try {
2499
+ fs8.accessSync(full, fs8.constants.X_OK);
2500
+ return full;
2501
+ } catch {
2502
+ }
2503
+ }
2504
+ }
2505
+ return cmd;
2506
+ }
2507
+ function resolvePowerShell(cmd) {
2508
+ if (process.platform !== "win32") return cmd;
2509
+ const lower = cmd.toLowerCase();
2510
+ if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
2511
+ return resolveWin32Command(cmd);
2512
+ }
2513
+ const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
2514
+ const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
2515
+ const resolved = resolveWin32Command(primary);
2516
+ if (resolved !== primary) {
2517
+ const fb = resolveWin32Command(fallback);
2518
+ return fb === fallback ? cmd : fb;
2519
+ }
2520
+ return resolved;
2521
+ }
2522
+ var WIN32_SHELL_META = /[&|<>\r\n\0]/;
2523
+ function assertSafeWin32ShellArgs(args) {
2524
+ for (const a of args) {
2525
+ if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
2526
+ throw new Error(
2527
+ "win32 shell spawn: argument contains a shell metacharacter (one of & | < > or a newline) that could enable command injection through the .cmd/.bat wrapper \u2014 refusing to run. Offending argument: " + JSON.stringify(a)
2528
+ );
2529
+ }
2530
+ }
2531
+ }
2360
2532
 
2361
2533
  // src/bash.ts
2362
2534
  var MAX_OUTPUT = 32768;
@@ -2453,18 +2625,36 @@ var bashTool = {
2453
2625
  }
2454
2626
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
2455
2627
  const isWin3 = os2.platform() === "win32";
2456
- const shell = (() => {
2457
- const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
2458
- if (explicit) return explicit;
2459
- if (isWin3) return process.env["COMSPEC"] ?? "cmd.exe";
2460
- const fromEnv = process.env["SHELL"];
2461
- if (fromEnv) {
2462
- const name = fromEnv.split("/").pop() ?? "";
2463
- if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) return fromEnv;
2464
- }
2465
- return "/bin/bash";
2466
- })();
2467
- const args = isWin3 ? ["/c", input.command] : ["-c", input.command];
2628
+ let plan;
2629
+ let winShellKind;
2630
+ if (isWin3) {
2631
+ const shell2 = pickShell("win32", input.command, {
2632
+ get: (k) => process.env[k]
2633
+ });
2634
+ winShellKind = shell2;
2635
+ const bin = shell2 === "powershell" ? resolvePowerShell("powershell.exe") : shell2 === "pwsh" ? resolvePowerShell("pwsh.exe") : process.env["COMSPEC"] ?? "cmd.exe";
2636
+ plan = {
2637
+ bin,
2638
+ argv: shellArgs(shell2),
2639
+ useStdin: shell2 === "powershell" || shell2 === "pwsh",
2640
+ stdinBody: shell2 === "powershell" || shell2 === "pwsh" ? wrapPowerShellScript(input.command) : void 0
2641
+ };
2642
+ } else {
2643
+ const explicit = process.env["WRONGSTACK_SHELL"];
2644
+ let bin;
2645
+ if (explicit) bin = explicit;
2646
+ else {
2647
+ const fromEnv = process.env["SHELL"];
2648
+ if (fromEnv) {
2649
+ const name = fromEnv.split("/").pop() ?? "";
2650
+ if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) bin = fromEnv;
2651
+ else bin = "/bin/bash";
2652
+ } else bin = "/bin/bash";
2653
+ }
2654
+ plan = { bin, argv: ["-c"], useStdin: false, stdinBody: void 0 };
2655
+ }
2656
+ const shell = plan.bin;
2657
+ const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
2468
2658
  const env = buildChildEnv(ctx.session?.id);
2469
2659
  const detached = !isWin3;
2470
2660
  const startedAt = Date.now();
@@ -2474,7 +2664,9 @@ var bashTool = {
2474
2664
  const child2 = spawn(shell, args, {
2475
2665
  cwd: ctx.projectRoot,
2476
2666
  env,
2477
- stdio: ["ignore", "pipe", "pipe"],
2667
+ // PowerShell takes the script on stdin (no argv quoting); cmd.exe
2668
+ // and POSIX shells ignore stdin when given the command inline.
2669
+ stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
2478
2670
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
2479
2671
  // DETACHED_PROCESS (detached: true) is set, so the console-less
2480
2672
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -2485,6 +2677,13 @@ var bashTool = {
2485
2677
  detached: !isWin3,
2486
2678
  windowsHide: true
2487
2679
  });
2680
+ if (plan.useStdin) {
2681
+ try {
2682
+ child2.stdin?.write(plan.stdinBody ?? input.command);
2683
+ child2.stdin?.end();
2684
+ } catch {
2685
+ }
2686
+ }
2488
2687
  const pid2 = child2.pid;
2489
2688
  if (typeof pid2 === "number") {
2490
2689
  registry.register({
@@ -2539,11 +2738,20 @@ var bashTool = {
2539
2738
  const child = spawn(shell, args, {
2540
2739
  cwd: ctx.projectRoot,
2541
2740
  env,
2542
- stdio: ["ignore", "pipe", "pipe"],
2741
+ // PowerShell takes the script on stdin (no argv quoting); cmd.exe
2742
+ // and POSIX shells ignore stdin when given the command inline.
2743
+ stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
2543
2744
  detached,
2544
2745
  windowsHide: true,
2545
2746
  ...isWin3 ? {} : { signal: opts.signal }
2546
2747
  });
2748
+ if (plan.useStdin) {
2749
+ try {
2750
+ child.stdin?.write(plan.stdinBody ?? input.command);
2751
+ child.stdin?.end();
2752
+ } catch {
2753
+ }
2754
+ }
2547
2755
  const pid = child.pid;
2548
2756
  if (typeof pid === "number") {
2549
2757
  registry.register({
@@ -2694,10 +2902,13 @@ var bashTool = {
2694
2902
  yield { type: "partial_output", text: remainder };
2695
2903
  }
2696
2904
  const spooled = spool.finalize();
2905
+ const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
2697
2906
  yield {
2698
2907
  type: "final",
2699
2908
  output: {
2700
- output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
2909
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
2910
+
2911
+ ${hint}` : ""),
2701
2912
  exit_code: c.code,
2702
2913
  timed_out: timedOut
2703
2914
  }
@@ -2725,82 +2936,115 @@ var bashTool = {
2725
2936
  }
2726
2937
  }
2727
2938
  };
2728
- function resolveWin32Command(cmd) {
2729
- if (process.platform !== "win32") return cmd;
2730
- if (cmd.includes("/") || cmd.includes("\\") || path.extname(cmd.replace(/\//g, "\\"))) {
2731
- return cmd;
2732
- }
2733
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
2734
- const pathDirs = (process.env["PATH"] ?? "").split(path.delimiter);
2735
- for (const dir of pathDirs) {
2736
- const base = path.join(dir, cmd);
2737
- for (const ext of pathext) {
2738
- const full = `${base}${ext}`;
2739
- try {
2740
- fs8.accessSync(full, fs8.constants.X_OK);
2741
- return full;
2742
- } catch {
2743
- }
2744
- }
2745
- }
2746
- return cmd;
2747
- }
2748
- var WIN32_SHELL_META = /[&|<>\r\n\0]/;
2749
- function assertSafeWin32ShellArgs(args) {
2750
- for (const a of args) {
2751
- if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
2752
- throw new Error(
2753
- "win32 shell spawn: argument contains a shell metacharacter (one of & | < > or a newline) that could enable command injection through the .cmd/.bat wrapper \u2014 refusing to run. Offending argument: " + JSON.stringify(a)
2754
- );
2755
- }
2756
- }
2757
- }
2758
2939
 
2759
- // src/exec.ts
2940
+ // src/_session-shell.ts
2941
+ function normalizeShell(value) {
2942
+ const v = value?.trim().toLowerCase();
2943
+ if (v === "cmd" || v === "cmd.exe") return "cmd";
2944
+ if (v === "powershell" || v === "powershell.exe") return "powershell";
2945
+ if (v === "pwsh" || v === "pwsh.exe") return "pwsh";
2946
+ return void 0;
2947
+ }
2948
+ function resolveSessionShell(platform4, env, deps = {}) {
2949
+ if (platform4 !== "win32") return void 0;
2950
+ const override = normalizeShell(env.get("WRONGSTACK_SHELL"));
2951
+ if (override) return override;
2952
+ const hasBinary = deps.hasBinary ?? ((bin) => resolveWin32Command(bin) !== bin);
2953
+ if (hasBinary("pwsh.exe")) return "pwsh";
2954
+ if (hasBinary("powershell.exe")) return "powershell";
2955
+ return "cmd";
2956
+ }
2957
+ function ensureSessionShell(opts = {}) {
2958
+ const env = opts.env ?? process.env;
2959
+ const platform4 = opts.platform ?? process.platform;
2960
+ if (platform4 !== "win32") return void 0;
2961
+ const existing = normalizeShell(env["WRONGSTACK_SHELL"]);
2962
+ if (existing) return existing;
2963
+ const chosen = resolveSessionShell(platform4, { get: (k) => env[k] }, { hasBinary: opts.hasBinary }) ?? "cmd";
2964
+ env["WRONGSTACK_SHELL"] = chosen;
2965
+ return chosen;
2966
+ }
2760
2967
  var isWin = process.platform === "win32";
2761
- var ALLOWED_COMMANDS = {
2762
- node: ["--version", "-r", "--input-type=module"],
2763
- npm: ["--version", "list", "pkg", "doctor", "view", "outdated", "audit"],
2764
- pnpm: ["--version", "remove", "list", "view", "outdated", "audit"],
2765
- npx: ["--version"],
2766
- git: [
2767
- "--version",
2768
- "status",
2769
- "log",
2770
- "diff",
2771
- "branch",
2772
- "checkout",
2773
- "stash",
2774
- "add",
2775
- "commit",
2776
- "push",
2777
- "pull"
2778
- ],
2779
- ls: ["-la", "-l", "-a"],
2780
- cat: [],
2781
- head: ["-n"],
2782
- tail: ["-n"],
2783
- wc: ["-l", "-w", "-c"],
2784
- grep: [],
2785
- find: [],
2786
- echo: [],
2787
- mkdir: ["-p"],
2788
- cp: ["-r"],
2789
- mv: [],
2790
- rm: ["-rf"],
2791
- touch: [],
2792
- bun: ["--version"],
2793
- tsc: ["--version", "--noEmit", "--project"],
2794
- vitest: ["--version", "run", "--coverage"],
2795
- biome: ["--version", "lint", "format", "check"],
2796
- cargo: ["--version", "build", "test", "check"],
2797
- rustc: ["--version"],
2798
- go: ["version", "run", "build", "test"],
2799
- python: ["--version"],
2800
- pip: ["--version", "list"],
2801
- docker: ["--version", "ps", "images"],
2802
- kubectl: ["version", "get", "describe", "logs"]
2803
- };
2968
+ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
2969
+ // JS / TS toolchain
2970
+ "node",
2971
+ "npm",
2972
+ "pnpm",
2973
+ "yarn",
2974
+ "npx",
2975
+ "bun",
2976
+ "deno",
2977
+ "tsc",
2978
+ "vitest",
2979
+ "jest",
2980
+ "biome",
2981
+ "eslint",
2982
+ "prettier",
2983
+ // version control
2984
+ "git",
2985
+ // Rust
2986
+ "cargo",
2987
+ "rustc",
2988
+ // Go
2989
+ "go",
2990
+ // Python
2991
+ "python",
2992
+ "python3",
2993
+ "pip",
2994
+ "pip3",
2995
+ // Ruby
2996
+ "ruby",
2997
+ "gem",
2998
+ "bundle",
2999
+ // JVM
3000
+ "java",
3001
+ "javac",
3002
+ "mvn",
3003
+ "gradle",
3004
+ "gradlew",
3005
+ // .NET
3006
+ "dotnet",
3007
+ // C / C++ / native build
3008
+ "make",
3009
+ "cmake",
3010
+ // containers / orchestration (read-only subcommands; see BLOCKED_ARG_PATTERNS)
3011
+ "docker",
3012
+ "kubectl",
3013
+ // common POSIX file/text utilities
3014
+ "ls",
3015
+ "cat",
3016
+ "head",
3017
+ "tail",
3018
+ "wc",
3019
+ "grep",
3020
+ "find",
3021
+ "echo",
3022
+ "mkdir",
3023
+ "cp",
3024
+ "mv",
3025
+ "rm",
3026
+ "touch"
3027
+ ]);
3028
+ var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
3029
+ var normalizeCmd = (c) => c.trim();
3030
+ function configureExecPolicy(opts = {}) {
3031
+ const next = new Set(DEFAULT_ALLOWED_COMMANDS);
3032
+ for (const c of opts.allow ?? []) {
3033
+ const n = normalizeCmd(c);
3034
+ if (n) next.add(n);
3035
+ }
3036
+ for (const c of opts.deny ?? []) next.delete(normalizeCmd(c));
3037
+ allowedCommands = next;
3038
+ }
3039
+ function resetExecPolicy() {
3040
+ allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
3041
+ }
3042
+ function isExecCommandAllowed(cmd) {
3043
+ return allowedCommands.has(normalizeCmd(cmd));
3044
+ }
3045
+ function getExecAllowlist() {
3046
+ return [...allowedCommands].sort();
3047
+ }
2804
3048
  var MAX_ARGS = 20;
2805
3049
  var MAX_OUTPUT2 = 2e5;
2806
3050
  var DEFAULT_TIMEOUT_MS2 = 3e4;
@@ -2862,7 +3106,7 @@ var execTool = {
2862
3106
  name: "exec",
2863
3107
  category: "Shell",
2864
3108
  description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
2865
- usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be one of the allowed commands (node, npm, pnpm, git, tsc, eslint, vitest, etc.).\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- For anything that requires real shell features (pipes, complex redirection, arbitrary commands), fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
3109
+ usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
2866
3110
  permission: "confirm",
2867
3111
  mutating: true,
2868
3112
  riskTier: "standard",
@@ -2916,12 +3160,12 @@ var execTool = {
2916
3160
  truncated: false,
2917
3161
  allowed: false
2918
3162
  };
2919
- if (!(cmd in ALLOWED_COMMANDS)) {
3163
+ if (!isExecCommandAllowed(cmd)) {
2920
3164
  return {
2921
3165
  command: cmd,
2922
3166
  args: input.args ?? [],
2923
3167
  stdout: "",
2924
- stderr: `Command "${cmd}" not in allowlist. Use the bash tool for arbitrary commands.`,
3168
+ stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
2925
3169
  exitCode: 1,
2926
3170
  truncated: false,
2927
3171
  allowed: false
@@ -2964,19 +3208,58 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2964
3208
  let stdout = "";
2965
3209
  let stderr = "";
2966
3210
  let killed = false;
3211
+ const resolvedOnce = { value: false };
3212
+ const finish = (result) => {
3213
+ if (resolvedOnce.value) return;
3214
+ resolvedOnce.value = true;
3215
+ resolve6(result);
3216
+ };
2967
3217
  const startedAt = Date.now();
2968
3218
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
2969
3219
  const resolved = resolveWin32Command(cmd);
2970
3220
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2971
3221
  const spawnCmd = needsShell ? cmd : resolved;
2972
3222
  if (needsShell) assertSafeWin32ShellArgs(args);
2973
- const child = spawn(spawnCmd, args, {
2974
- cwd,
2975
- env: buildChildEnv(sessionId),
2976
- stdio: ["ignore", "pipe", "pipe"],
2977
- windowsHide: true,
2978
- ...isWin ? {} : { signal },
2979
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3223
+ let child;
3224
+ try {
3225
+ child = spawn(spawnCmd, args, {
3226
+ cwd,
3227
+ env: buildChildEnv(sessionId),
3228
+ stdio: ["ignore", "pipe", "pipe"],
3229
+ windowsHide: true,
3230
+ ...isWin ? {} : { signal },
3231
+ ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3232
+ });
3233
+ } catch (err) {
3234
+ spool.finalize();
3235
+ finish({
3236
+ command: cmd,
3237
+ args,
3238
+ stdout: "",
3239
+ stderr: `spawn failed: ${toErrorMessage$1(err)}`,
3240
+ exitCode: 1,
3241
+ truncated: false,
3242
+ allowed: true
3243
+ });
3244
+ return;
3245
+ }
3246
+ child.on("error", (err) => {
3247
+ const isAbort = err && err.code === "ABORT_ERR";
3248
+ const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
3249
+ clearTimeout(timer);
3250
+ if (isWin) signal.removeEventListener("abort", onAbort);
3251
+ if (typeof pid === "number") registry.unregister(pid);
3252
+ registry.afterCall(Date.now() - startedAt, true);
3253
+ spool.finalize();
3254
+ finish({
3255
+ command: cmd,
3256
+ args,
3257
+ stdout: normalizeCommandOutput(stdout),
3258
+ stderr: stderrText,
3259
+ exitCode: isAbort ? 124 : 1,
3260
+ truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
3261
+ allowed: true
3262
+ });
2980
3263
  });
2981
3264
  const registry = getProcessRegistry();
2982
3265
  const pid = child.pid;
@@ -3016,7 +3299,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3016
3299
  const exitCode = killed ? 124 : code ?? 1;
3017
3300
  registry.afterCall(durationMs, exitCode !== 0);
3018
3301
  const spooled = spool.finalize();
3019
- resolve6({
3302
+ finish({
3020
3303
  command: cmd,
3021
3304
  args,
3022
3305
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
@@ -3026,22 +3309,6 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3026
3309
  allowed: true
3027
3310
  });
3028
3311
  });
3029
- child.on("error", (err) => {
3030
- clearTimeout(timer);
3031
- if (isWin) signal.removeEventListener("abort", onAbort);
3032
- if (typeof pid === "number") registry.unregister(pid);
3033
- registry.afterCall(Date.now() - startedAt, true);
3034
- spool.finalize();
3035
- resolve6({
3036
- command: cmd,
3037
- args,
3038
- stdout: normalizeCommandOutput(stdout),
3039
- stderr: err.message,
3040
- exitCode: 1,
3041
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
3042
- allowed: true
3043
- });
3044
- });
3045
3312
  });
3046
3313
  }
3047
3314
  var TD = new TurndownService({
@@ -3406,7 +3673,7 @@ async function duckduckgoSearch(query2, num, signal) {
3406
3673
  truncated: results.length >= num
3407
3674
  };
3408
3675
  } catch (err) {
3409
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
3676
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$2(err) }));
3410
3677
  return {
3411
3678
  query: query2,
3412
3679
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -7645,7 +7912,7 @@ function loadDatabaseSync() {
7645
7912
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
7646
7913
  } catch (err) {
7647
7914
  throw new Error(
7648
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$1(err)}`
7915
+ `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$2(err)}`
7649
7916
  );
7650
7917
  }
7651
7918
  return DatabaseSyncCtor;
@@ -8982,11 +9249,15 @@ async function tryNativeParse(file, content) {
8982
9249
  const crateDir = path.join(toolsDir, "syn-parser");
8983
9250
  const tmpFile = path.join(crateDir, "src", "input.rs");
8984
9251
  await fs7.writeFile(tmpFile, content, "utf8");
8985
- const proc = spawn("cargo", ["run", "--manifest-path", path.join(toolsDir, "Cargo.toml")], {
8986
- cwd: process.cwd(),
8987
- stdio: ["pipe", "pipe", "pipe"],
8988
- windowsHide: true
8989
- });
9252
+ const proc = spawn(
9253
+ "cargo",
9254
+ ["run", "--manifest-path", path.join(toolsDir, "Cargo.toml")],
9255
+ {
9256
+ cwd: process.cwd(),
9257
+ stdio: ["pipe", "pipe", "pipe"],
9258
+ windowsHide: true
9259
+ }
9260
+ );
8990
9261
  let stdout = "";
8991
9262
  proc.stdout?.on("data", (chunk) => {
8992
9263
  stdout += chunk.toString();
@@ -10332,7 +10603,7 @@ var setWorkingDirTool = {
10332
10603
  } catch (err) {
10333
10604
  return {
10334
10605
  current: ctx.workingDir,
10335
- error: toErrorMessage$1(err)
10606
+ error: toErrorMessage$2(err)
10336
10607
  };
10337
10608
  }
10338
10609
  try {
@@ -10482,7 +10753,11 @@ var taskTool = {
10482
10753
  const newIds = new Set(input.tasks.map((t) => t.id));
10483
10754
  if (newIds.size !== input.tasks.length) {
10484
10755
  const seen = /* @__PURE__ */ new Set();
10485
- const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
10756
+ const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => {
10757
+ if (seen.has(id)) return true;
10758
+ seen.add(id);
10759
+ return false;
10760
+ }))];
10486
10761
  early = {
10487
10762
  ok: false,
10488
10763
  message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
@@ -10517,7 +10792,7 @@ var taskTool = {
10517
10792
  }
10518
10793
  case "add": {
10519
10794
  const t = input.task;
10520
- if (!t || !t.title) {
10795
+ if (!t?.title) {
10521
10796
  early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
10522
10797
  return f;
10523
10798
  }
@@ -10974,6 +11249,6 @@ var TOOL_ICON_CONFIG = {
10974
11249
  };
10975
11250
  var FALLBACK_ICON = "fallback";
10976
11251
 
10977
- export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, resetPersistentProcessRegistry, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
11252
+ export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
10978
11253
  //# sourceMappingURL=index.js.map
10979
11254
  //# sourceMappingURL=index.js.map