@wrongstack/tools 0.305.1 → 0.306.0

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.
Files changed (61) hide show
  1. package/dist/_shell-pick.d.ts +4 -5
  2. package/dist/_util.d.ts +22 -5
  3. package/dist/audit.d.ts +0 -1
  4. package/dist/audit.js +135 -46
  5. package/dist/bash.js +61 -37
  6. package/dist/browser/index.js +29 -9
  7. package/dist/browser/types.d.ts +7 -1
  8. package/dist/builtin.js +1279 -726
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.js +223 -152
  11. package/dist/diff.d.ts +5 -0
  12. package/dist/diff.js +78 -12
  13. package/dist/document.js +18 -6
  14. package/dist/edit.js +69 -16
  15. package/dist/exec.js +44 -22
  16. package/dist/fetch.js +13 -1
  17. package/dist/format.d.ts +4 -2
  18. package/dist/format.js +81 -31
  19. package/dist/glob.js +12 -4
  20. package/dist/grep.d.ts +2 -0
  21. package/dist/grep.js +15 -4
  22. package/dist/index.js +1342 -761
  23. package/dist/install.js +96 -37
  24. package/dist/kanban-tool-types.d.ts +6 -1
  25. package/dist/kanban.js +60 -0
  26. package/dist/languages/index.js +28 -13
  27. package/dist/lint.js +28 -13
  28. package/dist/logs.d.ts +0 -1
  29. package/dist/logs.js +44 -13
  30. package/dist/memory.d.ts +8 -0
  31. package/dist/memory.js +23 -3
  32. package/dist/mode.d.ts +1 -1
  33. package/dist/mode.js +3 -0
  34. package/dist/next-steps.d.ts +2 -3
  35. package/dist/next-steps.js +3 -3
  36. package/dist/outdated.d.ts +0 -3
  37. package/dist/outdated.js +89 -48
  38. package/dist/pack.js +1279 -726
  39. package/dist/plan.js +76 -3
  40. package/dist/process-registry.d.ts +8 -2
  41. package/dist/process-registry.js +28 -13
  42. package/dist/ps-slash.js +22 -12
  43. package/dist/read.js +10 -3
  44. package/dist/replace.d.ts +4 -0
  45. package/dist/replace.js +104 -7
  46. package/dist/search.d.ts +6 -0
  47. package/dist/search.js +47 -26
  48. package/dist/skill.d.ts +6 -0
  49. package/dist/skill.js +9 -10
  50. package/dist/task.js +66 -2
  51. package/dist/test.js +28 -13
  52. package/dist/todo.js +64 -2
  53. package/dist/tool-icons.js +4 -2
  54. package/dist/tool-summary.d.ts +1 -1
  55. package/dist/tool-summary.js +76 -1
  56. package/dist/tool-tier.js +1279 -726
  57. package/dist/tree.js +9 -10
  58. package/dist/typecheck.d.ts +0 -2
  59. package/dist/typecheck.js +98 -31
  60. package/dist/write.js +58 -10
  61. package/package.json +4 -4
@@ -77,11 +77,10 @@ export declare function pickShell(platform: NodeJS.Platform, command: string, en
77
77
  * `-replace`, `-split`, `-join`, `-is`, `-as`, `-f`).
78
78
  * - `.ps1` extension mentioned in the command.
79
79
  * - `&` call operator followed by a `$`-variable (`& $cmd ...`).
80
- * - Common PowerShell aliases that don't exist in cmd.exe: `gci`, `gi`,
81
- * `gp`, `sl`, `cd` is ambiguous (cmd.exe has `cd` too skip), `ls`
82
- * ambiguous (cmd.exe does NOT have `ls`), `cat`, `cp`, `mv`, `rm`,
83
- * `echo` (echo is in cmd too), `gcm`, `gci`, `gps`, `ps`, `select`,
84
- * `where`, `?`, `%`.
80
+ * - PowerShell-only aliases: `gci`, `gi`, `gp`, `gcm`, `gps`. Unix-style
81
+ * names (`ls`, `cat`, `cp`, `mv`, `rm`, `sl`) are deliberately NOT
82
+ * treated as PowerShell tells they are Git-Bash/MSYS-normal on
83
+ * Windows and their PS alias semantics differ (`rm -rf` fails in PS).
85
84
  * - `#requires` directive at start of script.
