@wrongstack/tools 0.289.0 → 0.291.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/auto-proceed-loop-guard.d.ts +128 -0
  2. package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
  3. package/dist/auto-proceed-loop-guard.js +46 -0
  4. package/dist/auto-proceed-loop-guard.js.map +7 -0
  5. package/dist/bash-kill-guard.d.ts +10 -1
  6. package/dist/bash-kill-guard.d.ts.map +1 -1
  7. package/dist/bash.js +140 -14
  8. package/dist/bash.js.map +2 -2
  9. package/dist/builtin.js +662 -203
  10. package/dist/builtin.js.map +4 -4
  11. package/dist/codebase-index/background-indexer.d.ts +1 -1
  12. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  13. package/dist/codebase-index/index.js +181 -104
  14. package/dist/codebase-index/index.js.map +3 -3
  15. package/dist/codebase-index/indexer.d.ts.map +1 -1
  16. package/dist/codebase-index/refs-extractor.d.ts +2 -17
  17. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  18. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  19. package/dist/codebase-index/worker.js +164 -96
  20. package/dist/codebase-index/worker.js.map +3 -3
  21. package/dist/codebase-index/writer.d.ts +33 -0
  22. package/dist/codebase-index/writer.d.ts.map +1 -1
  23. package/dist/exec-kill-guard.d.ts +29 -0
  24. package/dist/exec-kill-guard.d.ts.map +1 -0
  25. package/dist/exec.d.ts.map +1 -1
  26. package/dist/exec.js +670 -10
  27. package/dist/exec.js.map +4 -4
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +765 -237
  31. package/dist/index.js.map +4 -4
  32. package/dist/kanban.d.ts +10 -0
  33. package/dist/kanban.d.ts.map +1 -1
  34. package/dist/kanban.js +34 -17
  35. package/dist/kanban.js.map +2 -2
  36. package/dist/pack.js +662 -203
  37. package/dist/pack.js.map +4 -4
  38. package/dist/plan.js.map +2 -2
  39. package/dist/read.d.ts.map +1 -1
  40. package/dist/read.js.map +2 -2
  41. package/dist/session-kanban.d.ts.map +1 -1
  42. package/dist/session-kanban.js +60 -3
  43. package/dist/session-kanban.js.map +2 -2
  44. package/dist/task.js.map +2 -2
  45. package/dist/todo.js.map +2 -2
  46. package/package.json +9 -5
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Auto-proceed loop guard.
3
+ *
4
+ * ## Problem
5
+ *
6
+ * The REPL and TUI both implement an "auto" autonomy mode that, after every
7
+ * completed agent turn, feeds the next suggestion (or top auto="true" item)
8
+ * back as a fresh prompt. When the agent emits the same `<nextsteps>` block
9
+ * on every reply — which happens whenever the model's output is stable but
10
+ * autonomy treats the next step as "still actionable" — the loop self-feeds
11
+ * the same instruction 2–3 times in a row before the longer
12
+ * `autoProceedMaxIterations` cap (default 50) trips.
13
+ *
14
+ * The user reports the visible symptom: the same response, including the same
15
+ * `<nextsteps>` block, repeats. Manual input from the user is the only thing
16
+ * that breaks out. They suspect the next-steps mechanism is the source.
17
+ *
18
+ * The 50-iteration cap is intentionally loose — it is a runaway safety net,
19
+ * not a UX signal. By the time it trips, the user has already watched the
20
+ * same response 50 times.
21
+ *
22
+ * ## Approach
23
+ *
24
+ * A small, pure, browser-safe stateful helper that records the last few
25
+ * prompts fed through the auto-proceed / auto-submit path. It normalizes
26
+ * prompts (whitespace, casing) so trivial rewordings are not collapsed, but
27
+ * identical re-feeds are. When the same normalized prompt is fed
28
+ * `repeatThreshold` (default 2) times in a row, the guard returns
29
+ * `{ shouldHalt: true }`.
30
+ *
31
+ * Callers are expected to:
32
+ * 1. Call `record(prompt)` immediately before each auto-feed.
33
+ * 2. If `shouldHalt` is true, clear the suggestion + auto-suggestion store,
34
+ * cancel any pending countdown, and surface a "we detected a loop"
35
+ * message asking the user what's happening. Do NOT feed the prompt.
36
+ * 3. Call `reset()` whenever the user types anything manually (REPL manual
37
+ * input or any user-driven submit) so the next auto-feed starts clean.
38
+ *
39
+ * The default `repeatThreshold` is 2 — i.e. the second consecutive identical
40
+ * feed (the first re-feed of an already-seen prompt) halts the loop. That
41
+ * matches the user's report ("receiving the same prompt 2-3 times in a row
42
+ * causes the system to enter a loop"). Setting it to 2 catches the loop on
43
+ * the first identical re-feed rather than after the 50-iteration runaway cap trips.
44
+ *
45
+ * The window size is intentionally small (default 3). The loop we care about
46
+ * is the immediate repetition — a longer history would misfire on legitimate
47
+ * "do X, then do Y, then do X" sequences the model occasionally returns to.
48
+ *
49
+ * This module is BROWSER-SAFE — no Node-only imports — so it can be imported
50
+ * from Vite-bundled WebUI as well as Node-based CLI/TUI.
51
+ */
52
+ /**
53
+ * Normalize a prompt for repetition comparison. Whitespace is collapsed and
54
+ * trimmed; case is folded to lowercase. Nothing semantic is removed, so two
55
+ * prompts that differ in any meaningful way (extra word, different code,
56
+ * different file path) compare as distinct. Two prompts that differ only by
57
+ * leading/trailing whitespace and casing are treated as identical.
58
+ */
59
+ export declare function normalizeForRepetition(prompt: string): string;
60
+ export interface LoopGuardOptions {
61
+ /**
62
+ * How many consecutive identical prompts (after normalization) trigger a
63
+ * halt. Default 2 — i.e. the second consecutive identical feed (the first
64
+ * re-feed of an already-seen prompt) halts the loop. Clamped to >= 2.
65
+ */
66
+ repeatThreshold?: number;
67
+ /**
68
+ * How many of the most recent feeds to retain for comparison. The window
69
+ * is searched from newest to oldest; the guard cares about the run of
70
+ * identical entries ending at the most recent one, not about identical
71
+ * entries anywhere in history. Default 3.
72
+ */
73
+ windowSize?: number;
74
+ }
75
+ export interface RepetitionSignal {
76
+ /** The normalized prompt that just got recorded. */
77
+ normalized: string;
78
+ /**
79
+ * Number of consecutive identical feeds ending at the most recent feed,
80
+ * including this one. Always >= 1.
81
+ */
82
+ runLength: number;
83
+ /**
84
+ * True when the run length has crossed `repeatThreshold`. Callers must
85
+ * stop feeding and surface a user prompt instead.
86
+ */
87
+ shouldHalt: boolean;
88
+ }
89
+ export interface AutoProceedLoopGuard {
90
+ /**
91
+ * Record a prompt that is about to be auto-fed. Returns the repetition
92
+ * signal for that prompt. When `shouldHalt` is true the caller MUST NOT
93
+ * feed the prompt — it has been recorded for post-mortem inspection, but
94
+ * the loop must be broken instead.
95
+ */
96
+ record(prompt: string): RepetitionSignal;
97
+ /**
98
+ * Drop the history. Call this on any manual user input so a fresh run
99
+ * starts with no memory of the prior cycle.
100
+ */
101
+ reset(): void;
102
+ /**
103
+ * Read-only view of the most recent normalized prompts (newest last).
104
+ * Useful for diagnostics and for the message shown to the user when the
105
+ * guard halts the loop.
106
+ */
107
+ history(): readonly string[];
108
+ /**
109
+ * How many entries the guard is currently retaining.
110
+ */
111
+ size(): number;
112
+ }
113
+ /**
114
+ * Build a fresh loop guard. Defaults: `repeatThreshold = 2`, `windowSize = 3`.
115
+ *
116
+ * @example
117
+ * const guard = createAutoProceedLoopGuard();
118
+ * for (const prompt of candidates) {
119
+ * const signal = guard.record(prompt);
120
+ * if (signal.shouldHalt) {
121
+ * haltAutoProceedAndAskUser(signal);
122
+ * break;
123
+ * }
124
+ * feedPrompt(prompt);
125
+ * }
126
+ */
127
+ export declare function createAutoProceedLoopGuard(options?: LoopGuardOptions): AutoProceedLoopGuard;
128
+ //# sourceMappingURL=auto-proceed-loop-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-proceed-loop-guard.d.ts","sourceRoot":"","sources":["../src/auto-proceed-loop-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACzC;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAC;IACd;;;;OAIG;IACH,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC;IAC7B;;OAEG;IACH,IAAI,IAAI,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,gBAAqB,GAAG,oBAAoB,CAmD/F"}
@@ -0,0 +1,46 @@
1
+ // src/auto-proceed-loop-guard.ts
2
+ function normalizeForRepetition(prompt) {
3
+ return prompt.replace(/\s+/g, " ").trim().toLowerCase();
4
+ }
5
+ function createAutoProceedLoopGuard(options = {}) {
6
+ const safeIntegerOption = (value, fallback) => {
7
+ if (value === void 0) return fallback;
8
+ if (!Number.isFinite(value)) return fallback;
9
+ return Math.floor(value);
10
+ };
11
+ const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));
12
+ const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));
13
+ let buffer = [];
14
+ function record(prompt) {
15
+ const normalized = normalizeForRepetition(prompt);
16
+ buffer.push(normalized);
17
+ if (buffer.length > windowSize) {
18
+ buffer = buffer.slice(buffer.length - windowSize);
19
+ }
20
+ let runLength = 0;
21
+ for (let i = buffer.length - 1; i >= 0; i--) {
22
+ if (buffer[i] === normalized) runLength++;
23
+ else break;
24
+ }
25
+ return {
26
+ normalized,
27
+ runLength,
28
+ shouldHalt: runLength >= repeatThreshold
29
+ };
30
+ }
31
+ function reset() {
32
+ buffer = [];
33
+ }
34
+ function history() {
35
+ return buffer.slice();
36
+ }
37
+ function size() {
38
+ return buffer.length;
39
+ }
40
+ return { record, reset, history, size };
41
+ }
42
+ export {
43
+ createAutoProceedLoopGuard,
44
+ normalizeForRepetition
45
+ };
46
+ //# sourceMappingURL=auto-proceed-loop-guard.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/auto-proceed-loop-guard.ts"],
4
+ "sourcesContent": ["/**\n * Auto-proceed loop guard.\n *\n * ## Problem\n *\n * The REPL and TUI both implement an \"auto\" autonomy mode that, after every\n * completed agent turn, feeds the next suggestion (or top auto=\"true\" item)\n * back as a fresh prompt. When the agent emits the same `<nextsteps>` block\n * on every reply \u2014 which happens whenever the model's output is stable but\n * autonomy treats the next step as \"still actionable\" \u2014 the loop self-feeds\n * the same instruction 2\u20133 times in a row before the longer\n * `autoProceedMaxIterations` cap (default 50) trips.\n *\n * The user reports the visible symptom: the same response, including the same\n * `<nextsteps>` block, repeats. Manual input from the user is the only thing\n * that breaks out. They suspect the next-steps mechanism is the source.\n *\n * The 50-iteration cap is intentionally loose \u2014 it is a runaway safety net,\n * not a UX signal. By the time it trips, the user has already watched the\n * same response 50 times.\n *\n * ## Approach\n *\n * A small, pure, browser-safe stateful helper that records the last few\n * prompts fed through the auto-proceed / auto-submit path. It normalizes\n * prompts (whitespace, casing) so trivial rewordings are not collapsed, but\n * identical re-feeds are. When the same normalized prompt is fed\n * `repeatThreshold` (default 2) times in a row, the guard returns\n * `{ shouldHalt: true }`.\n *\n * Callers are expected to:\n * 1. Call `record(prompt)` immediately before each auto-feed.\n * 2. If `shouldHalt` is true, clear the suggestion + auto-suggestion store,\n * cancel any pending countdown, and surface a \"we detected a loop\"\n * message asking the user what's happening. Do NOT feed the prompt.\n * 3. Call `reset()` whenever the user types anything manually (REPL manual\n * input or any user-driven submit) so the next auto-feed starts clean.\n *\n * The default `repeatThreshold` is 2 \u2014 i.e. the second consecutive identical\n * feed (the first re-feed of an already-seen prompt) halts the loop. That\n * matches the user's report (\"receiving the same prompt 2-3 times in a row\n * causes the system to enter a loop\"). Setting it to 2 catches the loop on\n * the first identical re-feed rather than after the 50-iteration runaway cap trips.\n *\n * The window size is intentionally small (default 3). The loop we care about\n * is the immediate repetition \u2014 a longer history would misfire on legitimate\n * \"do X, then do Y, then do X\" sequences the model occasionally returns to.\n *\n * This module is BROWSER-SAFE \u2014 no Node-only imports \u2014 so it can be imported\n * from Vite-bundled WebUI as well as Node-based CLI/TUI.\n */\n\n/**\n * Normalize a prompt for repetition comparison. Whitespace is collapsed and\n * trimmed; case is folded to lowercase. Nothing semantic is removed, so two\n * prompts that differ in any meaningful way (extra word, different code,\n * different file path) compare as distinct. Two prompts that differ only by\n * leading/trailing whitespace and casing are treated as identical.\n */\nexport function normalizeForRepetition(prompt: string): string {\n return prompt.replace(/\\s+/g, ' ').trim().toLowerCase();\n}\n\nexport interface LoopGuardOptions {\n /**\n * How many consecutive identical prompts (after normalization) trigger a\n * halt. Default 2 \u2014 i.e. the second consecutive identical feed (the first\n * re-feed of an already-seen prompt) halts the loop. Clamped to >= 2.\n */\n repeatThreshold?: number;\n /**\n * How many of the most recent feeds to retain for comparison. The window\n * is searched from newest to oldest; the guard cares about the run of\n * identical entries ending at the most recent one, not about identical\n * entries anywhere in history. Default 3.\n */\n windowSize?: number;\n}\n\nexport interface RepetitionSignal {\n /** The normalized prompt that just got recorded. */\n normalized: string;\n /**\n * Number of consecutive identical feeds ending at the most recent feed,\n * including this one. Always >= 1.\n */\n runLength: number;\n /**\n * True when the run length has crossed `repeatThreshold`. Callers must\n * stop feeding and surface a user prompt instead.\n */\n shouldHalt: boolean;\n}\n\nexport interface AutoProceedLoopGuard {\n /**\n * Record a prompt that is about to be auto-fed. Returns the repetition\n * signal for that prompt. When `shouldHalt` is true the caller MUST NOT\n * feed the prompt \u2014 it has been recorded for post-mortem inspection, but\n * the loop must be broken instead.\n */\n record(prompt: string): RepetitionSignal;\n /**\n * Drop the history. Call this on any manual user input so a fresh run\n * starts with no memory of the prior cycle.\n */\n reset(): void;\n /**\n * Read-only view of the most recent normalized prompts (newest last).\n * Useful for diagnostics and for the message shown to the user when the\n * guard halts the loop.\n */\n history(): readonly string[];\n /**\n * How many entries the guard is currently retaining.\n */\n size(): number;\n}\n\n/**\n * Build a fresh loop guard. Defaults: `repeatThreshold = 2`, `windowSize = 3`.\n *\n * @example\n * const guard = createAutoProceedLoopGuard();\n * for (const prompt of candidates) {\n * const signal = guard.record(prompt);\n * if (signal.shouldHalt) {\n * haltAutoProceedAndAskUser(signal);\n * break;\n * }\n * feedPrompt(prompt);\n * }\n */\nexport function createAutoProceedLoopGuard(options: LoopGuardOptions = {}): AutoProceedLoopGuard {\n // Validate options defensively. Non-finite values (NaN, Infinity) MUST\n // fall back to the documented default \u2014 a guard that silently disables\n // itself is worse than no guard at all. Specifically:\n // - `repeatThreshold: NaN` would cause `Math.floor(NaN) = NaN`,\n // `Math.max(2, NaN) = NaN`, and `runLength >= NaN` is always false,\n // making the guard permanent no-op.\n // - `windowSize: NaN` / `Infinity` would skip the trim branch entirely\n // (`buffer.length > NaN` and `buffer.length > Infinity` are both\n // false), so the buffer would grow without bound.\n const safeIntegerOption = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value)) return fallback;\n return Math.floor(value);\n };\n const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));\n const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));\n let buffer: string[] = [];\n\n function record(prompt: string): RepetitionSignal {\n const normalized = normalizeForRepetition(prompt);\n buffer.push(normalized);\n if (buffer.length > windowSize) {\n buffer = buffer.slice(buffer.length - windowSize);\n }\n // Count the trailing run of identical normalized prompts.\n let runLength = 0;\n for (let i = buffer.length - 1; i >= 0; i--) {\n if (buffer[i] === normalized) runLength++;\n else break;\n }\n return {\n normalized,\n runLength,\n shouldHalt: runLength >= repeatThreshold,\n };\n }\n\n function reset(): void {\n buffer = [];\n }\n\n function history(): readonly string[] {\n return buffer.slice();\n }\n\n function size(): number {\n return buffer.length;\n }\n\n return { record, reset, history, size };\n}\n"],
5
+ "mappings": ";AA2DO,SAAS,uBAAuB,QAAwB;AAC7D,SAAO,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,YAAY;AACxD;AAwEO,SAAS,2BAA2B,UAA4B,CAAC,GAAyB;AAU/F,QAAM,oBAAoB,CAAC,OAA2B,aAA6B;AACjF,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AACA,QAAM,kBAAkB,KAAK,IAAI,GAAG,kBAAkB,QAAQ,iBAAiB,CAAC,CAAC;AACjF,QAAM,aAAa,KAAK,IAAI,iBAAiB,kBAAkB,QAAQ,YAAY,CAAC,CAAC;AACrF,MAAI,SAAmB,CAAC;AAExB,WAAS,OAAO,QAAkC;AAChD,UAAM,aAAa,uBAAuB,MAAM;AAChD,WAAO,KAAK,UAAU;AACtB,QAAI,OAAO,SAAS,YAAY;AAC9B,eAAS,OAAO,MAAM,OAAO,SAAS,UAAU;AAAA,IAClD;AAEA,QAAI,YAAY;AAChB,aAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,UAAI,OAAO,CAAC,MAAM,WAAY;AAAA,UACzB;AAAA,IACP;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,YAAY,aAAa;AAAA,IAC3B;AAAA,EACF;AAEA,WAAS,QAAc;AACrB,aAAS,CAAC;AAAA,EACZ;AAEA,WAAS,UAA6B;AACpC,WAAO,OAAO,MAAM;AAAA,EACtB;AAEA,WAAS,OAAe;AACtB,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO,EAAE,QAAQ,OAAO,SAAS,KAAK;AACxC;",
6
+ "names": []
7
+ }
@@ -11,7 +11,14 @@
11
11
  * - Shell -c wrapped: bash -c "kill -9 12345" (any shell path — see P2 #10)
12
12
  * - Full path kills: /bin/kill -9 12345
13
13
  * - Name-based kills: pkill, killall, pgrep
14
- * - Windows equivalents: taskkill
14
+ * - Windows equivalents: taskkill, tskill
15
+ * - PowerShell Stop-Process / kill alias: Stop-Process -Name node, kill -Id 12345
16
+ * - WMIC process termination: wmic process where "name='node.exe'" delete
17
+ * - Script-based kill (script is named kill*.sh, kill*.ps1, kill*.bat)
18
+ *
19
+ * Security contract: every "Handles" bullet must map to both a detector
20
+ * AND a block path in isKillRelatedCommand + parseKillCommand + isKillProtected.
21
+ * Script-based kills are blocked conservatively (can't inspect script content).
15
22
  *
16
23
  * Known bypasses (NOT handled — this is a static regex parser, not a shell):
17
24
  * Static analysis of shell strings is inherently defeatable by obfuscation.
@@ -25,6 +32,8 @@
25
32
  * - String concatenation / quote-splitting: `ki''ll -9 12345`, `k"i"ll 12345`
26
33
  * - Aliases and functions: `alias x=kill; x -9 12345`
27
34
  * - eval / source: `eval "ki""ll -9 12345"`
35
+ * - node -e eval: `node -e "process.kill(12345)"` (handled by exec-kill-guard.ts)
36
+ * - Scripts not named kill/terminate/stop*: `runkill.sh`, `/tmp/cleanup.bat`
28
37
  *
29
38
  * Mitigation: rely on the permission policy (confirm/deny gate) and YOLO
30
39
  * destructive detection as the primary controls; this guard is a best-effort
@@ -1 +1 @@
1
- {"version":3,"file":"bash-kill-guard.d.ts","sourceRoot":"","sources":["../src/bash-kill-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA6FpE;AAqBD;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAqCzE;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CA2CxF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E"}
1
+ {"version":3,"file":"bash-kill-guard.d.ts","sourceRoot":"","sources":["../src/bash-kill-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAqBH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA+ED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAkPpE;AAqBD;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA0CzE;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAqDxF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E"}
package/dist/bash.js CHANGED
@@ -1199,17 +1199,18 @@ function getPersistentProcessRegistry() {
1199
1199
 
1200
1200
  // src/bash-kill-guard.ts
1201
1201
  var isWin = os3.platform() === "win32";
1202
+ var SCRIPT_KILL_RE = /^(?:\.\\|\.\/)?(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)(?:\s|$)/i;
1203
+ var SCRIPT_KILL_RE_POSIX = /^(?:\.\/)?(?:kill|terminate|stop)\S*\.sh(?:\s|$)/i;
1204
+ var SCRIPT_KILL_FALLBACK_RE = /^\S*(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)\b/i;
1202
1205
  function extractKillCommand(command) {
1203
1206
  const normalized = command.replace(/\s+/g, " ").trim();
1204
- const shellCMatch = normalized.match(
1205
- /^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
1206
- );
1207
- if (shellCMatch?.[1]) {
1208
- const inner = shellCMatch[1].trim();
1207
+ const shellCMatch = normalized.match(/^.+?\s+-c\s+(['"])([\s\S]+)\1$/);
1208
+ if (shellCMatch?.[2]) {
1209
+ const inner = shellCMatch[2].trim();
1209
1210
  return isKillRelatedCommand(inner) ? inner : null;
1210
1211
  }
1211
1212
  const shellCUnquoted = normalized.match(
1212
- /^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1213
+ /^.+?\s+-c\s+(kill(?:\s+-s\s+[a-zA-Z0-9]+|\s+-[a-zA-Z0-9]+)?\s+\d+)$/
1213
1214
  );
1214
1215
  if (shellCUnquoted?.[1]) {
1215
1216
  return shellCUnquoted[1];
@@ -1221,22 +1222,47 @@ function isKillRelatedCommand(cmd) {
1221
1222
  if (isWin) {
1222
1223
  if (/^taskkill\s/i.test(normalized)) return true;
1223
1224
  if (/^tskill\s/i.test(normalized)) return true;
1225
+ if (/^(stop-process|kill|stop)\s/i.test(normalized)) return true;
1226
+ if (/^wmic\s+process\s/i.test(normalized) && /\bdelete\b/i.test(normalized)) return true;
1227
+ if (SCRIPT_KILL_RE.test(normalized)) return true;
1224
1228
  return false;
1225
1229
  }
1226
1230
  if (/^kill(\s|$)/.test(normalized)) return true;
1227
1231
  if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
1228
1232
  if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
1233
+ if (SCRIPT_KILL_RE_POSIX.test(normalized)) return true;
1229
1234
  return false;
1230
1235
  }
1231
1236
  function parseKillCommand(command) {
1232
1237
  const normalized = command.replace(/\s+/g, " ").trim();
1233
1238
  if (isWin) {
1234
- const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1235
- if (taskkillMatch?.[1]) {
1236
- const pidStr = taskkillMatch[1];
1239
+ const hasTaskkillForce = /(?:^|\s)\/F(?=\s|$)/i.test(normalized);
1240
+ const isSimpleTaskkill = /^taskkill\s+/i.test(normalized) && !/[|&<>]/.test(normalized);
1241
+ const taskkillPidMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/PID\s+(\d+)(?=\s|$)/i) : null;
1242
+ if (taskkillPidMatch?.[1]) {
1243
+ return {
1244
+ pid: parseInt(taskkillPidMatch[1], 10),
1245
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
1246
+ isGroupKill: false,
1247
+ isAllKill: false,
1248
+ originalCommand: command
1249
+ };
1250
+ }
1251
+ const taskkillImMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/IM\s+([^\s/]+)(?=\s|$)/i) : null;
1252
+ if (taskkillImMatch?.[1]) {
1253
+ return {
1254
+ name: taskkillImMatch[1],
1255
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
1256
+ isGroupKill: false,
1257
+ isAllKill: false,
1258
+ originalCommand: command
1259
+ };
1260
+ }
1261
+ const taskkillFiMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/FI\s+"IMAGENAME\s+eq\s+([^"]+)"(?=\s|$)/i) : null;
1262
+ if (taskkillFiMatch?.[1]) {
1237
1263
  return {
1238
- pid: parseInt(pidStr, 10),
1239
- signal: normalized.includes("/F") ? "FORCE" : "TERM",
1264
+ name: taskkillFiMatch[1],
1265
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
1240
1266
  isGroupKill: false,
1241
1267
  isAllKill: false,
1242
1268
  originalCommand: command
@@ -1244,18 +1270,109 @@ function parseKillCommand(command) {
1244
1270
  }
1245
1271
  const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1246
1272
  if (tskillMatch?.[1]) {
1247
- const pidStr = tskillMatch[1];
1248
1273
  return {
1249
- pid: parseInt(pidStr, 10),
1274
+ pid: parseInt(tskillMatch[1], 10),
1250
1275
  signal: "TERM",
1251
1276
  isGroupKill: false,
1252
1277
  isAllKill: false,
1253
1278
  originalCommand: command
1254
1279
  };
1255
1280
  }
1281
+ const isStopProcIdCommand = /^(?:stop-process|kill)(?:\s+-(?:id|pid)\s+\d+|\s+-[a-zA-Z]+)+$/i.test(normalized);
1282
+ const stopProcIdMatch = normalized.match(/(?:^|\s)-(?:id|pid)\s+(\d+)(?=\s|$)/i);
1283
+ if (isStopProcIdCommand && stopProcIdMatch?.[1]) {
1284
+ return {
1285
+ pid: parseInt(stopProcIdMatch[1], 10),
1286
+ signal: "FORCE",
1287
+ isGroupKill: false,
1288
+ isAllKill: false,
1289
+ originalCommand: command
1290
+ };
1291
+ }
1292
+ const killSignalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+)$/i);
1293
+ if (killSignalOptionMatch?.[1] && killSignalOptionMatch[2]) {
1294
+ return {
1295
+ pid: parseInt(killSignalOptionMatch[2], 10),
1296
+ signal: killSignalOptionMatch[1].toUpperCase(),
1297
+ isGroupKill: false,
1298
+ isAllKill: false,
1299
+ originalCommand: command
1300
+ };
1301
+ }
1302
+ const killPosixMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+)$/);
1303
+ if (killPosixMatch?.[2]) {
1304
+ const sig = killPosixMatch[1] ? killPosixMatch[1].slice(1).toUpperCase() : "TERM";
1305
+ return {
1306
+ pid: parseInt(killPosixMatch[2], 10),
1307
+ signal: sig,
1308
+ isGroupKill: false,
1309
+ isAllKill: false,
1310
+ originalCommand: command
1311
+ };
1312
+ }
1313
+ const stopProcNameMatch = normalized.match(
1314
+ /^(?:stop-process|kill)\s+-(?:name|n)\s+(?:['"]([a-zA-Z0-9_.-]+)['"]|([a-zA-Z0-9_.-]+))(?:\s|$)/i
1315
+ );
1316
+ const stopProcName = stopProcNameMatch?.[1] ?? stopProcNameMatch?.[2];
1317
+ if (stopProcName) {
1318
+ return {
1319
+ name: stopProcName,
1320
+ signal: "FORCE",
1321
+ isGroupKill: false,
1322
+ isAllKill: false,
1323
+ originalCommand: command
1324
+ };
1325
+ }
1326
+ const stopProcStandalone = normalized.match(
1327
+ /^(?:stop-process|kill)\s+['"]?([a-zA-Z][a-zA-Z0-9_.-]+)['"]?$/i
1328
+ );
1329
+ if (stopProcStandalone?.[1]) {
1330
+ return {
1331
+ name: stopProcStandalone[1],
1332
+ signal: "FORCE",
1333
+ isGroupKill: false,
1334
+ isAllKill: false,
1335
+ originalCommand: command
1336
+ };
1337
+ }
1338
+ const wmicMatch = normalized.match(
1339
+ /^wmic\s+process\s+where\s+['"]?(?:name\s*=\s*['"]?)([a-zA-Z0-9_.-]+)/i
1340
+ );
1341
+ if (wmicMatch?.[1]) {
1342
+ return {
1343
+ name: wmicMatch[1],
1344
+ signal: "FORCE",
1345
+ isGroupKill: false,
1346
+ isAllKill: false,
1347
+ originalCommand: command
1348
+ };
1349
+ }
1350
+ const killScriptMatch = normalized.match(SCRIPT_KILL_RE);
1351
+ if (killScriptMatch) {
1352
+ return {
1353
+ name: "kill-script",
1354
+ // sentinel — isKillProtected always blocks "kill-script"
1355
+ signal: "FORCE",
1356
+ isGroupKill: false,
1357
+ isAllKill: false,
1358
+ originalCommand: command
1359
+ };
1360
+ }
1256
1361
  return null;
1257
1362
  }
1258
- const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
1363
+ const signalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+|-?\d+)$/i);
1364
+ if (signalOptionMatch?.[1] && signalOptionMatch[2]) {
1365
+ const pidOrGroup = signalOptionMatch[2];
1366
+ const isGroupKill = pidOrGroup.startsWith("-");
1367
+ return {
1368
+ pid: parseInt(isGroupKill ? pidOrGroup.slice(1) : pidOrGroup, 10),
1369
+ signal: signalOptionMatch[1].toUpperCase(),
1370
+ isGroupKill,
1371
+ isAllKill: false,
1372
+ originalCommand: command
1373
+ };
1374
+ }
1375
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+|-?\d+)$/);
1259
1376
  if (simpleMatch) {
1260
1377
  const signal = simpleMatch[1] ?? "-TERM";
1261
1378
  const pidOrGroup = simpleMatch[2];
@@ -1315,6 +1432,9 @@ async function getProtectedEntries() {
1315
1432
  }
1316
1433
  async function isKillProtected(kill) {
1317
1434
  const registry = getPersistentProcessRegistry();
1435
+ if (kill.name === "kill-script") {
1436
+ return true;
1437
+ }
1318
1438
  if (kill.name) {
1319
1439
  const entries = await getProtectedEntries();
1320
1440
  const killNameLower = kill.name.toLowerCase();
@@ -1354,6 +1474,12 @@ async function checkAndBlockKillCommand(command) {
1354
1474
  reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
1355
1475
  };
1356
1476
  }
1477
+ if (SCRIPT_KILL_FALLBACK_RE.test(killCmd)) {
1478
+ return {
1479
+ blocked: true,
1480
+ reason: `Blocked: script-based kill detected \u2014 "${killCmd.slice(0, 80)}" may target protected WrongStack processes (cannot inspect script body).`
1481
+ };
1482
+ }
1357
1483
  return { blocked: false };
1358
1484
  }
1359
1485
  if (await isKillProtected(parsed)) {