@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
package/dist/document.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/document.ts
2
2
  import * as fs from "node:fs/promises";
3
+ import * as path2 from "node:path";
3
4
 
4
5
  // src/_util.ts
5
6
  import * as path from "node:path";
@@ -30,8 +31,8 @@ function safeResolve(input, ctx) {
30
31
  var documentTool = {
31
32
  name: "document",
32
33
  category: "Project",
33
- description: "DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.",
34
- usageHint: "Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.",
34
+ description: "DEPRECATED \u2014 read-only preview stub that lists undocumented symbols as `skipped` candidates. It never writes files and does not generate real docstrings. If the auto-doc plugin is enabled, use its `auto_doc` tool (with `dry_run: true` to preview) instead.",
35
+ usageHint: "Deprecated: this tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc and writes nothing. When the auto-doc plugin is enabled, prefer its `auto_doc` tool (`dry_run: true` for previewing, without it for writing).",
35
36
  permission: "auto",
36
37
  mutating: false,
37
38
  timeoutMs: 3e4,
@@ -67,7 +68,11 @@ var documentTool = {
67
68
  const results = [];
68
69
  let filesProcessed = 0;
69
70
  let itemsDocumented = 0;
70
- const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
71
+ const fileList = input.files ? await resolveFiles(
72
+ Array.isArray(input.files) ? input.files.join(",") : input.files,
73
+ cwd,
74
+ ctx
75
+ ) : input.path ? [safeResolve(input.path, ctx)] : [];
71
76
  for (const absPath of fileList) {
72
77
  try {
73
78
  const content = await fs.readFile(absPath, "utf8");
@@ -100,11 +105,18 @@ var documentTool = {
100
105
  };
101
106
  }
102
107
  };
103
- async function resolveFiles(filesInput, cwd) {
104
- const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
108
+ async function resolveFiles(filesInput, cwd, ctx) {
109
+ const files = filesInput.split(",");
105
110
  const resolved = [];
106
111
  for (const f of files) {
107
- const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
112
+ const entry = f.trim();
113
+ if (!entry) continue;
114
+ let absPath;
115
+ try {
116
+ absPath = ensureInsideRoot(path2.resolve(cwd, entry), ctx);
117
+ } catch {
118
+ continue;
119
+ }
108
120
  try {
109
121
  const stat2 = await fs.stat(absPath);
110
122
  if (stat2.isFile()) resolved.push(absPath);
package/dist/edit.js CHANGED
@@ -381,8 +381,35 @@ async function safeResolveReal(input, ctx) {
381
381
  const abs = safeResolve(input, ctx);
382
382
  return await resolveRealInsideRoot(abs, ctx);
383
383
  }
384
+ function truncateDiffPayload(diff, maxBytes) {
385
+ const total = Buffer.byteLength(diff, "utf8");
386
+ if (total <= maxBytes) return { text: diff, truncated: false };
387
+ const MARKER_RESERVE = 96;
388
+ let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
389
+ const nl = head.lastIndexOf("\n");
390
+ if (nl > 0) head = head.slice(0, nl);
391
+ const kept = Buffer.byteLength(head, "utf8");
392
+ return {
393
+ text: `${head}
394
+ \u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
395
+ truncated: true
396
+ };
397
+ }
398
+ function takeHeadBytes(s, maxBytes) {
399
+ if (maxBytes <= 0) return "";
400
+ if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
401
+ let lo = 0;
402
+ let hi = s.length;
403
+ while (lo < hi) {
404
+ const mid = Math.ceil((lo + hi) / 2);
405
+ if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
406
+ else hi = mid - 1;
407
+ }
408
+ return s.slice(0, lo);
409
+ }
384
410
 
385
411
  // src/edit.ts
412
+ var MAX_DIFF_BYTES = 262144;
386
413
  var editTool = {
387
414
  name: "edit",
388
415
  category: "Filesystem",
@@ -393,17 +420,33 @@ var editTool = {
393
420
  useInstead: ["write", "patch"]
394
421
  },
395
422
  permission: "confirm",
423
+ // WS-046: gives permission decisions something to key on — the file being
424
+ // edited, so trust rules can scope by path.
425
+ subjectKey: "path",
396
426
  mutating: true,
397
427
  capabilities: ["fs.write"],
398
428
  icon: "edit",
399
429
  timeoutMs: 5e3,
430
+ maxOutputBytes: 262144,
400
431
  inputSchema: {
401
432
  type: "object",
402
433
  properties: {
403
- path: { type: "string" },
404
- old_string: { type: "string" },
405
- new_string: { type: "string" },
406
- replace_all: { type: "boolean" }
434
+ path: {
435
+ type: "string",
436
+ description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
437
+ },
438
+ old_string: {
439
+ type: "string",
440
+ description: "The exact text to replace, including whitespace and indentation. Must be unique in the file unless `replace_all` is set \u2014 add surrounding lines to disambiguate."
441
+ },
442
+ new_string: {
443
+ type: "string",
444
+ description: "The exact replacement text (may be empty to delete `old_string`)."
445
+ },
446
+ replace_all: {
447
+ type: "boolean",
448
+ description: "Replace every occurrence instead of requiring a unique match. Only allowed when `old_string` matches exactly (or up to trailing whitespace) \u2014 fuzzy matches stay single-target."
449
+ }
407
450
  },
408
451
  required: ["path", "old_string", "new_string"]
409
452
  },
@@ -483,6 +526,9 @@ var editTool = {
483
526
  const oldLf = normalizeToLf(input.old_string);
484
527
  const newLf = normalizeToLf(input.new_string);
485
528
  if (oldLf === newLf) {
529
+ if (!fileLf.includes(oldLf)) {
530
+ throw noMatchError(input.path, fileLf, oldLf);
531
+ }
486
532
  if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
487
533
  return {
488
534
  path: absPath,
@@ -496,13 +542,7 @@ var editTool = {
496
542
  const ladder = findLadderMatches(fileLf, oldLf);
497
543
  if (!ladder) {
498
544
  opts?.signal?.throwIfAborted();
499
- const hint = nearestMatchHint(fileLf, oldLf);
500
- throw new ToolValidationError({
501
- message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
502
- ${hint.snippet}
503
- Compare this against your old_string and retry with the file's actual text.` : ""}`,
504
- field: "old_string"
505
- });
545
+ throw noMatchError(input.path, fileLf, oldLf);
506
546
  }
507
547
  const { tier, matches } = ladder;
508
548
  const count = matches.length;
@@ -564,16 +604,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
564
604
  after: newFile
565
605
  });
566
606
  opts?.signal?.throwIfAborted();
567
- const diff = unifiedDiff(original, newFile, {
568
- fromFile: input.path,
569
- toFile: input.path
570
- });
607
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
608
+ unifiedDiff(original, newFile, {
609
+ fromFile: input.path,
610
+ toFile: input.path
611
+ }),
612
+ MAX_DIFF_BYTES
613
+ );
614
+ const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
571
615
  const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
572
616
  let syntaxNote;
573
617
  if (syntax && syntax.errors.length > 0) {
574
618
  syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
575
619
  }
576
- const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
620
+ const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
577
621
  return {
578
622
  path: absPath,
579
623
  replacements: input.replace_all ? count : 1,
@@ -584,6 +628,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
584
628
  };
585
629
  }
586
630
  };
631
+ function noMatchError(inputPath, fileLf, oldLf) {
632
+ const hint = nearestMatchHint(fileLf, oldLf);
633
+ return new ToolValidationError({
634
+ message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
635
+ ${hint.snippet}
636
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
637
+ field: "old_string"
638
+ });
639
+ }
587
640
  export {
588
641
  editTool
589
642
  };
package/dist/exec.js CHANGED
@@ -829,8 +829,11 @@ var SENSITIVE_FLAG_PATTERNS = [
829
829
  /--(?: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,
830
830
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
831
831
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
832
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
833
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
834
+ // redacted, not just the first.
832
835
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
833
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
836
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
834
837
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
835
838
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
836
839
  // redaction function. Synced with core copy.
@@ -838,8 +841,9 @@ var SENSITIVE_FLAG_PATTERNS = [
838
841
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
839
842
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
840
843
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
841
- // when preceded by a flag name (e.g. --github-token=EyJ...).
842
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
844
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
845
+ // every such flag in the command line is redacted, not just the first.
846
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
843
847
  ];
844
848
  function redactCommand(cmd) {
845
849
  let result = cmd;
@@ -928,11 +932,15 @@ var ProcessRegistryImpl = class {
928
932
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
929
933
  }
930
934
  _canSignalProcessGroup(p) {
931
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
935
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
932
936
  }
933
937
  _killChildDirect(p, signal) {
934
938
  try {
935
- p.child.kill(signal);
939
+ if (p.child) {
940
+ p.child.kill(signal);
941
+ return;
942
+ }
943
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
936
944
  } catch {
937
945
  }
938
946
  }
@@ -1130,15 +1138,15 @@ var ProcessRegistryImpl = class {
1130
1138
  this._pruneStale(pid);
1131
1139
  const p = this.processes.get(pid);
1132
1140
  if (!p) return false;
1133
- if (p.killed) return true;
1141
+ if (p.killed && opts.force !== true) return true;
1134
1142
  if (p.protected && opts.includeProtected !== true) return false;
1135
1143
  if (opts.preserveBackground && p.background) return false;
1136
1144
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1137
1145
  const isWin3 = os.platform() === "win32";
1138
1146
  if (isWin3) {
1139
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
1147
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
1140
1148
  const directFallback = () => {
1141
- if (p.child.exitCode === null) {
1149
+ if (p.child && p.child.exitCode === null) {
1142
1150
  try {
1143
1151
  p.child.kill("SIGKILL");
1144
1152
  } catch {
@@ -1150,10 +1158,7 @@ var ProcessRegistryImpl = class {
1150
1158
  onSettled: directFallback
1151
1159
  })) {
1152
1160
  } else {
1153
- try {
1154
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
1155
- } catch {
1156
- }
1161
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
1157
1162
  }
1158
1163
  p.killed = true;
1159
1164
  return true;
@@ -1164,7 +1169,7 @@ var ProcessRegistryImpl = class {
1164
1169
  } else {
1165
1170
  this._killPosix(p, "SIGTERM");
1166
1171
  const timer = setTimeout(() => {
1167
- if (this.processes.has(pid) && !p.child.killed) {
1172
+ if (this.processes.has(pid) && !p.child?.killed) {
1168
1173
  this._killPosix(p, "SIGKILL");
1169
1174
  }
1170
1175
  }, graceMs);
@@ -1219,6 +1224,16 @@ var ProcessRegistryImpl = class {
1219
1224
  * before reusing a PID, but we want to clean up before that becomes a risk.
1220
1225
  */
1221
1226
  _isStaleEntry(entry) {
1227
+ if (entry.child === null) {
1228
+ if (Date.now() - entry.startedAt <= 6e4) return false;
1229
+ if (os.platform() === "win32") return false;
1230
+ try {
1231
+ process.kill(entry.pid, 0);
1232
+ return false;
1233
+ } catch (err) {
1234
+ return err.code !== "EPERM";
1235
+ }
1236
+ }
1222
1237
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
1223
1238
  }
1224
1239
  /**
@@ -1512,7 +1527,6 @@ var PersistentProcessRegistry = class {
1512
1527
  try {
1513
1528
  const data = await readRegistryFile(this.registryPath);
1514
1529
  data.instances.set(String(entry.pid), entry);
1515
- const child = null;
1516
1530
  this.baseRegistry.register({
1517
1531
  pid: entry.pid,
1518
1532
  name: entry.name,
@@ -1520,7 +1534,7 @@ var PersistentProcessRegistry = class {
1520
1534
  startedAt: entry.startedAt,
1521
1535
  sessionId: entry.sessionId,
1522
1536
  protected: entry.protected,
1523
- child
1537
+ child: null
1524
1538
  });
1525
1539
  await writeRegistryFile(this.registryPath, data);
1526
1540
  } finally {
@@ -2602,6 +2616,7 @@ function getExecAllowlist() {
2602
2616
  var MAX_ARGS = 20;
2603
2617
  var MAX_OUTPUT = 2e5;
2604
2618
  var DEFAULT_TIMEOUT_MS = 3e4;
2619
+ var MAX_TIMEOUT_MS = 6e5;
2605
2620
  var BLOCKED_ARG_PATTERNS = {
2606
2621
  python: [],
2607
2622
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
@@ -2727,8 +2742,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
2727
2742
  var execTool = {
2728
2743
  name: "exec",
2729
2744
  category: "Shell",
2730
- 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.",
2731
- 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.",
2745
+ description: "Execute a command from a **curated command roster** with argument validation and confirm gating. This is the **preferred** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It is NOT a sandbox \u2014 several rostered commands (node, python, powershell, \u2026) can run arbitrary code \u2014 so prefer least-privilege commands.",
2746
+ 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).\nThe curated roster + confirm gating narrows the surface compared to full shell access, but this is not a sandbox \u2014 prefer least-privilege commands.",
2732
2747
  selection: {
2733
2748
  doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
2734
2749
  useInstead: ["bash"]
@@ -2744,7 +2759,13 @@ var execTool = {
2744
2759
  subjectKey: "command",
2745
2760
  mutating: true,
2746
2761
  riskTier: "standard",
2747
- timeoutMs: DEFAULT_TIMEOUT_MS,
2762
+ // Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
2763
+ // (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
2764
+ // tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
2765
+ // otherwise fire first and discard the structured timeout result. The 10s
2766
+ // margin covers the kill/teardown window. (The executor additionally clamps
2767
+ // to config `tools.maxToolTimeoutMs`.)
2768
+ timeoutMs: MAX_TIMEOUT_MS + 1e4,
2748
2769
  capabilities: ["shell.restricted"],
2749
2770
  icon: "terminal",
2750
2771
  inputSchema: {
@@ -2765,7 +2786,7 @@ var execTool = {
2765
2786
  },
2766
2787
  timeout: {
2767
2788
  type: "integer",
2768
- description: "Per-command timeout in milliseconds."
2789
+ description: "Per-command timeout in milliseconds (default 30000, max 600000)."
2769
2790
  }
2770
2791
  },
2771
2792
  required: ["command"]
@@ -2809,7 +2830,7 @@ var execTool = {
2809
2830
  };
2810
2831
  }
2811
2832
  const args = (input.args ?? []).slice(0, MAX_ARGS);
2812
- const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
2833
+ const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS));
2813
2834
  const danger = detectDanger(cmd, args, dangerBypass);
2814
2835
  const killCheck = await checkExecKillCommand(cmd, args);
2815
2836
  if (killCheck.blocked) {
@@ -2837,15 +2858,16 @@ var execTool = {
2837
2858
  danger
2838
2859
  };
2839
2860
  }
2861
+ const defaultCwd = ctx.workingDir ?? ctx.cwd;
2840
2862
  let cwd;
2841
2863
  try {
2842
- cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(ctx.cwd, ctx);
2864
+ cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
2843
2865
  } catch {
2844
2866
  return {
2845
2867
  command: cmd,
2846
2868
  args,
2847
2869
  stdout: "",
2848
- stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
2870
+ stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
2849
2871
  exitCode: 1,
2850
2872
  truncated: false,
2851
2873
  allowed: false,
package/dist/fetch.js CHANGED
@@ -102,6 +102,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
102
102
  if (res.status < 300 || res.status > 399) {
103
103
  return res;
104
104
  }
105
+ try {
106
+ await res.body?.cancel();
107
+ } catch {
108
+ }
105
109
  redirectCount++;
106
110
  if (redirectCount > maxRedirects) {
107
111
  throw new FetchError({
@@ -184,6 +188,8 @@ TD.addRule("stripDangerousElements", {
184
188
  filter: ["script", "style", "noscript"],
185
189
  replacement: () => ""
186
190
  });
191
+ var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
192
+ TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
187
193
  var MAX_BYTES = 131072;
188
194
  var TIMEOUT_MS = 2e4;
189
195
  var combineSignals = (signals) => AbortSignal.any(signals);
@@ -213,7 +219,7 @@ var fetchTool = {
213
219
  format: {
214
220
  type: "string",
215
221
  enum: ["markdown", "text", "raw"],
216
- description: 'Output format. "markdown" is recommended for HTML pages.'
222
+ description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
217
223
  }
218
224
  },
219
225
  required: ["url"]
@@ -248,6 +254,12 @@ var fetchTool = {
248
254
  });
249
255
  }
250
256
  const u = new URL(input.url);
257
+ if (u.username || u.password) {
258
+ throw new ToolValidationError2({
259
+ message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
260
+ field: "url"
261
+ });
262
+ }
251
263
  if (u.protocol !== "https:" && u.protocol !== "http:") {
252
264
  throw new ToolValidationError2({
253
265
  message: `fetch: unsupported protocol "${u.protocol}"`,
package/dist/format.d.ts CHANGED
@@ -7,8 +7,10 @@ interface FormatInput {
7
7
  }
8
8
  interface FormatOutput {
9
9
  fixer: string;
10
- files_checked: number;
11
- files_changed: number;
10
+ /** Parsed from formatter output when confidently available; undefined otherwise. */
11
+ files_checked: number | undefined;
12
+ /** Parsed from formatter output when confidently available; undefined otherwise. */
13
+ files_changed: number | undefined;
12
14
  output: string;
13
15
  truncated: boolean;
14
16
  }
package/dist/format.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
  /**
@@ -3590,8 +3605,9 @@ var formatTool = {
3590
3605
  type: "final",
3591
3606
  output: {
3592
3607
  fixer: bridge.language,
3593
- files_checked: 0,
3594
- files_changed: run.summary.errors > 0 ? 0 : 1,
3608
+ // Language-bridge runs don't report per-file counts.
3609
+ files_checked: void 0,
3610
+ files_changed: void 0,
3595
3611
  output: normalizeCommandOutput(run.output || run.error || ""),
3596
3612
  truncated: run.truncated
3597
3613
  }
@@ -3618,11 +3634,14 @@ var formatTool = {
3618
3634
  text: `Running ${detected}\u2026`,
3619
3635
  data: { fixer: detected, check: !!input.check }
3620
3636
  };
3621
- const args = ["format", "--write"];
3622
- if (input.check) args[args.length - 1] = "--check";
3623
- if (input.files) {
3624
- const files = Array.isArray(input.files) ? input.files : input.files.split(",");
3625
- args.push("--", ...files.map((f) => f.trim()));
3637
+ const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
3638
+ let args;
3639
+ if (detected === "prettier") {
3640
+ args = [input.check ? "--check" : "--write"];
3641
+ args.push(...fileList.length > 0 ? fileList : ["."]);
3642
+ } else {
3643
+ args = ["format", input.check ? "--check" : "--write"];
3644
+ if (fileList.length > 0) args.push("--", ...fileList);
3626
3645
  }
3627
3646
  const result = yield* spawnStream({
3628
3647
  cmd: detected,
@@ -3631,32 +3650,63 @@ var formatTool = {
3631
3650
  signal: opts.signal,
3632
3651
  maxBytes: 1e5
3633
3652
  });
3634
- const changed = [...result.stdout.matchAll(/\bchanged\b/gi)].length;
3653
+ const combinedOut = `${result.stdout}
3654
+ ${result.stderr}`;
3655
+ const counts = parseFormatterCounts(detected, combinedOut);
3635
3656
  yield {
3636
3657
  type: "final",
3637
3658
  output: {
3638
3659
  fixer: detected,
3639
- files_checked: 0,
3640
- files_changed: changed,
3660
+ files_checked: counts.checked,
3661
+ files_changed: counts.changed,
3641
3662
  output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
3642
3663
  truncated: result.truncated
3643
3664
  }
3644
3665
  };
3645
3666
  }
3646
3667
  };
3668
+ function parseFormatterCounts(fixer, output) {
3669
+ if (fixer !== "biome") return { checked: void 0, changed: void 0 };
3670
+ const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
3671
+ const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
3672
+ return {
3673
+ checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
3674
+ changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
3675
+ };
3676
+ }
3647
3677
  async function detectFixer(cwd) {
3648
- const { stat: stat5 } = await import("node:fs/promises");
3649
- try {
3650
- await stat5(`${cwd}/biome.json`);
3651
- return "biome";
3652
- } catch {
3678
+ const fs6 = await import("node:fs/promises");
3679
+ const exists = async (file) => {
3653
3680
  try {
3654
- await stat5(`${cwd}/.prettierrc`);
3655
- return "prettier";
3681
+ await fs6.stat(`${cwd}/${file}`);
3682
+ return true;
3656
3683
  } catch {
3657
- return "biome";
3684
+ return false;
3658
3685
  }
3686
+ };
3687
+ if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
3688
+ const PRETTIER_CONFIGS = [
3689
+ ".prettierrc",
3690
+ ".prettierrc.json",
3691
+ ".prettierrc.yml",
3692
+ ".prettierrc.yaml",
3693
+ ".prettierrc.js",
3694
+ ".prettierrc.cjs",
3695
+ ".prettierrc.mjs",
3696
+ "prettier.config.js",
3697
+ "prettier.config.cjs",
3698
+ "prettier.config.mjs"
3699
+ ];
3700
+ for (const cfg of PRETTIER_CONFIGS) {
3701
+ if (await exists(cfg)) return "prettier";
3702
+ }
3703
+ try {
3704
+ const raw = await fs6.readFile(`${cwd}/package.json`, "utf8");
3705
+ const pkg = JSON.parse(raw);
3706
+ if (pkg["prettier"] !== void 0) return "prettier";
3707
+ } catch {
3659
3708
  }
3709
+ return "biome";
3660
3710
  }
3661
3711
  export {
3662
3712
  formatTool