86
85
  * - `param()` block at start of script.
87
86
  *
package/dist/_util.d.ts CHANGED
@@ -8,12 +8,19 @@ export declare function sha256hex(content: string): string;
8
8
  /** Detected package manager for a project directory. */
9
9
  export type PackageManager = 'pnpm' | 'yarn' | 'npm';
10
10
  /**
11
- * Detect the project's package manager by inspecting lockfiles in `cwd`.
12
- * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall
13
- * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that
14
- * happens to be empty never aborts the tool.
11
+ * Detect the project's package manager.
12
+ *
13
+ * Per directory, precedence is: `package.json#packageManager` (authoritative
14
+ * when declared) `pnpm-lock.yaml` `yarn.lock` `bun.lockb`/`bun.lock`
15
+ * (bun is treated as npm-compatible and reported as `npm`) →
16
+ * `package-lock.json`/`npm-shrinkwrap.json`. When `stopAt` (usually the
17
+ * project root) is provided and `cwd` is nested inside it, parent directories
18
+ * are walked up to and including `stopAt` — monorepo packages rarely carry
19
+ * their own lockfile. Missing or unreadable directories fall back to `npm`
20
+ * rather than throwing, so a `safeResolve`-checked cwd that happens to be
21
+ * empty never aborts the tool.
15
22
  */
16
- export declare function detectPackageManager(cwd: string): Promise<PackageManager>;
23
+ export declare function detectPackageManager(cwd: string, stopAt?: string): Promise<PackageManager>;
17
24
  export declare function resolvePath(input: string, ctx: Context): string;
18
25
  export declare function ensureInsideRoot(absPath: string, ctx: Context): string;
19
26
  export declare function safeResolve(input: string, ctx: Context): string;
@@ -62,6 +69,16 @@ export declare function resolveRealInsideRoot(absPath: string, ctx: Context): Pr
62
69
  * caller then opened.
63
70
  */
64
71
  export declare function safeResolveReal(input: string, ctx: Context): Promise<string>;
72
+ /**
73
+ * Truncate a diff (or similar text payload) to `maxBytes`, cutting at a line
74
+ * boundary and appending an explicit marker. Used by the mutating file tools
75
+ * (`write`, `edit`, `replace`) so their returned diffs stay inside the tool's
76
+ * declared `maxOutputBytes` budget instead of relying on downstream clipping.
77
+ */
78
+ export declare function truncateDiffPayload(diff: string, maxBytes: number): {
79
+ text: string;
80
+ truncated: boolean;
81
+ };
65
82
  export declare function truncateMiddle(s: string, max: number): string;
66
83
  export declare function isBinaryBuffer(buf: Buffer): boolean;
67
84
  /** Unified byte cap for all command tool output fed to the model. */
package/dist/audit.d.ts CHANGED
@@ -3,7 +3,6 @@ interface AuditInput {
3
3
  cwd?: string | undefined;
4
4
  level?: 'low' | 'moderate' | 'high' | 'critical' | undefined;
5
5
  fix?: boolean | undefined;
6
- packages?: string | string[] | undefined;
7
6
  }
