@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/install.js CHANGED
@@ -371,8 +371,11 @@ var SENSITIVE_FLAG_PATTERNS = [
371
371
  /--(?: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,
372
372
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
373
373
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
374
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
375
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
376
+ // redacted, not just the first.
374
377
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
375
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
378
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
376
379
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
377
380
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
378
381
  // redaction function. Synced with core copy.
@@ -380,8 +383,9 @@ var SENSITIVE_FLAG_PATTERNS = [
380
383
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
381
384
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
382
385
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
383
- // when preceded by a flag name (e.g. --github-token=EyJ...).
384
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
386
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
387
+ // every such flag in the command line is redacted, not just the first.
388
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
385
389
  ];
386
390
  function redactCommand(cmd) {
387
391
  let result = cmd;
@@ -470,11 +474,15 @@ var ProcessRegistryImpl = class {
470
474
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
471
475
  }
472
476
  _canSignalProcessGroup(p) {
473
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
477
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
474
478
  }
475
479
  _killChildDirect(p, signal) {
476
480
  try {
477
- p.child.kill(signal);
481
+ if (p.child) {
482
+ p.child.kill(signal);
483
+ return;
484
+ }
485
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
478
486
  } catch {
479
487
  }
480
488
  }
@@ -672,15 +680,15 @@ var ProcessRegistryImpl = class {
672
680
  this._pruneStale(pid);
673
681
  const p = this.processes.get(pid);
674
682
  if (!p) return false;
675
- if (p.killed) return true;
683
+ if (p.killed && opts.force !== true) return true;
676
684
  if (p.protected && opts.includeProtected !== true) return false;
677
685
  if (opts.preserveBackground && p.background) return false;
678
686
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
679
687
  const isWin2 = os.platform() === "win32";
680
688
  if (isWin2) {
681
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
689
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
682
690
  const directFallback = () => {
683
- if (p.child.exitCode === null) {
691
+ if (p.child && p.child.exitCode === null) {
684
692
  try {
685
693
  p.child.kill("SIGKILL");
686
694
  } catch {
@@ -692,10 +700,7 @@ var ProcessRegistryImpl = class {
692
700
  onSettled: directFallback
693
701
  })) {
694
702
  } else {
695
- try {
696
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
697
- } catch {
698
- }
703
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
699
704
  }
700
705
  p.killed = true;
701
706
  return true;
@@ -706,7 +711,7 @@ var ProcessRegistryImpl = class {
706
711
  } else {
707
712
  this._killPosix(p, "SIGTERM");
708
713
  const timer = setTimeout(() => {
709
- if (this.processes.has(pid) && !p.child.killed) {
714
+ if (this.processes.has(pid) && !p.child?.killed) {
710
715
  this._killPosix(p, "SIGKILL");
711
716
  }
712
717
  }, graceMs);
@@ -761,6 +766,16 @@ var ProcessRegistryImpl = class {
761
766
  * before reusing a PID, but we want to clean up before that becomes a risk.
762
767
  */
763
768
  _isStaleEntry(entry) {
769
+ if (entry.child === null) {
770
+ if (Date.now() - entry.startedAt <= 6e4) return false;
771
+ if (os.platform() === "win32") return false;
772
+ try {
773
+ process.kill(entry.pid, 0);
774
+ return false;
775
+ } catch (err) {
776
+ return err.code !== "EPERM";
777
+ }
778
+ }
764
779
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
765
780
  }
766
781
  /**
@@ -1035,19 +1050,48 @@ function utf8Prefix(text, maxBytes) {
1035
1050
  // src/_util.ts
1036
1051
  import * as path3 from "node:path";
1037
1052
  import * as Core from "@wrongstack/core/utils";
1038
- async function detectPackageManager(cwd) {
1039
- const { stat: stat5 } = await import("node:fs/promises");
1040
- try {
1041
- await stat5(`${cwd}/pnpm-lock.yaml`);
1042
- return "pnpm";
1043
- } catch {
1053
+ async function detectPackageManager(cwd, stopAt) {
1054
+ let dir = path3.resolve(cwd);
1055
+ const stop = stopAt ? path3.resolve(stopAt) : dir;
1056
+ for (; ; ) {
1057
+ const found = await detectPackageManagerInDir(dir);
1058
+ if (found) return found;
1059
+ if (dir === stop) break;
1060
+ const parent = path3.dirname(dir);
1061
+ const relParent = path3.relative(stop, parent);
1062
+ if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
1063
+ dir = parent;
1044
1064
  }
1065
+ return "npm";
1066
+ }
1067
+ async function detectPackageManagerInDir(dir) {
1068
+ const fs6 = await import("node:fs/promises");
1045
1069
  try {
1046
- await stat5(`${cwd}/yarn.lock`);
1047
- return "yarn";
1070
+ const raw = await fs6.readFile(path3.join(dir, "package.json"), "utf8");
1071
+ const declared = JSON.parse(raw).packageManager;
1072
+ if (typeof declared === "string") {
1073
+ const name = declared.split("@")[0] ?? "";
1074
+ if (name === "pnpm" || name === "yarn") return name;
1075
+ if (name === "npm" || name === "bun") return "npm";
1076
+ }
1048
1077
  } catch {
1049
1078
  }
1050
- return "npm";
1079
+ const lockfiles = [
1080
+ ["pnpm-lock.yaml", "pnpm"],
1081
+ ["yarn.lock", "yarn"],
1082
+ ["bun.lockb", "npm"],
1083
+ ["bun.lock", "npm"],
1084
+ ["package-lock.json", "npm"],
1085
+ ["npm-shrinkwrap.json", "npm"]
1086
+ ];
1087
+ for (const [file, manager] of lockfiles) {
1088
+ try {
1089
+ await fs6.stat(`${dir}/${file}`);
1090
+ return manager;
1091
+ } catch {
1092
+ }
1093
+ }
1094
+ return null;
1051
1095
  }
1052
1096
  function resolvePath(input, ctx) {
1053
1097
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
@@ -3891,26 +3935,14 @@ var installTool = {
3891
3935
  return;
3892
3936
  }
3893
3937
  }
3894
- const pkgManager = await detectPackageManager(cwd);
3938
+ const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
3895
3939
  yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
3896
- const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
3897
3940
  const globalFlag = input.global ? ["-g"] : [];
3898
3941
  const ignoreScripts = input.lifecycleScripts !== true;
3899
- const args = [];
3900
- if (input.dry_run) args.push("--dry-run");
3901
- if (ignoreScripts) args.push("--ignore-scripts");
3902
- if (pkgManager === "pnpm") {
3903
- if (save) args.push(save);
3904
- args.push("add", ...globalFlag);
3905
- } else if (pkgManager === "yarn") {
3906
- args.push("add", ...globalFlag);
3907
- } else {
3908
- args.push("install", ...globalFlag);
3909
- }
3910
3942
  const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
3911
3943
  (p) => p.trim()
3912
3944
  ) : [];
3913
- const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
3945
+ const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
3914
3946
  for (const pkg of pkgList) {
3915
3947
  if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
3916
3948
  yield {
@@ -3926,7 +3958,34 @@ var installTool = {
3926
3958
  return;
3927
3959
  }
3928
3960
  }
3929
- if (pkgList.length > 0) args.push(...pkgList);
3961
+ const hasPkgs = pkgList.length > 0;
3962
+ const args = [];
3963
+ if (input.dry_run) args.push("--dry-run");
3964
+ if (ignoreScripts) args.push("--ignore-scripts");
3965
+ if (pkgManager === "pnpm") {
3966
+ if (hasPkgs) {
3967
+ if (input.save === "dev") args.push("-D");
3968
+ else if (input.save === "optional") args.push("-O");
3969
+ args.push("add", ...globalFlag);
3970
+ } else {
3971
+ args.push("install", ...globalFlag);
3972
+ }
3973
+ } else if (pkgManager === "yarn") {
3974
+ if (hasPkgs) {
3975
+ args.push("add", ...globalFlag);
3976
+ if (input.save === "dev") args.push("--dev");
3977
+ else if (input.save === "optional") args.push("--optional");
3978
+ } else {
3979
+ args.push("install", ...globalFlag);
3980
+ }
3981
+ } else {
3982
+ args.push("install", ...globalFlag);
3983
+ if (hasPkgs) {
3984
+ if (input.save === "dev") args.push("--save-dev");
3985
+ else if (input.save === "optional") args.push("--save-optional");
3986
+ }
3987
+ }
3988
+ if (hasPkgs) args.push(...pkgList);
3930
3989
  yield {
3931
3990
  type: "log",
3932
3991
  text: `Fetching ${pkgList.length || "all"} packages\u2026`,
@@ -1,6 +1,6 @@
1
1
  import type { SerializedTaskGraph } from '@wrongstack/core/types';
2
2
  import type { AssignKanbanTaskInput, KanbanAgentRunStatus, KanbanBoard, KanbanBoardSummary, KanbanCompletionGateEnforcement, KanbanContractEdgeType, KanbanContractEnforcement, KanbanContractGraph, KanbanContractGraphEnforcement, KanbanContractGraphEvaluation, KanbanContractNodeKind, KanbanContractNodeState, KanbanDecompositionSubtask, KanbanEvent, KanbanLifecycleStage, KanbanOrchestrationSnapshot, KanbanQueueHealth, KanbanSearchResult, KanbanTask, KanbanTaskPriority, KanbanTaskStatus, KanbanTaskType, KanbanVerificationReport, KanbanWorkbenchSnapshot } from '@wrongstack/kanban';
3
- export type KanbanAction = 'list_boards' | 'get_board' | 'create_board' | 'duplicate_board' | 'update_board' | 'adopt_managed_lifecycle' | 'release_managed_lifecycle' | 'delete_board' | 'generate_board' | 'export_markdown' | 'export_task_graph' | 'sync_task_graph' | 'create_from_graph' | 'import_session_tasks' | 'search_tasks' | 'ready_tasks' | 'snapshot' | 'workbench' | 'add_column' | 'update_column' | 'delete_column' | 'add_task' | 'split_task' | 'merge_tasks' | 'copy_task' | 'transfer_task' | 'get_task' | 'start_task' | 'update_task' | 'transition_task' | 'repair_managed_projection' | 'move_task' | 'delete_task' | 'set_chain' | 'get_chain' | 'claim_task' | 'release_task' | 'assign_task' | 'mark_assignment' | 'heartbeat_assignment' | 'recover_stale' | 'events' | 'queue_health' | 'add_dependency' | 'add_goal_metric' | 'update_goal_metric' | 'add_check' | 'update_check' | 'remove_check' | 'add_note' | 'add_link' | 'verify_completion' | 'split_atomic' | 'assess_atomicity' | 'propose_decomposition' | 'get_contract_graph' | 'configure_contract_graph' | 'upsert_contract_node' | 'remove_contract_node' | 'add_contract_edge' | 'remove_contract_edge';
3
+ export type KanbanAction = 'list_boards' | 'get_board' | 'create_board' | 'duplicate_board' | 'update_board' | 'adopt_managed_lifecycle' | 'release_managed_lifecycle' | 'delete_board' | 'generate_board' | 'export_markdown' | 'export_task_graph' | 'sync_task_graph' | 'create_from_graph' | 'import_session_tasks' | 'search_tasks' | 'ready_tasks' | 'snapshot' | 'workbench' | 'add_task' | 'split_task' | 'merge_tasks' | 'copy_task' | 'transfer_task' | 'get_task' | 'start_task' | 'update_task' | 'transition_task' | 'repair_managed_projection' | 'move_task' | 'delete_task' | 'set_chain' | 'get_chain' | 'claim_task' | 'release_task' | 'assign_task' | 'mark_assignment' | 'heartbeat_assignment' | 'recover_stale' | 'events' | 'queue_health' | 'add_dependency' | 'add_goal_metric' | 'update_goal_metric' | 'add_check' | 'update_check' | 'remove_check' | 'add_note' | 'add_link' | 'verify_completion' | 'split_atomic' | 'assess_atomicity' | 'propose_decomposition' | 'get_contract_graph' | 'configure_contract_graph' | 'upsert_contract_node' | 'remove_contract_node' | 'add_contract_edge' | 'remove_contract_edge';
4
4
  export interface KanbanToolInput extends Omit<AssignKanbanTaskInput, 'status'> {
5
5
  action: KanbanAction;
6
6
  boardId?: string | undefined;
@@ -101,6 +101,11 @@ export interface KanbanToolInput extends Omit<AssignKanbanTaskInput, 'status'> {
101
101
  lifecycleStage?: KanbanLifecycleStage | undefined;
102
102
  transitionAction?: string | undefined;
103
103
  transitionComment?: string | undefined;
104
+ /** `transition_task`: flip one or more manual criteria to `passed` before the gate fires. */
105
+ tickChecks?: {
106
+ checkId: string;
107
+ checkStatus: 'passed' | 'failed' | 'skipped';
108
+ }[] | undefined;
104
109
  attachmentUrl?: string | undefined;
105
110
  attachmentTitle?: string | undefined;
106
111
  attachmentType?: 'issue' | 'pr' | 'doc' | 'commit' | 'design' | 'file' | 'url' | 'other' | undefined;
package/dist/kanban.js CHANGED
@@ -860,6 +860,18 @@ var KANBAN_INPUT_SCHEMA = {
860
860
  },
861
861
  transitionAction: { type: "string" },
862
862
  transitionComment: { type: "string" },
863
+ tickChecks: {
864
+ type: "array",
865
+ items: {
866
+ type: "object",
867
+ properties: {
868
+ checkId: { type: "string" },
869
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
870
+ },
871
+ required: ["checkId", "checkStatus"]
872
+ },
873
+ description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
874
+ },
863
875
  attachmentUrl: { type: "string" },
864
876
  attachmentTitle: { type: "string" },
865
877
  attachmentType: {
@@ -1115,6 +1127,9 @@ var kanbanTool = {
1115
1127
  description: KANBAN_TOOL_DESCRIPTION,
1116
1128
  usageHint: KANBAN_TOOL_USAGE_HINT,
1117
1129
  permission: "confirm",
1130
+ // WS-046: gives permission decisions something to key on.
1131
+ // The action performed; kanban has no single file or path subject.
1132
+ subjectKey: "action",
1118
1133
  mutating: true,
1119
1134
  capabilities: ["fs.write"],
1120
1135
  icon: "task",
@@ -1556,6 +1571,7 @@ var kanbanTool = {
1556
1571
  actor: input.author,
1557
1572
  comment: input.transitionComment,
1558
1573
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1574
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1559
1575
  ...input.attachmentUrl !== void 0 ? {
1560
1576
  attachment: {
1561
1577
  url: input.attachmentUrl,
@@ -1948,8 +1964,52 @@ var kanbanTool = {
1948
1964
  } catch (err) {
1949
1965
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
1950
1966
  }
1967
+ },
1968
+ serialize(output, input) {
1969
+ return serializeKanbanOutput(output, input);
1951
1970
  }
1952
1971
  };
1972
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
1973
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
1974
+ "get_board",
1975
+ "export_markdown",
1976
+ "export_task_graph"
1977
+ ]);
1978
+ function serializeKanbanOutput(output, input) {
1979
+ const action = input && typeof input === "object" ? input.action : void 0;
1980
+ const board = output.board;
1981
+ if (board) {
1982
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
1983
+ let boardBytes = 0;
1984
+ if (!keepFull) {
1985
+ try {
1986
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
1987
+ } catch {
1988
+ boardBytes = 0;
1989
+ }
1990
+ }
1991
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
1992
+ const columns = {};
1993
+ for (const column of board.columns) {
1994
+ columns[column.title || column.id] = board.tasks.filter(
1995
+ (task) => task.columnId === column.id
1996
+ ).length;
1997
+ }
1998
+ const compact = {
1999
+ ...output,
2000
+ board: {
2001
+ id: board.id,
2002
+ title: board.title,
2003
+ columns,
2004
+ totalTasks: board.tasks.length,
2005
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2006
+ }
2007
+ };
2008
+ return JSON.stringify(compact, null, 2);
2009
+ }
2010
+ }
2011
+ return JSON.stringify(output, null, 2);
2012
+ }
1953
2013
  export {
1954
2014
  kanbanTool
1955
2015
  };
@@ -2492,8 +2492,11 @@ var SENSITIVE_FLAG_PATTERNS = [
2492
2492
  /--(?: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,
2493
2493
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
2494
2494
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
2495
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
2496
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
2497
+ // redacted, not just the first.
2495
2498
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
2496
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
2499
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
2497
2500
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
2498
2501
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
2499
2502
  // redaction function. Synced with core copy.
@@ -2501,8 +2504,9 @@ var SENSITIVE_FLAG_PATTERNS = [
2501
2504
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
2502
2505
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
2503
2506
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
2504
- // when preceded by a flag name (e.g. --github-token=EyJ...).
2505
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
2507
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
2508
+ // every such flag in the command line is redacted, not just the first.
2509
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
2506
2510
  ];
2507
2511
  function redactCommand(cmd) {
2508
2512
  let result = cmd;
@@ -2591,11 +2595,15 @@ var ProcessRegistryImpl = class {
2591
2595
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
2592
2596
  }
2593
2597
  _canSignalProcessGroup(p) {
2594
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
2598
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
2595
2599
  }
2596
2600
  _killChildDirect(p, signal) {
2597
2601
  try {
2598
- p.child.kill(signal);
2602
+ if (p.child) {
2603
+ p.child.kill(signal);
2604
+ return;
2605
+ }
2606
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
2599
2607
  } catch {
2600
2608
  }
2601
2609
  }
@@ -2793,15 +2801,15 @@ var ProcessRegistryImpl = class {
2793
2801
  this._pruneStale(pid);
2794
2802
  const p = this.processes.get(pid);
2795
2803
  if (!p) return false;
2796
- if (p.killed) return true;
2804
+ if (p.killed && opts.force !== true) return true;
2797
2805
  if (p.protected && opts.includeProtected !== true) return false;
2798
2806
  if (opts.preserveBackground && p.background) return false;
2799
2807
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
2800
2808
  const isWin2 = os.platform() === "win32";
2801
2809
  if (isWin2) {
2802
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
2810
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
2803
2811
  const directFallback = () => {
2804
- if (p.child.exitCode === null) {
2812
+ if (p.child && p.child.exitCode === null) {
2805
2813
  try {
2806
2814
  p.child.kill("SIGKILL");
2807
2815
  } catch {
@@ -2813,10 +2821,7 @@ var ProcessRegistryImpl = class {
2813
2821
  onSettled: directFallback
2814
2822
  })) {
2815
2823
  } else {
2816
- try {
2817
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
2818
- } catch {
2819
- }
2824
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
2820
2825
  }
2821
2826
  p.killed = true;
2822
2827
  return true;
@@ -2827,7 +2832,7 @@ var ProcessRegistryImpl = class {
2827
2832
  } else {
2828
2833
  this._killPosix(p, "SIGTERM");
2829
2834
  const timer = setTimeout(() => {
2830
- if (this.processes.has(pid) && !p.child.killed) {
2835
+ if (this.processes.has(pid) && !p.child?.killed) {
2831
2836
  this._killPosix(p, "SIGKILL");
2832
2837
  }
2833
2838
  }, graceMs);
@@ -2882,6 +2887,16 @@ var ProcessRegistryImpl = class {
2882
2887
  * before reusing a PID, but we want to clean up before that becomes a risk.
2883
2888
  */
2884
2889
  _isStaleEntry(entry) {
2890
+ if (entry.child === null) {
2891
+ if (Date.now() - entry.startedAt <= 6e4) return false;
2892
+ if (os.platform() === "win32") return false;
2893
+ try {
2894
+ process.kill(entry.pid, 0);
2895
+ return false;
2896
+ } catch (err) {
2897
+ return err.code !== "EPERM";
2898
+ }
2899
+ }
2885
2900
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
2886
2901
  }
2887
2902
  /**
package/dist/lint.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
  /**
package/dist/logs.d.ts CHANGED
@@ -3,7 +3,6 @@ interface LogsInput {
3
3
  service?: string | undefined;
4
4
  path?: string | undefined;
5
5
  lines?: number | undefined;
6
- stream?: boolean | undefined;
7
6
  filter?: string | undefined;
8
7
  since?: '1h' | '6h' | '24h' | 'all' | undefined;
9
8
  cwd?: string | undefined;
package/dist/logs.js CHANGED
@@ -126,6 +126,7 @@ function compileUserRegex(pattern, flags) {
126
126
  var MAX_SUBJECT_LEN = 64 * 1024;
127
127
 
128
128
  // src/_util.ts
129
+ import * as fsp from "node:fs/promises";
129
130
  import * as path from "node:path";
130
131
  import * as Core from "@wrongstack/core/utils";
131
132
  function resolvePath(input, ctx) {
@@ -149,16 +150,50 @@ function ensureInsideRoot(absPath, ctx) {
149
150
  function safeResolve(input, ctx) {
150
151
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
151
152
  }
153
+ async function resolveRealInsideRoot(absPath, ctx) {
154
+ if (ctx.allowOutsideProjectRoot) return absPath;
155
+ const realRoots = await Promise.all(
156
+ allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
157
+ );
158
+ let probe = absPath;
159
+ const pendingTail = [];
160
+ for (; ; ) {
161
+ let real;
162
+ try {
163
+ real = await fsp.realpath(probe);
164
+ } catch (err) {
165
+ if (err.code === "ENOENT") {
166
+ const parent = path.dirname(probe);
167
+ if (parent === probe) return absPath;
168
+ pendingTail.unshift(path.basename(probe));
169
+ probe = parent;
170
+ continue;
171
+ }
172
+ throw err;
173
+ }
174
+ if (isInsideAny(real, realRoots)) {
175
+ return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
176
+ }
177
+ throw new Error(
178
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
179
+ );
180
+ }
181
+ }
182
+ async function safeResolveReal(input, ctx) {
183
+ const abs = safeResolve(input, ctx);
184
+ return await resolveRealInsideRoot(abs, ctx);
185
+ }
152
186
 
153
187
  // src/logs.ts
154
188
  var logsTool = {
155
189
  name: "logs",
156
190
  category: "Logs",
157
- description: "Read or stream logs from files, Docker containers, or systemd services. Useful for debugging running applications.",
158
- usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers/systemd.\n- `stream: true` = live tail (can be expensive).\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- Long-running streams should be avoided unless the user explicitly wants live logs.",
191
+ description: "Read logs from files or Docker containers. Useful for debugging running applications.",
192
+ usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for Docker containers.\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- `since` narrows Docker logs to a recent window.",
159
193
  permission: "confirm",
160
194
  mutating: false,
161
195
  timeoutMs: 3e4,
196
+ maxOutputBytes: 262144,
162
197
  capabilities: ["shell.restricted"],
163
198
  icon: "logs",
164
199
  inputSchema: {
@@ -166,7 +201,7 @@ var logsTool = {
166
201
  properties: {
167
202
  service: {
168
203
  type: "string",
169
- description: "Service name for Docker or systemd journal"
204
+ description: "Docker container name (passed to `docker logs`)"
170
205
  },
171
206
  path: {
172
207
  type: "string",
@@ -178,10 +213,6 @@ var logsTool = {
178
213
  minimum: 0,
179
214
  maximum: 1e4
180
215
  },
181
- stream: {
182
- type: "boolean",
183
- description: "Stream logs continuously (like tail -f) (default: false)"
184
- },
185
216
  filter: {
186
217
  type: "string",
187
218
  description: "Regex pattern to filter log lines"
@@ -189,7 +220,7 @@ var logsTool = {
189
220
  since: {
190
221
  type: "string",
191
222
  enum: ["1h", "6h", "24h", "all"],
192
- description: "Only show logs since duration"
223
+ description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
193
224
  },
194
225
  cwd: { type: "string", description: "Working directory (default: cwd)" }
195
226
  }
@@ -206,10 +237,10 @@ var logsTool = {
206
237
  filterRe = compiled.regex;
207
238
  }
208
239
  if (input.service) {
209
- return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
240
+ return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
210
241
  }
211
242
  if (input.path) {
212
- return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);
243
+ return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
213
244
  }
214
245
  return {
215
246
  source: "none",
@@ -223,7 +254,7 @@ var logsTool = {
223
254
  async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
224
255
  const args = ["logs"];
225
256
  if (lines > 0) args.push("--tail", String(lines));
226
- if (since) {
257
+ if (since && since !== "all") {
227
258
  const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
228
259
  args.push("--since", sinceMap[since] ?? "1h");
229
260
  }
@@ -288,7 +319,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
288
319
  }
289
320
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
290
321
  var MAX_TAIL_LINES = 1e5;
291
- async function fileLogs(path2, lines, filterRe, stream) {
322
+ async function fileLogs(path2, lines, filterRe) {
292
323
  const { createInterface } = await import("node:readline");
293
324
  const { createReadStream } = await import("node:fs");
294
325
  const entries = [];
@@ -322,7 +353,7 @@ async function fileLogs(path2, lines, filterRe, stream) {
322
353
  entries,
323
354
  total: entries.length,
324
355
  truncated: totalLines > effLines,
325
- stream_mode: stream
356
+ stream_mode: false
326
357
  };
327
358
  }
328
359
  function parseLogLines(output, filterRe) {