@wrongstack/tools 0.273.0 → 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';
@@ -2379,6 +2380,155 @@ async function checkAndBlockKillCommand(command) {
2379
2380
  }
2380
2381
  return { blocked: false };
2381
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
+ }
2382
2532
 
2383
2533
  // src/bash.ts
2384
2534
  var MAX_OUTPUT = 32768;
@@ -2475,18 +2625,36 @@ var bashTool = {
2475
2625
  }
2476
2626
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
2477
2627
  const isWin3 = os2.platform() === "win32";
2478
- const shell = (() => {
2479
- const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
2480
- if (explicit) return explicit;
2481
- if (isWin3) return process.env["COMSPEC"] ?? "cmd.exe";
2482
- const fromEnv = process.env["SHELL"];
2483
- if (fromEnv) {
2484
- const name = fromEnv.split("/").pop() ?? "";
2485
- if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) return fromEnv;
2486
- }
2487
- return "/bin/bash";
2488
- })();
2489
- 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];
2490
2658
  const env = buildChildEnv(ctx.session?.id);
2491
2659
  const detached = !isWin3;
2492
2660
  const startedAt = Date.now();
@@ -2496,7 +2664,9 @@ var bashTool = {
2496
2664
  const child2 = spawn(shell, args, {
2497
2665
  cwd: ctx.projectRoot,
2498
2666
  env,
2499
- 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"],
2500
2670
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
2501
2671
  // DETACHED_PROCESS (detached: true) is set, so the console-less
2502
2672
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -2507,6 +2677,13 @@ var bashTool = {
2507
2677
  detached: !isWin3,
2508
2678
  windowsHide: true
2509
2679
  });
2680
+ if (plan.useStdin) {
2681
+ try {
2682
+ child2.stdin?.write(plan.stdinBody ?? input.command);
2683
+ child2.stdin?.end();
2684
+ } catch {
2685
+ }
2686
+ }
2510
2687
  const pid2 = child2.pid;
2511
2688
  if (typeof pid2 === "number") {
2512
2689
  registry.register({
@@ -2561,11 +2738,20 @@ var bashTool = {
2561
2738
  const child = spawn(shell, args, {
2562
2739
  cwd: ctx.projectRoot,
2563
2740
  env,
2564
- 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"],
2565
2744
  detached,
2566
2745
  windowsHide: true,
2567
2746
  ...isWin3 ? {} : { signal: opts.signal }
2568
2747
  });
2748
+ if (plan.useStdin) {
2749
+ try {
2750
+ child.stdin?.write(plan.stdinBody ?? input.command);
2751
+ child.stdin?.end();
2752
+ } catch {
2753
+ }
2754
+ }
2569
2755
  const pid = child.pid;
2570
2756
  if (typeof pid === "number") {
2571
2757
  registry.register({
@@ -2716,10 +2902,13 @@ var bashTool = {
2716
2902
  yield { type: "partial_output", text: remainder };
2717
2903
  }
2718
2904
  const spooled = spool.finalize();
2905
+ const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
2719
2906
  yield {
2720
2907
  type: "final",
2721
2908
  output: {
2722
- output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
2909
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
2910
+
2911
+ ${hint}` : ""),
2723
2912
  exit_code: c.code,
2724
2913
  timed_out: timedOut
2725
2914
  }
@@ -2747,82 +2936,115 @@ var bashTool = {
2747
2936
  }
2748
2937
  }
2749
2938
  };
2750
- function resolveWin32Command(cmd) {
2751
- if (process.platform !== "win32") return cmd;
2752
- if (cmd.includes("/") || cmd.includes("\\") || path.extname(cmd.replace(/\//g, "\\"))) {
2753
- return cmd;
2754
- }
2755
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
2756
- const pathDirs = (process.env["PATH"] ?? "").split(path.delimiter);
2757
- for (const dir of pathDirs) {
2758
- const base = path.join(dir, cmd);
2759
- for (const ext of pathext) {
2760
- const full = `${base}${ext}`;
2761
- try {
2762
- fs8.accessSync(full, fs8.constants.X_OK);
2763
- return full;
2764
- } catch {
2765
- }
2766
- }
2767
- }
2768
- return cmd;
2769
- }
2770
- var WIN32_SHELL_META = /[&|<>\r\n\0]/;
2771
- function assertSafeWin32ShellArgs(args) {
2772
- for (const a of args) {
2773
- if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
2774
- throw new Error(
2775
- "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)
2776
- );
2777
- }
2778
- }
2779
- }
2780
2939
 
2781
- // 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
+ }
2782
2967
  var isWin = process.platform === "win32";
2783
- var ALLOWED_COMMANDS = {
2784
- node: ["--version", "-r", "--input-type=module"],
2785
- npm: ["--version", "list", "pkg", "doctor", "view", "outdated", "audit"],
2786
- pnpm: ["--version", "remove", "list", "view", "outdated", "audit"],
2787
- npx: ["--version"],
2788
- git: [
2789
- "--version",
2790
- "status",
2791
- "log",
2792
- "diff",
2793
- "branch",
2794
- "checkout",
2795
- "stash",
2796
- "add",
2797
- "commit",
2798
- "push",
2799
- "pull"
2800
- ],
2801
- ls: ["-la", "-l", "-a"],
2802
- cat: [],
2803
- head: ["-n"],
2804
- tail: ["-n"],
2805
- wc: ["-l", "-w", "-c"],
2806
- grep: [],
2807
- find: [],
2808
- echo: [],
2809
- mkdir: ["-p"],
2810
- cp: ["-r"],
2811
- mv: [],
2812
- rm: ["-rf"],
2813
- touch: [],
2814
- bun: ["--version"],
2815
- tsc: ["--version", "--noEmit", "--project"],
2816
- vitest: ["--version", "run", "--coverage"],
2817
- biome: ["--version", "lint", "format", "check"],
2818
- cargo: ["--version", "build", "test", "check"],
2819
- rustc: ["--version"],
2820
- go: ["version", "run", "build", "test"],
2821
- python: ["--version"],
2822
- pip: ["--version", "list"],
2823
- docker: ["--version", "ps", "images"],
2824
- kubectl: ["version", "get", "describe", "logs"]
2825
- };
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
+ }
2826
3048
  var MAX_ARGS = 20;
2827
3049
  var MAX_OUTPUT2 = 2e5;
2828
3050
  var DEFAULT_TIMEOUT_MS2 = 3e4;
@@ -2884,7 +3106,7 @@ var execTool = {
2884
3106
  name: "exec",
2885
3107
  category: "Shell",
2886
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.",
2887
- 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.",
2888
3110
  permission: "confirm",
2889
3111
  mutating: true,
2890
3112
  riskTier: "standard",
@@ -2938,12 +3160,12 @@ var execTool = {
2938
3160
  truncated: false,
2939
3161
  allowed: false
2940
3162
  };
2941
- if (!(cmd in ALLOWED_COMMANDS)) {
3163
+ if (!isExecCommandAllowed(cmd)) {
2942
3164
  return {
2943
3165
  command: cmd,
2944
3166
  args: input.args ?? [],
2945
3167
  stdout: "",
2946
- 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.`,
2947
3169
  exitCode: 1,
2948
3170
  truncated: false,
2949
3171
  allowed: false
@@ -2986,19 +3208,58 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2986
3208
  let stdout = "";
2987
3209
  let stderr = "";
2988
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
+ };
2989
3217
  const startedAt = Date.now();
2990
3218
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
2991
3219
  const resolved = resolveWin32Command(cmd);
2992
3220
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2993
3221
  const spawnCmd = needsShell ? cmd : resolved;
2994
3222
  if (needsShell) assertSafeWin32ShellArgs(args);
2995
- const child = spawn(spawnCmd, args, {
2996
- cwd,
2997
- env: buildChildEnv(sessionId),
2998
- stdio: ["ignore", "pipe", "pipe"],
2999
- windowsHide: true,
3000
- ...isWin ? {} : { signal },
3001
- ...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
+ });
3002
3263
  });
3003
3264
  const registry = getProcessRegistry();
3004
3265
  const pid = child.pid;
@@ -3038,7 +3299,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3038
3299
  const exitCode = killed ? 124 : code ?? 1;
3039
3300
  registry.afterCall(durationMs, exitCode !== 0);
3040
3301
  const spooled = spool.finalize();
3041
- resolve6({
3302
+ finish({
3042
3303
  command: cmd,
3043
3304
  args,
3044
3305
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
@@ -3048,22 +3309,6 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3048
3309
  allowed: true
3049
3310
  });
3050
3311
  });
3051
- child.on("error", (err) => {
3052
- clearTimeout(timer);
3053
- if (isWin) signal.removeEventListener("abort", onAbort);
3054
- if (typeof pid === "number") registry.unregister(pid);
3055
- registry.afterCall(Date.now() - startedAt, true);
3056
- spool.finalize();
3057
- resolve6({
3058
- command: cmd,
3059
- args,
3060
- stdout: normalizeCommandOutput(stdout),
3061
- stderr: err.message,
3062
- exitCode: 1,
3063
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
3064
- allowed: true
3065
- });
3066
- });
3067
3312
  });
3068
3313
  }
3069
3314
  var TD = new TurndownService({
@@ -3428,7 +3673,7 @@ async function duckduckgoSearch(query2, num, signal) {
3428
3673
  truncated: results.length >= num
3429
3674
  };
3430
3675
  } catch (err) {
3431
- 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) }));
3432
3677
  return {
3433
3678
  query: query2,
3434
3679
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -7667,7 +7912,7 @@ function loadDatabaseSync() {
7667
7912
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
7668
7913
  } catch (err) {
7669
7914
  throw new Error(
7670
- `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)}`
7671
7916
  );
7672
7917
  }
7673
7918
  return DatabaseSyncCtor;
@@ -10358,7 +10603,7 @@ var setWorkingDirTool = {
10358
10603
  } catch (err) {
10359
10604
  return {
10360
10605
  current: ctx.workingDir,
10361
- error: toErrorMessage$1(err)
10606
+ error: toErrorMessage$2(err)
10362
10607
  };
10363
10608
  }
10364
10609
  try {
@@ -11004,6 +11249,6 @@ var TOOL_ICON_CONFIG = {
11004
11249
  };
11005
11250
  var FALLBACK_ICON = "fallback";
11006
11251
 
11007
- 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 };
11008
11253
  //# sourceMappingURL=index.js.map
11009
11254
  //# sourceMappingURL=index.js.map