8
7
  interface AuditVulnerability {
9
8
  severity: string;
package/dist/audit.js CHANGED
@@ -364,8 +364,11 @@ var SENSITIVE_FLAG_PATTERNS = [
364
364
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
365
365
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
366
366
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
367
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
368
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
369
+ // redacted, not just the first.
367
370
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
368
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
371
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
369
372
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
370
373
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
371
374
  // redaction function. Synced with core copy.
@@ -373,8 +376,9 @@ var SENSITIVE_FLAG_PATTERNS = [
373
376
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
374
377
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
375
378
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
376
- // when preceded by a flag name (e.g. --github-token=EyJ...).
377
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
379
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
380
+ // every such flag in the command line is redacted, not just the first.
381
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
378
382
  ];
379
383
  function redactCommand(cmd) {
380
384
  let result = cmd;
@@ -463,11 +467,15 @@ var ProcessRegistryImpl = class {
463
467
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
464
468
  }
465
469
  _canSignalProcessGroup(p) {
466
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
470
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
467
471
  }
468
472
  _killChildDirect(p, signal) {
469
473
  try {
470
- p.child.kill(signal);
474
+ if (p.child) {
475
+ p.child.kill(signal);
476
+ return;
477
+ }
478
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
471
479
  } catch {
472
480
  }
473
481
  }
@@ -665,15 +673,15 @@ var ProcessRegistryImpl = class {
665
673
  this._pruneStale(pid);
666
674
  const p = this.processes.get(pid);
667
675
  if (!p) return false;
668
- if (p.killed) return true;
676
+ if (p.killed && opts.force !== true) return true;
669
677
  if (p.protected && opts.includeProtected !== true) return false;
670
678
  if (opts.preserveBackground && p.background) return false;
671
679
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
672
680
  const isWin2 = os.platform() === "win32";
673
681
  if (isWin2) {
674
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
682
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
675
683
  const directFallback = () => {
676
- if (p.child.exitCode === null) {
684
+ if (p.child && p.child.exitCode === null) {
677
685
  try {
678
686
  p.child.kill("SIGKILL");
679
687
  } catch {
@@ -685,10 +693,7 @@ var ProcessRegistryImpl = class {
685
693
  onSettled: directFallback
686
694
  })) {
687
695
  } else {
688
- try {
689
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
690
- } catch {
691
- }
696
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
692
697
  }
693
698
  p.killed = true;
694
699
  return true;
@@ -699,7 +704,7 @@ var ProcessRegistryImpl = class {
699
704
  } else {
700
705
  this._killPosix(p, "SIGTERM");
701
706
  const timer = setTimeout(() => {
702
- if (this.processes.has(pid) && !p.child.killed) {
707
+ if (this.processes.has(pid) && !p.child?.killed) {
703
708
  this._killPosix(p, "SIGKILL");
704
709
  }
705
710
  }, graceMs);
@@ -754,6 +759,16 @@ var ProcessRegistryImpl = class {
754
759
  * before reusing a PID, but we want to clean up before that becomes a risk.
755
760
  */
756
761
  _isStaleEntry(entry) {
762
+ if (entry.child === null) {
763
+ if (Date.now() - entry.startedAt <= 6e4) return false;
764
+ if (os.platform() === "win32") return false;
765
+ try {
766
+ process.kill(entry.pid, 0);
767
+ return false;
768
+ } catch (err) {
769
+ return err.code !== "EPERM";
770
+ }
771
+ }
757
772
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
758
773
  }
759
774
  /**
@@ -1028,19 +1043,48 @@ function utf8Prefix(text, maxBytes) {
1028
1043
  // src/_util.ts
1029
1044
  import * as path3 from "node:path";
1030
1045
  import * as Core from "@wrongstack/core/utils";
1031
- async function detectPackageManager(cwd) {
1032
- const { stat: stat5 } = await import("node:fs/promises");
1033
- try {
1034
- await stat5(`${cwd}/pnpm-lock.yaml`);
1035
- return "pnpm";
1036
- } catch {
1046
+ async function detectPackageManager(cwd, stopAt) {
1047
+ let dir = path3.resolve(cwd);
1048
+ const stop = stopAt ? path3.resolve(stopAt) : dir;
1049
+ for (; ; ) {
1050
+ const found = await detectPackageManagerInDir(dir);
1051
+ if (found) return found;
1052
+ if (dir === stop) break;
1053
+ const parent = path3.dirname(dir);
1054
+ const relParent = path3.relative(stop, parent);
1055
+ if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
1056
+ dir = parent;
1037
1057
  }
1058
+ return "npm";
1059
+ }
1060
+ async function detectPackageManagerInDir(dir) {
1061
+ const fs6 = await import("node:fs/promises");
1038
1062
  try {
1039
- await stat5(`${cwd}/yarn.lock`);
1040
- return "yarn";
1063
+ const raw = await fs6.readFile(path3.join(dir, "package.json"), "utf8");
1064
+ const declared = JSON.parse(raw).packageManager;
1065
+ if (typeof declared === "string") {
1066
+ const name = declared.split("@")[0] ?? "";
1067
+ if (name === "pnpm" || name === "yarn") return name;
1068
+ if (name === "npm" || name === "bun") return "npm";
1069
+ }
1041
1070
  } catch {
1042
1071
  }
1043
- return "npm";
1072
+ const lockfiles = [
1073
+ ["pnpm-lock.yaml", "pnpm"],
1074
+ ["yarn.lock", "yarn"],
1075
+ ["bun.lockb", "npm"],
1076
+ ["bun.lock", "npm"],
1077
+ ["package-lock.json", "npm"],
1078
+ ["npm-shrinkwrap.json", "npm"]
1079
+ ];
1080
+ for (const [file, manager] of lockfiles) {
1081
+ try {
1082
+ await fs6.stat(`${dir}/${file}`);
1083
+ return manager;
1084
+ } catch {
1085
+ }
1086
+ }
1087
+ return null;
1044
1088
  }
1045
1089
  function resolvePath(input, ctx) {
1046
1090
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
@@ -3798,11 +3842,18 @@ async function tryLegacyPackageOperation(operation, ctx, packages = []) {
3798
3842
  }
3799
3843
 
3800
3844
  // src/audit.ts
3845
+ var SEVERITY_RANK = {
3846
+ info: 0,
3847
+ low: 1,
3848
+ moderate: 2,
3849
+ high: 3,
3850
+ critical: 4
3851
+ };
3801
3852
  var auditTool = {
3802
3853
  name: "audit",
3803
3854
  category: "Package Management",
3804
3855
  description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
3805
- usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `fix` can attempt automatic remediation for some vulnerabilities.\nThis is one of the most important tools for supply chain security.",
3856
+ usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- This tool is read-only: to remediate, use `install` (or `language_package`) to upgrade the affected packages.\nThis is one of the most important tools for supply chain security.",
3806
3857
  permission: "confirm",
3807
3858
  mutating: false,
3808
3859
  capabilities: ["shell.restricted"],
@@ -3817,8 +3868,10 @@ var auditTool = {
3817
3868
  enum: ["low", "moderate", "high", "critical"],
3818
3869
  description: "Minimum severity level to report"
3819
3870
  },
3820
- fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
3821
- packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
3871
+ fix: {
3872
+ type: "boolean",
3873
+ description: "Deprecated and rejected \u2014 this tool is read-only and never modifies dependencies. Use `install` (or `language_package`) to remediate vulnerabilities."
3874
+ }
3822
3875
  }
3823
3876
  },
3824
3877
  async execute(input, ctx, opts) {
@@ -3832,6 +3885,11 @@ var auditTool = {
3832
3885
  return final;
3833
3886
  },
3834
3887
  async *executeStream(input, ctx, opts) {
3888
+ if (input.fix === true) {
3889
+ throw new Error(
3890
+ "audit: `fix: true` is not supported \u2014 this tool is read-only (mutating: false). To remediate vulnerabilities, upgrade the affected packages with the `install` tool (or `language_package` for non-JS ecosystems)."
3891
+ );
3892
+ }
3835
3893
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
3836
3894
  const bridge = await tryLegacyPackageOperation("package-audit", {
3837
3895
  cwd,
@@ -3861,13 +3919,11 @@ var auditTool = {
3861
3919
  };
3862
3920
  return;
3863
3921
  }
3864
- const manager = await detectPackageManager(cwd);
3922
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
3865
3923
  yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
3866
3924
  const args = ["audit", "--json"];
3867
- if (input.fix) args.push("--fix");
3868
- if (input.packages) {
3869
- const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
3870
- args.push(...pkgs.map((p) => p.trim()));
3925
+ if (input.level && (manager === "npm" || manager === "pnpm")) {
3926
+ args.push(`--audit-level=${input.level}`);
3871
3927
  }
3872
3928
  const result = yield* spawnStream({
3873
3929
  cmd: manager,
@@ -3876,10 +3932,16 @@ var auditTool = {
3876
3932
  signal: opts.signal,
3877
3933
  maxBytes: 1e5
3878
3934
  });
3879
- yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
3935
+ yield {
3936
+ type: "final",
3937
+ output: parseAuditOutput(result.stdout, result.exitCode, {
3938
+ level: input.level,
3939
+ spawnTruncated: result.truncated
3940
+ })
3941
+ };
3880
3942
  }
3881
3943
  };
3882
- function parseAuditOutput(json, exitCode) {
3944
+ function parseAuditOutput(json, exitCode, opts = {}) {
3883
3945
  if (!json) {
3884
3946
  return {
3885
3947
  exit_code: exitCode,
@@ -3890,18 +3952,14 @@ function parseAuditOutput(json, exitCode) {
3890
3952
  truncated: false
3891
3953
  };
3892
3954
  }
3955
+ const cappedOutput = normalizeCommandOutput(json);
3956
+ const truncated = opts.spawnTruncated === true || Buffer.byteLength(json, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
3893
3957
  try {
3894
3958
  const data = JSON.parse(json);
3895
- const advisories = [];
3896
- const ads = data.advisories ?? {};
3897
- for (const id of Object.keys(ads)) {
3898
- const adv = ads[id];
3899
- advisories.push({
3900
- severity: adv.severity ?? "unknown",
3901
- package: adv.module_name ?? id,
3902
- title: adv.title ?? "Unknown vulnerability",
3903
- url: adv.url ?? ""
3904
- });
3959
+ let advisories = extractAdvisories(data);
3960
+ const minRank = opts.level ? SEVERITY_RANK[opts.level] ?? 0 : 0;
3961
+ if (minRank > 0) {
3962
+ advisories = advisories.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= minRank);
3905
3963
  }
3906
3964
  const total = advisories.length;
3907
3965
  const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
@@ -3910,8 +3968,8 @@ function parseAuditOutput(json, exitCode) {
3910
3968
  vulnerabilities: advisories,
3911
3969
  total,
3912
3970
  summary,
3913
- output: json,
3914
- truncated: json.length > 1e5
3971
+ output: cappedOutput,
3972
+ truncated
3915
3973
  };
3916
3974
  } catch {
3917
3975
  return {
@@ -3919,11 +3977,42 @@ function parseAuditOutput(json, exitCode) {
3919
3977
  vulnerabilities: [],
3920
3978
  total: 0,
3921
3979
  summary: "Could not parse audit output",
3922
- output: json,
3923
- truncated: false
3980
+ output: cappedOutput,
3981
+ truncated
3924
3982
  };
3925
3983
  }
3926
3984
  }
3985
+ function extractAdvisories(data) {
3986
+ const advisories = [];
3987
+ const ads = data["advisories"];
3988
+ if (ads && typeof ads === "object") {
3989
+ for (const [id, value] of Object.entries(ads)) {
3990
+ const adv = value ?? {};
3991
+ advisories.push({
3992
+ severity: typeof adv["severity"] === "string" ? adv["severity"] : "unknown",
3993
+ package: typeof adv["module_name"] === "string" ? adv["module_name"] : id,
3994
+ title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
3995
+ url: typeof adv["url"] === "string" ? adv["url"] : ""
3996
+ });
3997
+ }
3998
+ return advisories;
3999
+ }
4000
+ const vulns = data["vulnerabilities"];
4001
+ if (vulns && typeof vulns === "object") {
4002
+ for (const [pkg, value] of Object.entries(vulns)) {
4003
+ const vuln = value ?? {};
4004
+ const via = Array.isArray(vuln["via"]) ? vuln["via"] : [];
4005
+ const detail = via.find((v) => !!v && typeof v === "object");
4006
+ advisories.push({
4007
+ severity: typeof vuln["severity"] === "string" ? vuln["severity"] : "unknown",
4008
+ package: pkg,
4009
+ title: detail && typeof detail["title"] === "string" ? detail["title"] : "Unknown vulnerability",
4010
+ url: detail && typeof detail["url"] === "string" ? detail["url"] : ""
4011
+ });
4012
+ }
4013
+ }
4014
+ return advisories;
4015
+ }
3927
4016
  export {
3928
4017
  auditTool
3929
4018
  };
package/dist/bash.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/bash.ts
2
2
  import { spawn as spawn2 } from "node:child_process";
3
3
  import * as os4 from "node:os";
4
+ import { StringDecoder } from "node:string_decoder";
4
5
  import {
5
6
  emitProcessCompleted,
6
7
  emitProcessOutput,
@@ -394,8 +395,11 @@ var SENSITIVE_FLAG_PATTERNS = [
394
395
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
395
396
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
396
397
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
398
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
399
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
400
+ // redacted, not just the first.
397
401
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
398
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
402
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
399
403
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
400
404
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
401
405
  // redaction function. Synced with core copy.
@@ -403,8 +407,9 @@ var SENSITIVE_FLAG_PATTERNS = [
403
407
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
404
408
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
405
409
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
406
- // when preceded by a flag name (e.g. --github-token=EyJ...).
407
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
410
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
411
+ // every such flag in the command line is redacted, not just the first.
412
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
408
413
  ];
409
414
  function redactCommand(cmd) {
410
415
  let result = cmd;
@@ -493,11 +498,15 @@ var ProcessRegistryImpl = class {
493
498
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
494
499
  }
495
500
  _canSignalProcessGroup(p) {
496
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
501
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
497
502
  }
498
503
  _killChildDirect(p, signal) {
499
504
  try {
500
- p.child.kill(signal);
505
+ if (p.child) {
506
+ p.child.kill(signal);
507
+ return;
508
+ }
509
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
501
510
  } catch {
502
511
  }
503
512
  }
@@ -695,15 +704,15 @@ var ProcessRegistryImpl = class {
695
704
  this._pruneStale(pid);
696
705
  const p = this.processes.get(pid);
697
706
  if (!p) return false;
698
- if (p.killed) return true;
707
+ if (p.killed && opts.force !== true) return true;
699
708
  if (p.protected && opts.includeProtected !== true) return false;
700
709
  if (opts.preserveBackground && p.background) return false;
701
710
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
702
711
  const isWin2 = os.platform() === "win32";
703
712
  if (isWin2) {
704
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
713
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
705
714
  const directFallback = () => {
706
- if (p.child.exitCode === null) {
715
+ if (p.child && p.child.exitCode === null) {
707
716
  try {
708
717
  p.child.kill("SIGKILL");
709
718
  } catch {
@@ -715,10 +724,7 @@ var ProcessRegistryImpl = class {
715
724
  onSettled: directFallback
716
725
  })) {
717
726
  } else {
718
- try {
719
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
720
- } catch {
721
- }
727
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
722
728
  }
723
729
  p.killed = true;
724
730
  return true;
@@ -729,7 +735,7 @@ var ProcessRegistryImpl = class {
729
735
  } else {
730
736
  this._killPosix(p, "SIGTERM");
731
737
  const timer = setTimeout(() => {
732
- if (this.processes.has(pid) && !p.child.killed) {
738
+ if (this.processes.has(pid) && !p.child?.killed) {
733
739
  this._killPosix(p, "SIGKILL");
734
740
  }
735
741
  }, graceMs);
@@ -784,6 +790,16 @@ var ProcessRegistryImpl = class {
784
790
  * before reusing a PID, but we want to clean up before that becomes a risk.
785
791
  */
786
792
  _isStaleEntry(entry) {
793
+ if (entry.child === null) {
794
+ if (Date.now() - entry.startedAt <= 6e4) return false;
795
+ if (os.platform() === "win32") return false;
796
+ try {
797
+ process.kill(entry.pid, 0);
798
+ return false;
799
+ } catch (err) {
800
+ return err.code !== "EPERM";
801
+ }
802
+ }
787
803
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
788
804
  }
789
805
  /**
@@ -1084,7 +1100,6 @@ var PersistentProcessRegistry = class {
1084
1100
  try {
1085
1101
  const data = await readRegistryFile(this.registryPath);
1086
1102
  data.instances.set(String(entry.pid), entry);
1087
- const child = null;
1088
1103
  this.baseRegistry.register({
1089
1104
  pid: entry.pid,
1090
1105
  name: entry.name,
@@ -1092,7 +1107,7 @@ var PersistentProcessRegistry = class {
1092
1107
  startedAt: entry.startedAt,
1093
1108
  sessionId: entry.sessionId,
1094
1109
  protected: entry.protected,
1095
- child
1110
+ child: null
1096
1111
  });
1097
1112
  await writeRegistryFile(this.registryPath, data);
1098
1113
  } finally {
@@ -1652,7 +1667,7 @@ function looksLikePowerShell(command) {
1652
1667
  return true;
1653
1668
  }
1654
1669
  if (PS_VERB_RE.test(trimmed)) return true;
1655
- if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
1670
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps)\b/i.test(trimmed)) {
1656
1671
  return true;
1657
1672
  }
1658
1673
  if (looksLikePowerShellExtended(command)) return true;
@@ -1782,7 +1797,7 @@ var bashTool = {
1782
1797
  name: "bash",
1783
1798
  category: "Shell",
1784
1799
  description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
1785
- usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
1800
+ usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the session working dir (changed via `set_working_dir`), defaulting to the project root.\n- Output may be truncated in the middle for very large results.",
1786
1801
  selection: {
1787
1802
  doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
1788
1803
  useInstead: ["exec"]
@@ -1796,7 +1811,14 @@ var bashTool = {
1796
1811
  // explicitly removes the implicit cross-tool aliasing.
1797
1812
  subjectKey: "command",
1798
1813
  capabilities: ["shell.arbitrary"],
1799
- timeoutMs: 3e5,
1814
+ // Executor-level abort ceiling. Must sit ABOVE the per-call `timeout_ms`
1815
+ // ceiling (600_000): the tool's own timer tree-kills and returns a
1816
+ // structured `timed_out: true` result, while the executor's
1817
+ // AbortSignal.timeout is a blunt abort. The old value (300_000) meant any
1818
+ // timeout_ms > 5min was silently cut short by the executor. The 10s margin
1819
+ // covers the kill/teardown window. (The executor additionally clamps to
1820
+ // config `tools.maxToolTimeoutMs`.)
1821
+ timeoutMs: 61e4,
1800
1822
  maxOutputBytes: MAX_OUTPUT,
1801
1823
  estimatedDurationMs: 3e4,
1802
1824
  inputSchema: {
@@ -1808,7 +1830,7 @@ var bashTool = {
1808
1830
  },
1809
1831
  timeout_ms: {
1810
1832
  type: "integer",
1811
- description: "Optional timeout for this specific command in milliseconds."
1833
+ description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
1812
1834
  },
1813
1835
  background: {
1814
1836
  type: "boolean",
@@ -1859,16 +1881,7 @@ var bashTool = {
1859
1881
  return;
1860
1882
  }
1861
1883
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
1862
- if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
1863
- console.warn(JSON.stringify({
1864
- level: "warn",
1865
- event: "bash.pipe_to_shell_detected",
1866
- message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
1867
- command_prefix: input.command.slice(0, 100),
1868
- // Log first 100 chars for review
1869
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1870
- }));
1871
- }
1884
+ const pipeToShellNote = PIPE_TO_SHELL_PATTERN.test(input.command) ? "\n\n[wrongstack] Caution: this command pipes output into a shell interpreter (pipe-to-shell). Piped content executes as arbitrary code \u2014 review the source before trusting the result, and prefer downloading to a file and inspecting it first." : "";
1872
1885
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1873
1886
  const isWin2 = os4.platform() === "win32";
1874
1887
  let plan;
@@ -1902,11 +1915,12 @@ var bashTool = {
1902
1915
  const shell = plan.bin;
1903
1916
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
1904
1917
  const env = buildChildEnv(ctx.session?.id);
1918
+ const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
1905
1919
  const detached = !isWin2;
1906
1920
  const startedAt = Date.now();
1907
1921
  if (input.background) {
1908
1922
  const child2 = spawn2(shell, args, {
1909
- cwd: ctx.projectRoot,
1923
+ cwd: spawnCwd,
1910
1924
  env,
1911
1925
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
1912
1926
  // and POSIX shells ignore stdin when given the command inline.
@@ -1937,7 +1951,7 @@ var bashTool = {
1937
1951
  parentPid: process.pid,
1938
1952
  command: redactCommand(`${shell} ${args.join(" ")}`),
1939
1953
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
1940
- cwd: ctx.projectRoot,
1954
+ cwd: spawnCwd,
1941
1955
  background: true,
1942
1956
  startedAt: new Date(startedAt).toISOString()
1943
1957
  });
@@ -1981,7 +1995,9 @@ var bashTool = {
1981
1995
  yield {
1982
1996
  type: "final",
1983
1997
  output: {
1984
- output: "",
1998
+ // Background runs have no captured output; the pipe-to-shell caution
1999
+ // (when present) is the only thing worth surfacing.
2000
+ output: pipeToShellNote.trim(),
1985
2001
  exit_code: null,
1986
2002
  timed_out: false,
1987
2003
  pid: pid2
@@ -1998,7 +2014,7 @@ var bashTool = {
1998
2014
  return;
1999
2015
  }
2000
2016
  const child = spawn2(shell, args, {
2001
- cwd: ctx.projectRoot,
2017
+ cwd: spawnCwd,
2002
2018
  env,
2003
2019
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
2004
2020
  // and POSIX shells ignore stdin when given the command inline.
@@ -2023,7 +2039,7 @@ var bashTool = {
2023
2039
  parentPid: process.pid,
2024
2040
  command: redactCommand(`${shell} ${args.join(" ")}`),
2025
2041
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
2026
- cwd: ctx.projectRoot,
2042
+ cwd: spawnCwd,
2027
2043
  background: false,
2028
2044
  startedAt: new Date(startedAt).toISOString()
2029
2045
  });
@@ -2142,8 +2158,10 @@ var bashTool = {
2142
2158
  child.stderr?.resume();
2143
2159
  }
2144
2160
  };
2161
+ const stdoutDecoder = new StringDecoder("utf8");
2162
+ const stderrDecoder = new StringDecoder("utf8");
2145
2163
  const onData = (chunk, stream) => {
2146
- const text = chunk.toString();
2164
+ const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
2147
2165
  if (stream === "stdout") stdoutBytes += chunk.byteLength;
2148
2166
  else stderrBytes += chunk.byteLength;
2149
2167
  emitProcessOutput({ pid, stream, chunk });
@@ -2170,6 +2188,12 @@ var bashTool = {
2170
2188
  if (typeof pid === "number") registry.unregister(pid);
2171
2189
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
2172
2190
  completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
2191
+ const tail = stdoutDecoder.end() + stderrDecoder.end();
2192
+ if (tail) {
2193
+ if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
2194
+ spool.write(tail);
2195
+ pending += tail;
2196
+ }
2173
2197
  push({ kind: "end", code });
2174
2198
  });
2175
2199
  try {
@@ -2189,7 +2213,7 @@ var bashTool = {
2189
2213
  output: {
2190
2214
  output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
2191
2215
 
2192
- ${hint}` : ""),
2216
+ ${hint}` : "") + pipeToShellNote,
2193
2217
  exit_code: c.code,
2194
2218
  timed_out: timedOut
2195
2219
  }
@@ -2251,7 +2275,7 @@ ${hint}` : ""),
2251
2275
  if (!sessionId) return;
2252
2276
  for (const entry of registry.bySession(sessionId)) {
2253
2277
  if (entry.name !== "bash") continue;
2254
- if (entry.child.exitCode !== null) continue;
2278
+ if (entry.child && entry.child.exitCode !== null) continue;
2255
2279
  if (entry.background) continue;
2256
2280
  if (entry.protected) continue;
2257
2281
  registry.kill(entry.pid, { force: true });