@delt/claude-jev-advisor 0.1.2 → 0.3.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.
package/dist/rm-hook.js CHANGED
@@ -4,19 +4,123 @@
4
4
  import os from "os";
5
5
 
6
6
  // src/rm/guard.ts
7
+ import path2 from "path";
8
+
9
+ // src/rm/targets.ts
7
10
  import fs from "fs";
8
11
  import path from "path";
9
12
  import { spawnSync } from "child_process";
10
13
  var win = path.win32;
14
+ var BUILD_DIRS = /* @__PURE__ */ new Set(["target", "build", "dist", "out", "node_modules", "coverage", "__pycache__", ".pytest_cache", ".gradle", ".next", ".nuxt", ".turbo", ".cache", "bin", "obj"]);
15
+ var MAX_LISTED = 3;
16
+ var emptyRead = () => ({ deny: null, targets: [], failed: [], shells: [] });
17
+ var DELETE_WORDS = /(?<![\w.-])(?:rm|rmdir|del|erase|rd|ri|remove-item)(?![\w-])|(?:\.|::)\s*delete\w*/i;
18
+ function isInside(child, parent) {
19
+ const c = child.toLowerCase();
20
+ const p = parent.toLowerCase().replace(/\\+$/, "");
21
+ return c.startsWith(`${p}\\`);
22
+ }
23
+ function segments(p) {
24
+ return p.toLowerCase().split("\\");
25
+ }
26
+ function judgePath(p, pattern, ctx) {
27
+ const shown = pattern === null ? p : `${p.replace(/\\$/, "")}\\${pattern}`;
28
+ const narrowPattern = pattern !== null && !/^[*?.]+$/.test(pattern);
29
+ if (ctx.tmpdirs.some((t) => isInside(p, t) || narrowPattern && p.toLowerCase() === t.toLowerCase().replace(/\\+$/, ""))) return null;
30
+ if (segments(p).includes(".superpowers")) return null;
31
+ if (pattern === null && !ctx.probe.exists(p)) return null;
32
+ if (segments(p).some((s) => BUILD_DIRS.has(s)) && ctx.probe.isIgnored(p)) return null;
33
+ return { shown, path: pattern === null ? p : null };
34
+ }
35
+ function splitPattern(value, wildcard) {
36
+ const cut = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
37
+ if (wildcard.test(value.slice(0, Math.max(cut, 0)))) return null;
38
+ return { dir: value.slice(0, Math.max(cut, 0)) || ".", pattern: value.slice(cut + 1) };
39
+ }
40
+ function splitWindowsPattern(value, wildcard) {
41
+ const drive = /^([A-Za-z]:)([^\\/]*)$/.exec(value);
42
+ if (drive) return { dir: drive[1], pattern: drive[2] };
43
+ const split = splitPattern(value, wildcard);
44
+ if (split === null) return null;
45
+ const root = /^((?:[A-Za-z]:)?[\\/])[^\\/]*$/.exec(value);
46
+ return root ? { dir: root[1], pattern: split.pattern } : split;
47
+ }
48
+ function resolveWindowsPath(value, cwd, home) {
49
+ if (/^(?:\\\\|\/\/)/.test(value)) return { unc: value };
50
+ if (home !== null && /^~(?:[\\/]|$)/.test(value)) return { path: win.resolve(home, value.slice(2) || ".") };
51
+ if (/^[A-Za-z]:[\\/]/.test(value)) return { path: win.normalize(value) };
52
+ if (/^[A-Za-z]:/.test(value)) return { unresolvable: `a path relative to another drive's folder (${value})` };
53
+ if (cwd === null) return { unresolvable: `a path relative to a folder that cannot be worked out (${value})` };
54
+ return { path: win.resolve(cwd, value) };
55
+ }
56
+ var EXAMPLES = {
57
+ bash: {
58
+ what: "this rm",
59
+ rules: "no shell variables, command substitutions, xargs, brace expansion or wildcards in folder names, and no cd to such a path before it",
60
+ example: 'rm -f "C:/full/path/file.txt"',
61
+ then: "rm the printed paths"
62
+ },
63
+ powershell: {
64
+ what: "this PowerShell command",
65
+ rules: "no variables other than $env:TEMP, $env:TMP, $env:USERPROFILE, $HOME and $PWD, no expressions, script blocks or pipeline input, no wildcards in folder names, and no Set-Location to such a path before it",
66
+ example: 'Remove-Item -LiteralPath "C:\\full\\path\\file.txt"',
67
+ then: "delete the printed paths"
68
+ },
69
+ cmd: {
70
+ what: "this del or rd",
71
+ rules: "no variables other than %TEMP%, %TMP%, %USERPROFILE% and %CD%, no wildcards in folder names, and no cd to such a path before it",
72
+ example: 'del /q "C:\\full\\path\\file.txt"',
73
+ then: "delete the printed paths"
74
+ }
75
+ };
76
+ function denyReason(shell, why) {
77
+ const { what, rules, example, then } = EXAMPLES[shell];
78
+ return `rm-guard could not work out what ${what} would delete (${why}). Rewrite it with literal paths: ${rules}. Example: ${example}. If you need a variable or a listing to find the paths, run that on its own first, then ${then}.`;
79
+ }
80
+ function listed(targets) {
81
+ const names = targets.slice(0, MAX_LISTED).map((t) => t.shown).join(", ");
82
+ return targets.length > MAX_LISTED ? `${names} \uC678 ${targets.length - MAX_LISTED}\uAC1C` : names;
83
+ }
84
+ var UNREAD_TARGET = "(\uBD84\uC11D\uD558\uC9C0 \uBABB\uD55C \uBA85\uB839)";
85
+ function decisionOf({ deny, targets, failed }) {
86
+ if (deny !== null) return { decision: "deny", reason: deny };
87
+ if (failed.length) {
88
+ const note = `\uC0AD\uC81C \uBA85\uB839\uC744 \uBD84\uC11D\uD558\uC9C0 \uBABB\uD568: ${failed[0]}`;
89
+ return { decision: "ask", reason: targets.length ? `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed(targets)} \xB7 ${note}` : note, targets: [...targets, { shown: UNREAD_TARGET, path: null }] };
90
+ }
91
+ if (!targets.length) return null;
92
+ return { decision: "ask", reason: `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed(targets)}`, targets };
93
+ }
94
+ function hookOutput({ decision, reason }) {
95
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision, permissionDecisionReason: reason } });
96
+ }
97
+ var realProbe = {
98
+ exists(p) {
99
+ try {
100
+ fs.lstatSync(p);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ },
106
+ isIgnored(p) {
107
+ const r = spawnSync("git", ["-C", win.dirname(p), "check-ignore", "-q", "--", p], { timeout: 3e3, windowsHide: true });
108
+ return r.status === 0;
109
+ }
110
+ };
111
+
112
+ // src/rm/guard.ts
113
+ var win2 = path2.win32;
11
114
  var SEPARATORS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
12
115
  var SKIP_WORDS = /* @__PURE__ */ new Set(["then", "else", "elif", "do", "while", "until", "if", "!", "{", "time"]);
13
116
  var HEADER_WORDS = /* @__PURE__ */ new Set(["for", "select", "case"]);
14
117
  var CLOSING_WORDS = /* @__PURE__ */ new Set(["done", "fi", "esac", "}"]);
15
- var BUILD_DIRS = /* @__PURE__ */ new Set(["target", "build", "dist", "out", "node_modules", "coverage", "__pycache__", ".pytest_cache", ".gradle", ".next", ".nuxt", ".turbo", ".cache", "bin", "obj"]);
16
- var MAX_LISTED = 3;
17
118
  var ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=/;
18
119
  var DECLARERS = /* @__PURE__ */ new Set(["export", "readonly", "declare", "typeset", "local"]);
19
120
  var KNOWN_ENV = ["TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE"];
121
+ var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "cmd", "powershell", "pwsh"]);
122
+ var NATIVE_SHELLS = /* @__PURE__ */ new Set(["cmd", "powershell", "pwsh"]);
123
+ var SHELL_WORDS = /\b(?:sh|bash|cmd|powershell|pwsh)(?:\.exe)?\b/i;
20
124
  function stripHeredocs(text) {
21
125
  const out = [];
22
126
  const pending = [];
@@ -149,6 +253,7 @@ function tokenize(text) {
149
253
  if (word !== null && /^\d+$/.test(word.value) && !word.dynamic) word = null;
150
254
  else finish();
151
255
  const m = /^(&>>?|\d*[<>]+[&|]?)/.exec(text.slice(i));
256
+ if (/^\d*<<<?$/.test(m[0])) tokens.push({ type: "here" });
152
257
  i += m[0].length;
153
258
  if (/&$/.test(m[0]) && /^\d|^-/.test(text[i] ?? "")) {
154
259
  while (i < text.length && /[\d-]/.test(text[i])) i++;
@@ -177,13 +282,16 @@ function tokenize(text) {
177
282
  function splitCommands(tokens) {
178
283
  const commands = [];
179
284
  let current = [];
285
+ let here = false;
180
286
  for (const t of tokens) {
181
287
  if (t.type === "op" && SEPARATORS.has(t.value)) {
182
- commands.push({ words: current, after: t.value });
288
+ commands.push({ words: current, after: t.value, here });
183
289
  current = [];
290
+ here = false;
184
291
  } else if (t.type === "word") current.push(t);
292
+ else if (t.type === "here") here = true;
185
293
  }
186
- commands.push({ words: current, after: null });
294
+ commands.push({ words: current, after: null, here });
187
295
  return commands;
188
296
  }
189
297
  function expand(word, vars) {
@@ -200,31 +308,31 @@ function expand(word, vars) {
200
308
  return known && !/[$`]/.test(out) ? out : null;
201
309
  }
202
310
  function assign(word, vars, home) {
203
- const name = ASSIGNMENT.exec(word.value)[1];
204
- let value = expand({ value: word.value.slice(name.length + 1), dynamic: word.dynamic }, vars);
311
+ const name2 = ASSIGNMENT.exec(word.value)[1];
312
+ let value = expand({ value: word.value.slice(name2.length + 1), dynamic: word.dynamic }, vars);
205
313
  if (value !== null && /^~(?:[\\/]|$)/.test(value)) value = home + value.slice(1);
206
- vars.set(name, value);
314
+ vars.set(name2, value);
207
315
  }
208
316
  function commandName(word) {
209
317
  if (word.dynamic) return null;
210
- return win.basename(word.value.replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
318
+ return win2.basename(word.value.replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
211
319
  }
212
320
  function stripPrefixes(words) {
213
321
  let rest = words;
214
322
  for (; ; ) {
215
323
  while (rest.length && ASSIGNMENT.test(rest[0].value)) rest = rest.slice(1);
216
324
  if (!rest.length) return rest;
217
- const name = commandName(rest[0]);
218
- if (SKIP_WORDS.has(rest[0].value) || name === "command" || name === "builtin" || name === "exec" || name === "nohup") {
325
+ const name2 = commandName(rest[0]);
326
+ if (SKIP_WORDS.has(rest[0].value) || name2 === "command" || name2 === "builtin" || name2 === "exec" || name2 === "nohup") {
219
327
  rest = rest.slice(1);
220
328
  continue;
221
329
  }
222
- if (name === "sudo" || name === "env") {
330
+ if (name2 === "sudo" || name2 === "env") {
223
331
  rest = rest.slice(1);
224
332
  while (rest.length && !rest[0].dynamic && rest[0].value.startsWith("-")) rest = rest.slice(1);
225
333
  continue;
226
334
  }
227
- if (name === "timeout") {
335
+ if (name2 === "timeout") {
228
336
  rest = rest.slice(1);
229
337
  while (rest.length && !rest[0].dynamic && rest[0].value.startsWith("-")) rest = rest.slice(1);
230
338
  rest = rest.slice(1);
@@ -235,23 +343,24 @@ function stripPrefixes(words) {
235
343
  }
236
344
  function toWindowsPath(word, cwd, home, tmpdir) {
237
345
  const v = word.value;
238
- if (word.tilde && (v === "~" || v.startsWith("~/") || v.startsWith("~\\"))) return { path: win.resolve(home, v.slice(2) || ".") };
239
- if (/^[A-Za-z]:[\\/]/.test(v)) return { path: win.normalize(v) };
346
+ if (word.tilde && (v === "~" || v.startsWith("~/") || v.startsWith("~\\"))) return { path: win2.resolve(home, v.slice(2) || ".") };
347
+ if (/^[A-Za-z]:[\\/]/.test(v)) return { path: win2.normalize(v) };
240
348
  const drive = /^\/([A-Za-z])(?:\/(.*))?$/.exec(v);
241
- if (drive) return { path: win.normalize(`${drive[1].toUpperCase()}:\\${drive[2] ?? ""}`) };
349
+ if (drive) return { path: win2.normalize(`${drive[1].toUpperCase()}:\\${drive[2] ?? ""}`) };
242
350
  const tmp = /^\/tmp(?:\/(.*))?$/.exec(v);
243
- if (tmp) return { path: win.resolve(tmpdir, tmp[1] ?? ".") };
351
+ if (tmp) return { path: win2.resolve(tmpdir, tmp[1] ?? ".") };
244
352
  if (v.startsWith("/")) return { unknown: v };
245
353
  if (cwd === null) return { unresolvable: "a relative path after cd to a path that cannot be worked out" };
246
- return { path: win.resolve(cwd, v) };
354
+ return { path: win2.resolve(cwd, v) };
247
355
  }
248
- function isInside(child, parent) {
249
- const c = child.toLowerCase();
250
- const p = parent.toLowerCase().replace(/\\+$/, "");
251
- return c.startsWith(`${p}\\`);
252
- }
253
- function segments(p) {
254
- return p.toLowerCase().split("\\");
356
+ function nativeArg(value, tmpdir) {
357
+ if (value === null || !value.startsWith("/") || value === "/?") return value;
358
+ if (value.startsWith("//")) return /^\/\/[^/]*$/.test(value) ? value.slice(1) : value;
359
+ const drive = /^\/([A-Za-z])(\/.*)?$/.exec(value);
360
+ if (drive) return `${drive[1].toUpperCase()}:${drive[2] ?? "/"}`;
361
+ const tmp = /^\/tmp(\/.*)?$/.exec(value);
362
+ if (tmp) return `${tmpdir}${(tmp[1] ?? "").replace(/\//g, "\\")}`;
363
+ return null;
255
364
  }
256
365
  function classify(target, ctx) {
257
366
  if (target.brace) return { unresolvable: `brace expansion (${target.value})` };
@@ -260,31 +369,19 @@ function classify(target, ctx) {
260
369
  let w = { ...target, value, dynamic: false };
261
370
  let pattern = null;
262
371
  if (target.glob) {
263
- const parts = value.split(/[\\/]/);
264
- if (parts.slice(0, -1).some((s) => /[*?[]/.test(s))) return { unresolvable: `a wildcard in a folder name (${target.value})` };
265
- pattern = parts[parts.length - 1];
266
- w = { ...w, value: parts.slice(0, -1).join("/") || "." };
372
+ const split = splitWindowsPattern(value, /[*?[]/);
373
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${target.value})` };
374
+ pattern = split.pattern;
375
+ w = { ...w, value: split.dir };
267
376
  }
268
377
  const resolved = toWindowsPath(w, ctx.cwd, ctx.home, ctx.tmpdirs[0]);
269
378
  if (resolved.unresolvable) return { unresolvable: resolved.unresolvable };
270
- if (resolved.unknown) return { real: target.value };
271
- const p = resolved.path;
272
- const shown = pattern === null ? p : `${p}\\${pattern}`;
273
- const narrowPattern = pattern !== null && !/^[*?.]+$/.test(pattern);
274
- if (ctx.tmpdirs.some((t) => isInside(p, t) || narrowPattern && p.toLowerCase() === t.toLowerCase().replace(/\\+$/, ""))) return {};
275
- if (segments(p).includes(".superpowers")) return {};
276
- if (pattern === null && !ctx.probe.exists(p)) return {};
277
- if (segments(p).some((s) => BUILD_DIRS.has(s)) && ctx.probe.isIgnored(p)) return {};
278
- return { real: shown };
379
+ if (resolved.unknown) return { real: { shown: target.value, path: null } };
380
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
279
381
  }
280
- function deny(why) {
281
- return {
282
- decision: "deny",
283
- reason: `rm-guard could not work out what this rm would delete (${why}). Rewrite it with literal paths: no shell variables, command substitutions, xargs, brace expansion or wildcards in folder names, and no cd to such a path before it. Example: rm -f "C:/full/path/file.txt". If you need a variable or a listing to find the paths, run that on its own first, then rm the printed paths.`
284
- };
285
- }
286
- function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
287
- if (!/\b(?:rm|rmdir|xargs)\b/.test(command)) return null;
382
+ function readBash(command, { cwd, home, tmpdirs, env, probe }) {
383
+ const out = emptyRead();
384
+ if (!/\b(?:rm|rmdir|xargs)\b/.test(command) && !SHELL_WORDS.test(command)) return out;
288
385
  const commands = splitCommands(tokenize(stripHeredocs(command)));
289
386
  const vars = new Map(KNOWN_ENV.filter((n) => env[n]).map((n) => [n, env[n]]));
290
387
  const ctx = { cwd, home, tmpdirs, probe, vars };
@@ -292,33 +389,36 @@ function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
292
389
  ctx.cwd = next;
293
390
  vars.set("PWD", next);
294
391
  };
392
+ const deny = (why) => ({ ...out, deny: denyReason("bash", why) });
295
393
  setCwd(cwd);
296
394
  const stack = [];
297
- const real = [];
298
- for (const { words, after } of commands) {
395
+ let piped = false;
396
+ for (const { words, after, here } of commands) {
299
397
  const rest = stripPrefixes(words);
300
398
  if (!rest.length) {
301
399
  for (const w of words) if (ASSIGNMENT.test(w.value)) assign(w, vars, home);
302
400
  } else if (HEADER_WORDS.has(rest[0].value)) {
303
401
  if (rest[0].value !== "case" && rest[1]) vars.set(rest[1].value, null);
304
402
  } else if (!CLOSING_WORDS.has(rest[0].value)) {
305
- const name = commandName(rest[0]);
306
- if (name !== null && DECLARERS.has(name)) {
403
+ const name2 = commandName(rest[0]);
404
+ if (name2 !== null && DECLARERS.has(name2)) {
307
405
  for (const w of rest.slice(1)) if (ASSIGNMENT.test(w.value)) assign(w, vars, home);
308
- } else if (name === "cd" || name === "pushd" || name === "popd") {
406
+ } else if (name2 === "cd" || name2 === "pushd" || name2 === "popd") {
309
407
  const arg = rest[1];
310
408
  const value = arg ? expand(arg, vars) : null;
311
- if (name === "popd" || arg && (value === null || value === "-")) setCwd(null);
409
+ if (name2 === "popd" || arg && (value === null || value === "-")) setCwd(null);
312
410
  else if (!arg) setCwd(home);
313
411
  else setCwd(toWindowsPath({ ...arg, value }, ctx.cwd, home, tmpdirs[0]).path ?? null);
314
- } else if (name === "xargs") {
412
+ } else if (name2 === "xargs") {
315
413
  if (rest.slice(1).some((t) => {
316
414
  const n = commandName(t);
317
415
  return n === "rm" || n === "rmdir";
318
- })) {
319
- return deny("xargs feeding rm");
320
- }
321
- } else if (name === "rm" || name === "rmdir") {
416
+ })) return deny("xargs feeding rm");
417
+ if (rest.slice(1).some((t) => SHELLS.has(commandName(t) ?? "")) && DELETE_WORDS.test(rest.slice(1).map((t) => t.value).join(" "))) return deny("xargs feeding a shell that deletes");
418
+ } else if (name2 !== null && SHELLS.has(name2)) {
419
+ const args = rest.slice(1).map((t) => t.brace ? null : expand(t, vars));
420
+ out.shells.push({ name: name2, args: NATIVE_SHELLS.has(name2) ? args.map((a) => nativeArg(a, tmpdirs[0])) : args, cwd: ctx.cwd, raw: rest.map((t) => t.value).join(" "), ...piped || here ? { piped: true } : {} });
421
+ } else if (name2 === "rm" || name2 === "rmdir") {
322
422
  let options = true;
323
423
  for (const t of rest.slice(1)) {
324
424
  if (options && !t.dynamic && t.value === "--") {
@@ -328,51 +428,34 @@ function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
328
428
  if (options && !t.dynamic && t.value.length > 1 && t.value.startsWith("-")) continue;
329
429
  const c = classify(t, ctx);
330
430
  if (c.unresolvable) return deny(c.unresolvable);
331
- if (c.real) real.push(c.real);
431
+ if (c.real) out.targets.push(c.real);
332
432
  }
333
433
  }
334
434
  }
435
+ piped = after === "|" || after === "|&" || piped && !words.length && after === "(";
335
436
  if (after === "(") stack.push(ctx.cwd);
336
437
  if (after === ")" && stack.length) setCwd(stack.pop() ?? null);
337
438
  }
338
- if (!real.length) return null;
339
- const listed = real.slice(0, MAX_LISTED).join(", ");
340
- const more = real.length > MAX_LISTED ? ` \uC678 ${real.length - MAX_LISTED}\uAC1C` : "";
341
- return { decision: "ask", reason: `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed}${more}` };
342
- }
343
- function hookOutput({ decision, reason }) {
344
- return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision, permissionDecisionReason: reason } });
439
+ return out;
345
440
  }
346
- var realProbe = {
347
- exists(p) {
348
- try {
349
- fs.lstatSync(p);
350
- return true;
351
- } catch {
352
- return false;
353
- }
354
- },
355
- isIgnored(p) {
356
- const r = spawnSync("git", ["-C", win.dirname(p), "check-ignore", "-q", "--", p], { timeout: 3e3, windowsHide: true });
357
- return r.status === 0;
358
- }
359
- };
360
441
 
361
442
  // src/rm/run.ts
362
- import path4 from "path";
443
+ import path10 from "path";
363
444
 
364
445
  // src/config.ts
365
446
  import fs2 from "fs";
366
- import path3 from "path";
447
+ import path4 from "path";
367
448
 
368
449
  // src/config-shape.ts
450
+ var CONFIG_VERSION = 2;
369
451
  var DEFAULT_CONFIG = {
370
452
  lang: "ko",
371
453
  keyFile: null,
372
454
  display: "mod",
373
- context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
455
+ context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
374
456
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
375
457
  };
458
+ var LEGACY_CONTEXT_DEFAULTS = { minTokens: 1e5, compactMinTokens: 2e5, unitDoneYes: 0.7 };
376
459
  var LANGS = ["ko", "en"];
377
460
  var DISPLAYS = ["mod", "statusline", "message"];
378
461
  function isRecord(value) {
@@ -386,22 +469,27 @@ function mergeSection(defaults, raw) {
386
469
  }
387
470
  return out;
388
471
  }
472
+ function withoutLegacyDefaults(context) {
473
+ if (!isRecord(context)) return context;
474
+ return Object.fromEntries(Object.entries(context).filter(([key, value]) => LEGACY_CONTEXT_DEFAULTS[key] !== value));
475
+ }
389
476
  function normalizeConfig(raw) {
390
477
  const r = isRecord(raw) ? raw : {};
391
478
  return {
392
479
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
393
480
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
394
481
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
395
- context: mergeSection(DEFAULT_CONFIG.context, r.context),
482
+ context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
396
483
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
397
484
  };
398
485
  }
399
486
 
400
487
  // src/paths.ts
401
- import path2 from "path";
402
- var claudeDir = (home) => path2.join(home, ".claude");
403
- var dataDir = (home) => path2.join(claudeDir(home), "claude-jev-advisor");
404
- var configPath = (home) => path2.join(dataDir(home), "config.json");
488
+ import path3 from "path";
489
+ var claudeDir = (home) => path3.join(home, ".claude");
490
+ var dataDir = (home) => path3.join(claudeDir(home), "claude-jev-advisor");
491
+ var configPath = (home) => path3.join(dataDir(home), "config.json");
492
+ var logDir = (home) => path3.join(dataDir(home), "log");
405
493
 
406
494
  // src/config.ts
407
495
  function readConfig(home) {
@@ -412,17 +500,1310 @@ function readConfig(home) {
412
500
  }
413
501
  }
414
502
 
503
+ // src/context/files.ts
504
+ import fs3 from "fs";
505
+ import path5 from "path";
506
+ function logFile(home, now) {
507
+ return path5.join(logDir(home), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.jsonl`);
508
+ }
509
+ function appendLog(home, now, record, secrets = []) {
510
+ const file = logFile(home, now);
511
+ fs3.mkdirSync(path5.dirname(file), { recursive: true });
512
+ let line = JSON.stringify({ at: now.toISOString(), ...record });
513
+ for (const secret of secrets) {
514
+ if (!secret) continue;
515
+ for (const form of [secret, JSON.stringify(secret).slice(1, -1)]) line = line.split(form).join("[redacted]");
516
+ }
517
+ fs3.appendFileSync(file, `${line}
518
+ `);
519
+ }
520
+
521
+ // src/jev.ts
522
+ import fs4 from "fs";
523
+ var JEV_URL = "https://api.typesafe.ai/v1/systemone";
524
+ var JEV_TIMEOUT_MS = 8e3;
525
+ var KEY_PREFIX = "TYPESAFE_API_KEY=";
526
+ function readJevKey(env, keyFile) {
527
+ const fromEnv = env.TYPESAFE_API_KEY?.trim();
528
+ if (fromEnv) return fromEnv;
529
+ if (!keyFile) return null;
530
+ let text;
531
+ try {
532
+ text = fs4.readFileSync(keyFile, "utf8");
533
+ } catch {
534
+ return null;
535
+ }
536
+ const line = text.replace(/^\uFEFF/, "").split(/\r?\n/).find((l) => l.startsWith(KEY_PREFIX));
537
+ const key = line?.slice(KEY_PREFIX.length).trim().replace(/^["']|["']$/g, "");
538
+ return key || null;
539
+ }
540
+ function jevKeys(env, keyFile) {
541
+ return [...new Set([readJevKey(env, keyFile), readJevKey({}, keyFile)].filter((key) => Boolean(key)))];
542
+ }
543
+ function withoutKeys(text, keys) {
544
+ return keys.reduce((out, key) => out.split(key).join("[redacted]"), text);
545
+ }
546
+ async function callJev(request, opts) {
547
+ const now = opts.now ?? Date.now;
548
+ const fetchFn = opts.fetchFn ?? fetch;
549
+ const limit = opts.timeoutMs ?? JEV_TIMEOUT_MS;
550
+ const redact = (text) => text.split(opts.key).join("[redacted]");
551
+ const started = now();
552
+ const controller = new AbortController();
553
+ let timer;
554
+ const expired = new Promise((_resolve, reject) => {
555
+ timer = setTimeout(() => {
556
+ const err = new Error(`Jev did not answer within ${limit} ms`);
557
+ controller.abort(err);
558
+ reject(err);
559
+ }, limit);
560
+ });
561
+ let requestId = null;
562
+ const exchange = (async () => {
563
+ const res = await fetchFn(JEV_URL, {
564
+ method: "POST",
565
+ headers: { Authorization: `Bearer ${opts.key}`, "Content-Type": "application/json" },
566
+ body: JSON.stringify({ model: "jev-latest", ...request }),
567
+ signal: controller.signal
568
+ });
569
+ requestId = res.headers.get("request-id") ?? res.headers.get("x-request-id");
570
+ return { res, text: await res.text() };
571
+ })();
572
+ exchange.catch(() => {
573
+ });
574
+ try {
575
+ const { res, text } = await Promise.race([exchange, expired]);
576
+ if (!res.ok) return { error: redact(`HTTP ${res.status}: ${text}`).slice(0, 300), requestId, ms: now() - started };
577
+ const body = JSON.parse(text);
578
+ const answers = {};
579
+ for (const [id, answer] of Object.entries(body.answers ?? {})) {
580
+ if (typeof answer?.noul === "number") answers[id] = answer.noul;
581
+ }
582
+ return { answers, model: typeof body.model === "string" ? body.model : null, requestId, ms: now() - started };
583
+ } catch (err) {
584
+ return { error: redact(String(err?.message ?? err)).slice(0, 300), requestId, ms: now() - started };
585
+ } finally {
586
+ clearTimeout(timer);
587
+ }
588
+ }
589
+
590
+ // src/rm/cmd.ts
591
+ import path6 from "path";
592
+ var win3 = path6.win32;
593
+ var SHELLS2 = /* @__PURE__ */ new Set(["sh", "bash", "cmd", "powershell", "pwsh"]);
594
+ var DELETES = /* @__PURE__ */ new Set(["del", "erase", "rd", "rmdir"]);
595
+ var CONTROL = /* @__PURE__ */ new Set(["if", "else", "for", "call", "start"]);
596
+ var UNKNOWN = "\0";
597
+ var EXCERPT_CHARS = 200;
598
+ function expandVars(line, vars) {
599
+ return line.replace(/%([^%\s"]+)%/g, (_m, name2) => vars.get(name2.toLowerCase()) ?? UNKNOWN).replace(/%~[^\s"]*/g, UNKNOWN).replace(/![^!\s"]+!/g, UNKNOWN);
600
+ }
601
+ function hasBlock(line) {
602
+ let quoted = false;
603
+ for (let i = 0; i < line.length; i++) {
604
+ if (line[i] === '"') quoted = !quoted;
605
+ else if (!quoted && line[i] === "^") i++;
606
+ else if (!quoted && (line[i] === "(" || line[i] === ")")) return true;
607
+ }
608
+ return false;
609
+ }
610
+ function segmentsOf(line) {
611
+ const out = [];
612
+ let current = "";
613
+ let piped = false;
614
+ let quoted = false;
615
+ for (let i = 0; i < line.length; i++) {
616
+ const c = line[i];
617
+ if (c === '"') quoted = !quoted;
618
+ if (!quoted && c === "^" && i + 1 < line.length) {
619
+ current += c + line[++i];
620
+ continue;
621
+ }
622
+ const redirect = c === "&" && /[<>]$/.test(current);
623
+ if (!quoted && !redirect && "&|\n".includes(c)) {
624
+ out.push({ text: current, piped });
625
+ current = "";
626
+ piped = c === "|" && line[i + 1] !== "|";
627
+ if ((c === "&" || c === "|") && line[i + 1] === c) i++;
628
+ continue;
629
+ }
630
+ current += c;
631
+ }
632
+ out.push({ text: current, piped });
633
+ return out.filter((s) => s.text.trim() !== "");
634
+ }
635
+ function wordsOf(segment, delimiters) {
636
+ const words = [];
637
+ let word = null;
638
+ let quoted = false;
639
+ let skipNext = false;
640
+ const start = () => word ??= { value: "", dynamic: false, shown: "", end: 0 };
641
+ const add = (c) => {
642
+ const w = start();
643
+ if (c === UNKNOWN) {
644
+ w.dynamic = true;
645
+ w.shown += "%?%";
646
+ } else {
647
+ w.value += c;
648
+ w.shown += c;
649
+ }
650
+ };
651
+ const finish = (at) => {
652
+ if (word === null) return;
653
+ word.end = at;
654
+ if (skipNext) skipNext = false;
655
+ else words.push(word);
656
+ word = null;
657
+ };
658
+ for (let i = 0; i < segment.length; i++) {
659
+ const c = segment[i];
660
+ if (c === '"') {
661
+ quoted = !quoted;
662
+ start();
663
+ continue;
664
+ }
665
+ if (quoted) {
666
+ add(c);
667
+ continue;
668
+ }
669
+ if (c === "^") {
670
+ if (i + 1 < segment.length) add(segment[++i]);
671
+ continue;
672
+ }
673
+ if (c === " " || c === " " || c === "\r" || delimiters.includes(c)) {
674
+ finish(i);
675
+ continue;
676
+ }
677
+ if (c === ">" || c === "<") {
678
+ if (word !== null && /^\d$/.test(word.value) && !word.dynamic) word = null;
679
+ else finish(i);
680
+ while (segment[i + 1] === ">") i++;
681
+ if (segment[i + 1] === "&") {
682
+ i++;
683
+ while (/\d/.test(segment[i + 1] ?? "")) i++;
684
+ continue;
685
+ }
686
+ skipNext = true;
687
+ continue;
688
+ }
689
+ add(c);
690
+ }
691
+ finish(segment.length);
692
+ return words;
693
+ }
694
+ function nameOf(word) {
695
+ if (word.dynamic && /%\?%[^\\/]*$/.test(word.shown)) return null;
696
+ const text = word.value.replace(/^@/, "");
697
+ const glued = /^(del|erase|rd|rmdir|cd|chdir|pushd|popd|if|else|for|call|start)([/,;=].*)$/i.exec(text) ?? /^(cd|chdir|pushd)([.\\].*)$/i.exec(text);
698
+ if (glued) return { name: glued[1].toLowerCase(), rest: glued[2] };
699
+ return { name: win3.basename(text.replace(/\//g, "\\")).replace(/\.(?:exe|com)$/i, "").toLowerCase(), rest: "" };
700
+ }
701
+ function classify2(word, cwd, everywhere, ctx) {
702
+ if (word.dynamic || word.value.includes("%")) return { unresolvable: `a variable that cannot be worked out (${word.shown})` };
703
+ let base = word.value;
704
+ let pattern = null;
705
+ if (everywhere || /[*?]/.test(word.value)) {
706
+ const split = splitWindowsPattern(word.value, /[*?]/);
707
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${word.value})` };
708
+ base = split.dir;
709
+ pattern = split.pattern;
710
+ }
711
+ const resolved = resolveWindowsPath(base, cwd, null);
712
+ if ("unresolvable" in resolved) return { unresolvable: resolved.unresolvable };
713
+ if ("unc" in resolved) return { real: { shown: word.value, path: null } };
714
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
715
+ }
716
+ function readCmd(line, ctx) {
717
+ const out = emptyRead();
718
+ const vars = /* @__PURE__ */ new Map();
719
+ for (const name2 of ["TEMP", "TMP", "USERPROFILE"]) {
720
+ const value = ctx.env[name2];
721
+ if (value) vars.set(name2.toLowerCase(), value);
722
+ }
723
+ if (ctx.cwd !== null) vars.set("cd", ctx.cwd);
724
+ let cwd = ctx.cwd;
725
+ const deny = (why) => ({ ...out, deny: denyReason("cmd", why) });
726
+ const expanded = expandVars(line, vars);
727
+ const pieces = segmentsOf(expanded).map((s) => ({ ...s, text: s.text.replace(/^[\s@,;=]+/, "") })).filter((s) => s.text !== "");
728
+ const segments2 = pieces.map((s) => s.text);
729
+ const control = segments2.some((s) => {
730
+ const first = wordsOf(s, "")[0];
731
+ const named = first ? nameOf(first) : null;
732
+ return first !== void 0 && (named === null || CONTROL.has(named.name));
733
+ });
734
+ if (control || hasBlock(expanded)) {
735
+ if (!DELETE_WORDS.test(expanded)) return out;
736
+ const shown = expanded.trim().replace(/\s+/g, " ").slice(0, EXCERPT_CHARS).split(UNKNOWN).join("%?%");
737
+ return deny(`a delete on a cmd line with if, else, for, call, start, parentheses or a command that cannot be worked out (${shown}); write plain del or rd lines, with names that hold parentheses in double quotes`);
738
+ }
739
+ for (const { text: segment, piped } of pieces) {
740
+ const words = wordsOf(segment, "");
741
+ if (!words.length) continue;
742
+ const first = words[0];
743
+ const named = nameOf(first);
744
+ const shown = segment.trim().split(UNKNOWN).join("%?%");
745
+ const { name: name2, rest } = named;
746
+ const args = () => [...rest ? wordsOf(rest, ",;=") : [], ...wordsOf(segment.slice(first.end), ",;=")];
747
+ if (/^[a-z]:$/i.test(first.value.replace(/^@/, ""))) {
748
+ cwd = null;
749
+ } else if (SHELLS2.has(name2)) {
750
+ const msys = name2 === "sh" || name2 === "bash";
751
+ out.shells.push({ name: name2, args: words.slice(1).map((w) => w.dynamic || msys && w.value.includes("'") ? null : w.value), cwd, raw: shown, ...piped ? { piped } : {} });
752
+ } else if (name2 === "popd") {
753
+ cwd = null;
754
+ } else if (name2 === "cd" || name2 === "chdir" || name2 === "pushd") {
755
+ const given = args();
756
+ const switchDrive = name2 === "pushd" || given.some((w) => !w.dynamic && w.value.toLowerCase() === "/d");
757
+ const parts = given.filter((w) => w.dynamic || w.value.toLowerCase() !== "/d");
758
+ if (!parts.length) continue;
759
+ const resolved = parts.some((w) => w.dynamic) ? null : resolveWindowsPath(parts.map((w) => w.value).join(" "), cwd, null);
760
+ if (resolved === null || !("path" in resolved)) cwd = null;
761
+ else if (switchDrive || cwd === null || resolved.path.slice(0, 2).toLowerCase() === cwd.slice(0, 2).toLowerCase()) cwd = resolved.path;
762
+ } else if (DELETES.has(name2)) {
763
+ const given = args();
764
+ const switches = given.filter((w) => !w.dynamic && w.value.startsWith("/"));
765
+ const everywhere = (name2 === "del" || name2 === "erase") && switches.some((w) => w.value.split("/").some((s) => s.toLowerCase() === "s"));
766
+ for (const word of given) {
767
+ if (switches.includes(word)) continue;
768
+ const c = classify2(word, cwd, everywhere, ctx);
769
+ if (c.unresolvable) return deny(c.unresolvable);
770
+ if (c.real) out.targets.push(c.real);
771
+ }
772
+ }
773
+ }
774
+ return out;
775
+ }
776
+
777
+ // src/rm/powershell.ts
778
+ import fs5 from "fs";
779
+ import path7 from "path";
780
+ import { spawn } from "child_process";
781
+ var PS_PARSE_TIMEOUT_MS = 3e3;
782
+ var POWERSHELL_WORDS = /\b(?:remove-item|ri|rm|del|erase|rd|rmdir|sh|bash|cmd|powershell|pwsh)(?:\.exe)?\b|\bdelete/i;
783
+ var DELETE_WORD = new RegExp(`${DELETE_WORDS.source}|\\bdelete`, "i");
784
+ var OTHER_DRIVES = /* @__PURE__ */ new Set(["alias", "env", "function", "variable", "hklm", "hkcu", "cert", "wsman"]);
785
+ var EXCERPT_CHARS2 = 200;
786
+ var KNOWN_ENV2 = ["TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE"];
787
+ var READER = String.raw`
788
+ param($source)
789
+ $ErrorActionPreference = 'Stop'
790
+ $tokens = $null
791
+ $errors = $null
792
+ $root = [System.Management.Automation.Language.Parser]::ParseInput($source, [ref]$tokens, [ref]$errors)
793
+
794
+ function Value($ast) {
795
+ if ($null -eq $ast) { return @{ dynamic = '' } }
796
+ if ($ast -is [System.Management.Automation.Language.StringConstantExpressionAst]) { return @{ const = $ast.Value } }
797
+ if ($ast -is [System.Management.Automation.Language.ConstantExpressionAst]) { return @{ const = [string]$ast.Value } }
798
+ if ($ast -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) {
799
+ if ($ast.Extent.Text.Contains([string][char]96 + '$')) { return @{ dynamic = $ast.Extent.Text } }
800
+ $parts = New-Object System.Collections.ArrayList
801
+ $at = 0
802
+ foreach ($n in $ast.NestedExpressions) {
803
+ if (-not ($n -is [System.Management.Automation.Language.VariableExpressionAst]) -or $n.Splatted) { return @{ dynamic = $ast.Extent.Text } }
804
+ $i = $ast.Value.IndexOf($n.Extent.Text, $at, [System.StringComparison]::Ordinal)
805
+ if ($i -lt 0) { return @{ dynamic = $ast.Extent.Text } }
806
+ if ($i -gt $at) { [void]$parts.Add(@{ const = $ast.Value.Substring($at, $i - $at) }) }
807
+ [void]$parts.Add(@{ variable = $n.VariablePath.UserPath })
808
+ $at = $i + $n.Extent.Text.Length
809
+ }
810
+ if ($at -lt $ast.Value.Length) { [void]$parts.Add(@{ const = $ast.Value.Substring($at) }) }
811
+ return @{ expand = $parts.ToArray(); text = $ast.Extent.Text }
812
+ }
813
+ if ($ast -is [System.Management.Automation.Language.VariableExpressionAst] -and -not $ast.Splatted) { return @{ variable = $ast.VariablePath.UserPath } }
814
+ if ($ast -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @{ array = @($ast.Elements | ForEach-Object { Value $_ }) } }
815
+ if ($ast -is [System.Management.Automation.Language.CommandParameterAst]) { return @{ const = $ast.Extent.Text } }
816
+ return @{ dynamic = $ast.Extent.Text }
817
+ }
818
+
819
+ function Constant($ast) {
820
+ if ($ast -is [System.Management.Automation.Language.CommandParameterAst]) { return ($null -eq $ast.Argument) -or (Constant $ast.Argument) }
821
+ if ($ast -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @($ast.Elements | Where-Object { -not (Constant $_) }).Count -eq 0 }
822
+ return $ast -is [System.Management.Automation.Language.ConstantExpressionAst]
823
+ }
824
+
825
+ function NameOf($command) {
826
+ $name = $command.GetCommandName()
827
+ if ($null -eq $name) { return $null }
828
+ if ($name -eq 'cd..' -or $name -eq 'cd\') { return $name }
829
+ $name = $name -replace '^Microsoft\.PowerShell\.Management\\', ''
830
+ $name = ($name -split '[\\/]')[-1]
831
+ return ($name -replace '\.exe$', '').ToLowerInvariant()
832
+ }
833
+
834
+ function TopLevel($ast) {
835
+ for ($p = $ast.Parent; $null -ne $p; $p = $p.Parent) {
836
+ if ($p -eq $root) { return $true }
837
+ if (-not ($p -is [System.Management.Automation.Language.PipelineAst] -or $p -is [System.Management.Automation.Language.NamedBlockAst])) { return $false }
838
+ }
839
+ return $false
840
+ }
841
+
842
+ function InOrder($ast) {
843
+ for ($p = $ast.Parent; $null -ne $p -and $p -ne $root; $p = $p.Parent) {
844
+ if ($p -is [System.Management.Automation.Language.LoopStatementAst] -or $p -is [System.Management.Automation.Language.SwitchStatementAst] -or $p -is [System.Management.Automation.Language.ScriptBlockAst] -or $p -is [System.Management.Automation.Language.TrapStatementAst]) { return $false }
845
+ }
846
+ return $true
847
+ }
848
+
849
+ function Bound($command) {
850
+ $binding = [System.Management.Automation.Language.StaticParameterBinder]::BindCommand($command, $true)
851
+ $out = @{ bindError = ($binding.BindingExceptions.Count -gt 0); whatIf = $false }
852
+ foreach ($key in $binding.BoundParameters.Keys) {
853
+ $result = $binding.BoundParameters[$key]
854
+ switch ($key) {
855
+ 'Path' { $out.path = Value $result.Value }
856
+ 'LiteralPath' { $out.literalPath = Value $result.Value }
857
+ 'Name' { $out.name = $result }
858
+ 'MemberName' { $out.member = $result }
859
+ 'WhatIf' {
860
+ $v = $result.Value
861
+ if ($null -eq $v) { $out.whatIf = ($result.ConstantValue -eq $true) }
862
+ else { $out.whatIf = ($v -is [System.Management.Automation.Language.VariableExpressionAst]) -and ($v.VariablePath.UserPath -eq 'true') }
863
+ }
864
+ }
865
+ }
866
+ return $out
867
+ }
868
+
869
+ $deletes = @('remove-item', 'ri', 'rm', 'del', 'erase', 'rd', 'rmdir')
870
+ $moves = @('set-location', 'sl', 'cd', 'chdir', 'push-location', 'pushd')
871
+ $pops = @('pop-location', 'popd')
872
+ $shells = @('sh', 'bash', 'cmd', 'powershell', 'pwsh')
873
+ $dynamics = @('invoke-expression', 'iex', 'start-process', 'saps', 'start', 'invoke-command', 'icm', 'start-job', 'sajb', 'start-threadjob', 'set-alias', 'sal', 'new-alias', 'nal', 'add-type')
874
+ $members = @('foreach-object', '%', 'foreach')
875
+ $drives = @('new-psdrive', 'ndr', 'mount')
876
+ $variableCommands = @('set-variable', 'sv', 'new-variable', 'nv', 'remove-variable', 'rv', 'clear-variable', 'clv', 'get-variable', 'gv')
877
+ $variableParameters = @('outvariable', 'errorvariable', 'warningvariable', 'informationvariable', 'pipelinevariable')
878
+ $variableAliases = @('ov', 'ev', 'wv', 'iv', 'pv')
879
+ $nameParameters = @('variable', 'bindingvariable') + $variableParameters
880
+
881
+ function NamesVariable($element) {
882
+ if (-not ($element -is [System.Management.Automation.Language.CommandParameterAst])) { return $false }
883
+ $p = $element.ParameterName.ToLowerInvariant()
884
+ return $variableAliases -contains $p -or ($p.Length -ge 2 -and @($nameParameters | Where-Object { $_.StartsWith($p) }).Count -gt 0)
885
+ }
886
+ $codeMethods = @('invoke', 'invokereturnasis', 'invokewithcontext', 'invokescript', 'newscriptblock', 'addscript', 'addcommand', 'begininvoke')
887
+ $dotnet = @('system.io.file', 'io.file', 'file', 'system.io.directory', 'io.directory', 'directory')
888
+ $fileCommands = @('get-item', 'gi', 'get-childitem', 'gci', 'ls', 'dir', 'new-item', 'ni')
889
+ $unstable = New-Object System.Collections.ArrayList
890
+ $anyVariable = $false
891
+ # An instance Delete() deletes a file only on an object a file command, -PassThru or a FileInfo made; COM objects have
892
+ # one too.
893
+ $fileObjects = ($source -match 'FileInfo|DirectoryInfo|FileSystemInfo') -or @($root.FindAll({
894
+ param($x)
895
+ ($x -is [System.Management.Automation.Language.CommandAst] -and $fileCommands -contains (NameOf $x)) -or
896
+ ($x -is [System.Management.Automation.Language.CommandParameterAst] -and $x.ParameterName.Length -ge 3 -and 'passthru'.StartsWith($x.ParameterName.ToLowerInvariant()))
897
+ }, $true)).Count -gt 0
898
+ $processStart = $source -match 'ProcessStartInfo|Diagnostics\.Process'
899
+
900
+ # A name handed over as text (-OutVariable d, Tee-Object -Variable d, Set-Item variable:d, 'TEMP' handed to
901
+ # SetEnvironmentVariable, '$d = 1' handed to iex) can write a variable without the parser seeing a variable there.
902
+ $named = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
903
+ foreach ($s in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true)) {
904
+ foreach ($m in [regex]::Matches($s.Value, '\$\{?(?:\w+:)?([\w?]+)')) { [void]$named.Add($m.Groups[1].Value) }
905
+ $parent = $s.Parent
906
+ $at = if ($parent -is [System.Management.Automation.Language.CommandAst]) { $parent.CommandElements.IndexOf($s) } else { -1 }
907
+ # A bare Variable: or Env: drive (New-Item -Path Env: -Name TEMP, Set-Location Variable:) writes names given elsewhere.
908
+ if ($s.Value -match '^(?:[\w.]+\\)?(?:variable|env|environment):{1,2}[\\/]?$') { $anyVariable = $true }
909
+ elseif ($s.Value -match '^(?:[\w.]+\\)?(?:variable|env|environment):{1,2}[\\/]?(.+)$') { [void]$named.Add($Matches[1]) }
910
+ elseif (($at -gt 0 -and (NamesVariable $parent.CommandElements[$at - 1])) -or (NamesVariable $parent) -or ($parent -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and $parent.Member -ne $s -and $parent.Member.Value -match 'variable|^set$|^get$')) {
911
+ [void]$named.Add(($s.Value -replace '^\+', '' -replace '^(?:script|global|local|private):', ''))
912
+ }
913
+ }
914
+ foreach ($d in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.DataStatementAst] }, $true)) { if ($d.Variable) { [void]$named.Add($d.Variable) } }
915
+ foreach ($v in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)) {
916
+ $path = $v.VariablePath.UserPath
917
+ if ($v.Parent -is [System.Management.Automation.Language.ParameterAst] -or $named.Contains(($path -split ':')[-1])) { [void]$unstable.Add($path) }
918
+ }
919
+
920
+ $items = New-Object System.Collections.ArrayList
921
+ $found = $root.FindAll({
922
+ param($a)
923
+ $a -is [System.Management.Automation.Language.CommandAst] -or
924
+ $a -is [System.Management.Automation.Language.AssignmentStatementAst] -or
925
+ $a -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -or
926
+ $a -is [System.Management.Automation.Language.UnaryExpressionAst] -or
927
+ $a -is [System.Management.Automation.Language.ForEachStatementAst] -or
928
+ $a -is [System.Management.Automation.Language.ConvertExpressionAst] -or
929
+ $a -is [System.Management.Automation.Language.CommandParameterAst]
930
+ }, $true)
931
+ foreach ($a in $found) {
932
+ if ($a -is [System.Management.Automation.Language.AssignmentStatementAst]) {
933
+ $left = $a.Left
934
+ if ($left -is [System.Management.Automation.Language.VariableExpressionAst] -and -not $left.Splatted -and ($left.VariablePath.IsUnqualified -or $left.VariablePath.IsDriveQualified) -and $a.Operator -eq 'Equals' -and (TopLevel $a)) {
935
+ $right = if ($a.Right -is [System.Management.Automation.Language.CommandExpressionAst]) { Value $a.Right.Expression } else { @{ dynamic = $a.Right.Extent.Text } }
936
+ [void]$items.Add(@{ kind = 'assign'; name = $left.VariablePath.UserPath; value = $right })
937
+ } else {
938
+ foreach ($v in $left.FindAll({ param($x) $x -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)) { [void]$unstable.Add($v.VariablePath.UserPath) }
939
+ }
940
+ continue
941
+ }
942
+ if ($a -is [System.Management.Automation.Language.UnaryExpressionAst]) {
943
+ if ($a.Child -is [System.Management.Automation.Language.VariableExpressionAst] -and @('PlusPlus', 'MinusMinus', 'PostfixPlusPlus', 'PostfixMinusMinus') -contains [string]$a.TokenKind) { [void]$unstable.Add($a.Child.VariablePath.UserPath) }
944
+ continue
945
+ }
946
+ if ($a -is [System.Management.Automation.Language.ForEachStatementAst]) {
947
+ [void]$unstable.Add($a.Variable.VariablePath.UserPath)
948
+ continue
949
+ }
950
+ if ($a -is [System.Management.Automation.Language.ConvertExpressionAst]) {
951
+ if ($a.Type.TypeName.Name -eq 'ref' -and $a.Child -is [System.Management.Automation.Language.VariableExpressionAst]) { [void]$unstable.Add($a.Child.VariablePath.UserPath) }
952
+ continue
953
+ }
954
+ if ($a -is [System.Management.Automation.Language.CommandParameterAst]) {
955
+ # PowerShell takes any unambiguous start of a parameter name, so -OutVar and -ErrorVar count too.
956
+ $p = $a.ParameterName.ToLowerInvariant()
957
+ if ($variableAliases -contains $p -or ($p.Length -ge 2 -and @($variableParameters | Where-Object { $_.StartsWith($p) }).Count -gt 0)) { $anyVariable = $true }
958
+ continue
959
+ }
960
+ if ($a -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) {
961
+ $member = if ($a.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $a.Member.Value.ToLowerInvariant() } else { $null }
962
+ $type = if ($a.Expression -is [System.Management.Automation.Language.TypeExpressionAst]) { $a.Expression.TypeName.FullName.ToLowerInvariant() } else { $null }
963
+ if ($a.Static -and $member -eq 'delete' -and $dotnet -contains $type) {
964
+ $first = if ($a.Arguments -and $a.Arguments.Count -gt 0) { Value $a.Arguments[0] } else { @{ dynamic = $a.Extent.Text } }
965
+ [void]$items.Add(@{ kind = 'delete'; path = $first; literalPath = $null; literal = $true; dotnet = $true; whatIf = $false; bindError = $false; inPipeline = $false; ordered = (InOrder $a) })
966
+ } elseif ($null -eq $member -or ($a.Static -and $null -eq $type) -or $codeMethods -contains $member -or
967
+ ($member -eq 'create' -and $type -match '(^|\.)(scriptblock|powershell)$') -or
968
+ ($member -eq 'start' -and ($type -match '(^|\.)process$' -or ($null -eq $type -and $processStart))) -or
969
+ ($member.StartsWith('delete') -and ($null -ne $type -or $fileObjects))) {
970
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
971
+ }
972
+ continue
973
+ }
974
+ if ($a.CommandElements[0] -is [System.Management.Automation.Language.ScriptBlockExpressionAst]) { continue }
975
+ $name = NameOf $a
976
+ if ($null -eq $name) {
977
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
978
+ continue
979
+ }
980
+ $pipeline = $a.Parent
981
+ $inPipeline = ($pipeline -is [System.Management.Automation.Language.PipelineAst]) -and ($pipeline.PipelineElements.IndexOf($a) -gt 0)
982
+ if ($deletes -contains $name) {
983
+ $b = Bound $a
984
+ [void]$items.Add(@{ kind = 'delete'; path = $b.path; literalPath = $b.literalPath; literal = ($null -ne $b.literalPath); dotnet = $false; whatIf = $b.whatIf; bindError = $b.bindError; inPipeline = $inPipeline; ordered = (InOrder $a) })
985
+ } elseif ($name -eq 'cd..' -or $name -eq 'cd\') {
986
+ [void]$items.Add(@{ kind = 'cd'; path = @{ const = $name.Substring(2) }; literal = $true; inBlock = -not (TopLevel $a) })
987
+ } elseif ($moves -contains $name) {
988
+ $b = Bound $a
989
+ $to = if ($null -ne $b.literalPath) { $b.literalPath } else { $b.path }
990
+ [void]$items.Add(@{ kind = 'cd'; path = $to; literal = ($null -ne $b.literalPath); inBlock = -not (TopLevel $a) })
991
+ } elseif ($pops -contains $name) {
992
+ [void]$items.Add(@{ kind = 'pop' })
993
+ } elseif ($shells -contains $name) {
994
+ $elements = @($a.CommandElements | Select-Object -Skip 1 | ForEach-Object { Value $_ })
995
+ [void]$items.Add(@{ kind = 'shell'; name = $name; args = $elements; raw = $a.Extent.Text; inPipeline = $inPipeline; ordered = (InOrder $a) })
996
+ } elseif ($dynamics -contains $name) {
997
+ if ($inPipeline) {
998
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
999
+ } else {
1000
+ $values = New-Object System.Collections.ArrayList
1001
+ foreach ($e in @($a.CommandElements | Select-Object -Skip 1)) {
1002
+ if (Constant $e) { continue }
1003
+ [void]$values.Add((Value $(if ($e -is [System.Management.Automation.Language.CommandParameterAst]) { $e.Argument } else { $e })))
1004
+ }
1005
+ [void]$items.Add(@{ kind = 'dynamic'; text = $a.Extent.Text; args = $values.ToArray() })
1006
+ }
1007
+ } elseif ($members -contains $name) {
1008
+ $m = (Bound $a).member
1009
+ if ($null -ne $m) {
1010
+ $v = $m.ConstantValue
1011
+ if ($null -eq $v -or ([string]$v) -match '^delete' -or $codeMethods -contains ([string]$v).ToLowerInvariant()) { [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() }) }
1012
+ }
1013
+ } elseif ($drives -contains $name) {
1014
+ # A drive made here can carry a one-letter name that reads like a real drive.
1015
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
1016
+ } elseif ($variableCommands -contains $name) {
1017
+ $b = Bound $a
1018
+ $names = if ($null -ne $b.name) { @($b.name.ConstantValue) } else { @() }
1019
+ if ($names.Count -eq 0 -or @($names | Where-Object { $null -eq $_ -or [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters([string]$_) }).Count -gt 0) { $anyVariable = $true }
1020
+ else { foreach ($n in $names) { [void]$unstable.Add([string]$n) } }
1021
+ }
1022
+ }
1023
+ $result = @{ errors = ($errors.Count -gt 0); items = $items.ToArray(); unstable = $unstable.ToArray(); anyVariable = $anyVariable }
1024
+ [Console]::Out.Write((ConvertTo-Json -InputObject $result -Depth 12 -Compress))
1025
+ `;
1026
+ var LOADER = String.raw`
1027
+ $utf8 = New-Object System.Text.UTF8Encoding $false
1028
+ [Console]::InputEncoding = $utf8
1029
+ [Console]::OutputEncoding = $utf8
1030
+ $all = [Console]::In.ReadToEnd()
1031
+ $cut = $all.IndexOf([char]0)
1032
+ & ([scriptblock]::Create($all.Substring(0, $cut))) $all.Substring($cut + 1)
1033
+ `;
1034
+ function powerShellExe(env) {
1035
+ for (const dir of (env.PATH ?? env.Path ?? "").split(path7.delimiter)) {
1036
+ if (dir && fs5.existsSync(path7.join(dir, "pwsh.exe"))) return path7.join(dir, "pwsh.exe");
1037
+ }
1038
+ return "powershell.exe";
1039
+ }
1040
+ function realPowerShell(env, timeoutMs = PS_PARSE_TIMEOUT_MS) {
1041
+ const encoded = Buffer.from(LOADER, "utf16le").toString("base64");
1042
+ return (source) => new Promise((resolve) => {
1043
+ let out = "";
1044
+ let done = false;
1045
+ const finish = (value) => {
1046
+ if (done) return;
1047
+ done = true;
1048
+ clearTimeout(timer);
1049
+ resolve(value);
1050
+ };
1051
+ const child = spawn(powerShellExe(env), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] });
1052
+ const timer = setTimeout(() => {
1053
+ child.kill();
1054
+ finish(null);
1055
+ }, timeoutMs);
1056
+ child.on("error", () => finish(null));
1057
+ child.on("close", (code) => finish(code === 0 ? out : null));
1058
+ child.stdout.setEncoding("utf8");
1059
+ child.stdout.on("data", (chunk) => {
1060
+ out += chunk;
1061
+ });
1062
+ child.stdin.on("error", () => {
1063
+ });
1064
+ child.stdin.end(`${READER}\0${source}`, "utf8");
1065
+ });
1066
+ }
1067
+ var list = (value) => Array.isArray(value) ? value : value == null ? [] : [value];
1068
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1069
+ function partOf(raw) {
1070
+ if (!isRecord2(raw)) throw new Error("bad value");
1071
+ if (typeof raw.const === "string") return { const: raw.const };
1072
+ if (typeof raw.variable === "string") return { variable: raw.variable };
1073
+ throw new Error("bad value");
1074
+ }
1075
+ function valueOf(raw) {
1076
+ if (!isRecord2(raw)) throw new Error("bad value");
1077
+ if ("expand" in raw) {
1078
+ if (!Array.isArray(raw.expand) || typeof raw.text !== "string") throw new Error("bad value");
1079
+ return { expand: raw.expand.map(partOf), text: raw.text };
1080
+ }
1081
+ if ("array" in raw) return { array: list(raw.array).map(valueOf) };
1082
+ if ("dynamic" in raw) {
1083
+ if (typeof raw.dynamic !== "string") throw new Error("bad value");
1084
+ return { dynamic: raw.dynamic };
1085
+ }
1086
+ return partOf(raw);
1087
+ }
1088
+ var optionalValue = (raw) => raw == null ? null : valueOf(raw);
1089
+ var flag = (raw) => {
1090
+ if (typeof raw !== "boolean") throw new Error("bad flag");
1091
+ return raw;
1092
+ };
1093
+ function itemOf(raw) {
1094
+ if (!isRecord2(raw) || typeof raw.kind !== "string") throw new Error("bad item");
1095
+ switch (raw.kind) {
1096
+ case "delete":
1097
+ return { kind: "delete", path: optionalValue(raw.path), literalPath: optionalValue(raw.literalPath), literal: flag(raw.literal), dotnet: flag(raw.dotnet), whatIf: flag(raw.whatIf), bindError: flag(raw.bindError), inPipeline: flag(raw.inPipeline), ordered: flag(raw.ordered) };
1098
+ case "cd":
1099
+ return { kind: "cd", path: optionalValue(raw.path), literal: flag(raw.literal), inBlock: flag(raw.inBlock) };
1100
+ case "pop":
1101
+ return { kind: "pop" };
1102
+ case "assign":
1103
+ if (typeof raw.name !== "string") throw new Error("bad item");
1104
+ return { kind: "assign", name: raw.name, value: valueOf(raw.value) };
1105
+ case "shell":
1106
+ if (typeof raw.name !== "string" || typeof raw.raw !== "string") throw new Error("bad item");
1107
+ return { kind: "shell", name: raw.name, args: list(raw.args).map(valueOf), raw: raw.raw, inPipeline: flag(raw.inPipeline), ordered: flag(raw.ordered) };
1108
+ case "dynamic":
1109
+ if (raw.text != null && typeof raw.text !== "string" || !Array.isArray(raw.args)) throw new Error("bad item");
1110
+ return { kind: "dynamic", text: typeof raw.text === "string" ? raw.text : null, args: raw.args.map(valueOf) };
1111
+ default:
1112
+ throw new Error("bad item");
1113
+ }
1114
+ }
1115
+ function parse(stdout) {
1116
+ const parsed = JSON.parse(stdout);
1117
+ if (!isRecord2(parsed) || parsed.errors !== false || !Array.isArray(parsed.items) || !Array.isArray(parsed.unstable) || typeof parsed.anyVariable !== "boolean") throw new Error("bad answer");
1118
+ const unstable = parsed.unstable.map((name2) => {
1119
+ if (typeof name2 !== "string") throw new Error("bad answer");
1120
+ return baseName(name2);
1121
+ });
1122
+ return { items: parsed.items.map(itemOf), unstable: new Set(unstable), anyVariable: parsed.anyVariable };
1123
+ }
1124
+ var baseName = (name2) => name2.toLowerCase().replace(/^(?:script|local|global|private|using|variable):/, "");
1125
+ function fileSystemPath(value) {
1126
+ const qualified = /^(?:[\w.]+\\)?(\w+)::/.exec(value);
1127
+ if (!qualified) return value;
1128
+ return qualified[1].toLowerCase() === "filesystem" ? value.slice(qualified[0].length) : null;
1129
+ }
1130
+ function classify3(value, literal, cwd, ctx) {
1131
+ const text = fileSystemPath(value);
1132
+ if (text === null) return {};
1133
+ const drive = /^([A-Za-z][A-Za-z0-9_]+):/.exec(text);
1134
+ if (drive) return OTHER_DRIVES.has(drive[1].toLowerCase()) ? {} : { unresolvable: `a path on a PowerShell drive that cannot be worked out (${value})` };
1135
+ let base = text;
1136
+ let pattern = null;
1137
+ if (!literal && /[*?[]/.test(text)) {
1138
+ const split = splitWindowsPattern(text, /[*?[]/);
1139
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${value})` };
1140
+ base = split.dir;
1141
+ pattern = split.pattern;
1142
+ }
1143
+ const resolved = resolveWindowsPath(base, cwd, ctx.home);
1144
+ if ("unresolvable" in resolved) return { unresolvable: resolved.unresolvable };
1145
+ if ("unc" in resolved) return { real: { shown: value, path: null } };
1146
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
1147
+ }
1148
+ function folderOf(value, literal, cwd, home) {
1149
+ const text = fileSystemPath(value);
1150
+ if (text === null || text === "-" || text === "+" || /^[A-Za-z][A-Za-z0-9_]+:/.test(text) || !literal && /[*?[]/.test(text)) return null;
1151
+ const resolved = resolveWindowsPath(text, cwd, home);
1152
+ return "path" in resolved ? resolved.path : null;
1153
+ }
1154
+ async function readPowerShell(source, ctx, run) {
1155
+ const out = emptyRead();
1156
+ if (!POWERSHELL_WORDS.test(source)) return out;
1157
+ const excerpt = source.slice(0, EXCERPT_CHARS2);
1158
+ const failed = () => ({ ...emptyRead(), failed: [excerpt] });
1159
+ try {
1160
+ const stdout = await run(source);
1161
+ if (stdout === null) return failed();
1162
+ const { items, unstable, anyVariable } = parse(stdout);
1163
+ const assigned = /* @__PURE__ */ new Map();
1164
+ for (const item of items) if (item.kind === "assign") assigned.set(baseName(item.name), (assigned.get(baseName(item.name)) ?? 0) + 1);
1165
+ const moves = items.some((item) => item.kind === "cd" || item.kind === "pop");
1166
+ const processFolder = /CurrentDirectory/i.test(source) ? null : ctx.cwd;
1167
+ const vars = /* @__PURE__ */ new Map();
1168
+ for (const name2 of KNOWN_ENV2) {
1169
+ const value = ctx.env[name2];
1170
+ if (value) vars.set(`env:${name2.toLowerCase()}`, value);
1171
+ }
1172
+ vars.set("home", ctx.home);
1173
+ let cwd = ctx.cwd;
1174
+ const variable = (name2, here) => {
1175
+ const key = baseName(name2);
1176
+ if (anyVariable || unstable.has(key)) return null;
1177
+ if (key === "pwd") return assigned.has("pwd") ? null : here;
1178
+ return vars.get(key) ?? null;
1179
+ };
1180
+ const textOf = (value, here = cwd) => {
1181
+ if (!value) return null;
1182
+ if ("const" in value) return value.const;
1183
+ if ("variable" in value) return variable(value.variable, here);
1184
+ if ("expand" in value) {
1185
+ let text = "";
1186
+ for (const part of value.expand) {
1187
+ const piece = "const" in part ? part.const : variable(part.variable, here);
1188
+ if (piece === null) return null;
1189
+ text += piece;
1190
+ }
1191
+ return text;
1192
+ }
1193
+ return null;
1194
+ };
1195
+ const deny = (why) => ({ ...out, deny: denyReason("powershell", why) });
1196
+ for (const item of items) {
1197
+ if (item.kind === "assign") {
1198
+ const key = baseName(item.name);
1199
+ vars.set(key, assigned.get(key) === 1 ? textOf(item.value) : null);
1200
+ } else if (item.kind === "pop") {
1201
+ cwd = null;
1202
+ } else if (item.kind === "cd") {
1203
+ const to = item.inBlock ? null : textOf(item.path);
1204
+ cwd = to === null ? null : folderOf(to, item.literal, cwd, ctx.home);
1205
+ } else if (item.kind === "shell") {
1206
+ if (item.inPipeline && DELETE_WORD.test(source)) return deny(`pipeline input feeding a shell (${item.raw})`);
1207
+ const here = item.ordered || !moves ? cwd : null;
1208
+ const args = item.args.map((a) => textOf(a, here)).map((t) => t !== null && item.name !== "cmd" && t.includes('"') ? null : t);
1209
+ out.shells.push({ name: item.name, args, cwd: here, raw: item.raw });
1210
+ } else if (item.kind === "dynamic") {
1211
+ const values = item.text === null ? null : item.args.map((a) => textOf(a));
1212
+ const code = values === null || values.includes(null) ? source : [item.text, ...values].join(" ");
1213
+ if (DELETE_WORD.test(code) && !out.failed.length) out.failed.push(excerpt);
1214
+ } else {
1215
+ if (item.whatIf) continue;
1216
+ if (item.bindError) {
1217
+ if (!out.failed.length) out.failed.push(excerpt);
1218
+ continue;
1219
+ }
1220
+ if (item.inPipeline) return deny("pipeline input feeding Remove-Item");
1221
+ const here = item.ordered || !moves ? cwd : null;
1222
+ const values = list(item.literalPath ?? item.path).flatMap((v) => "array" in v ? v.array : [v]);
1223
+ for (const v of values) {
1224
+ if ("dynamic" in v || "array" in v) return deny(`an expression that cannot be worked out (${"dynamic" in v ? v.dynamic : "a nested list"})`);
1225
+ const text = textOf(v, here);
1226
+ if (text === null) return deny(`a variable that cannot be worked out (${"variable" in v ? `$${v.variable}` : "expand" in v ? v.text : ""})`);
1227
+ const c = classify3(text, item.literal, item.dotnet ? processFolder : here, ctx);
1228
+ if (c.unresolvable) return deny(c.unresolvable);
1229
+ if (c.real) out.targets.push(c.real);
1230
+ }
1231
+ }
1232
+ }
1233
+ return out;
1234
+ } catch {
1235
+ return failed();
1236
+ }
1237
+ }
1238
+
1239
+ // src/rm/dispatch.ts
1240
+ var MAX_NESTING = 3;
1241
+ var POWERSHELL_VALUE_PARAMS = [["executionpolicy", 2], ["windowstyle", 2], ["outputformat", 2], ["inputformat", 2], ["workingdirectory", 2], ["configurationname", 6], ["settingsfile", 2], ["version", 2], ["psconsolefile", 2], ["custompipename", 2]];
1242
+ var POWERSHELL_VALUE_ALIASES = ["ep", "ex", "w", "o", "of", "if", "wd", "v"];
1243
+ var CMD_RUN = /^\/{1,2}(?:[a-z]+\/)*[ck](.*)$/is;
1244
+ var paramName = (arg) => arg.replace(/^(?:--|-|\/)/, "").toLowerCase();
1245
+ var isParam = (arg) => /^(?:--|-|\/)[A-Za-z]/.test(arg);
1246
+ var startsWord = (name2, full, min) => name2.length >= min && full.startsWith(name2);
1247
+ function cmdLine(args) {
1248
+ const line = args.map((a) => /\s/.test(a) && !/^".*"$/.test(a) ? `"${a}"` : a).join(" ");
1249
+ const last = line.lastIndexOf('"');
1250
+ return line.startsWith('"') && last > 0 ? `${line.slice(1, last)}${line.slice(last + 1)}` : line;
1251
+ }
1252
+ function bashScript(args) {
1253
+ let hasC = false;
1254
+ let i = 0;
1255
+ for (; i < args.length; i++) {
1256
+ const a = args[i];
1257
+ if (a === null) return { shell: "bash", script: null };
1258
+ if (a === "--") {
1259
+ i++;
1260
+ break;
1261
+ }
1262
+ if (!/^[-+]/.test(a)) break;
1263
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(a)) hasC = true;
1264
+ if (/^[-+][oO]$/.test(a) || a === "--rcfile" || a === "--init-file") i++;
1265
+ }
1266
+ if (!hasC || i >= args.length) return null;
1267
+ return { shell: "bash", script: args[i] };
1268
+ }
1269
+ function powerShellScript(name2, args) {
1270
+ let moved = false;
1271
+ const found = (script) => ({ shell: "powershell", script, ...moved ? { moved } : {} });
1272
+ for (let i = 0; i < args.length; i++) {
1273
+ const a = args[i];
1274
+ if (a === null) return found(null);
1275
+ if (a === "-") return null;
1276
+ if (!isParam(a)) {
1277
+ if (name2 === "pwsh") return null;
1278
+ const rest = args.slice(i);
1279
+ return found(rest.includes(null) ? null : rest.join(" "));
1280
+ }
1281
+ const p = paramName(a);
1282
+ if (p === "e" || p === "ec" || startsWord(p, "encodedcommand", 2)) {
1283
+ const encoded = args[i + 1];
1284
+ if (encoded === void 0) return null;
1285
+ return found(encoded === null ? null : Buffer.from(encoded, "base64").toString("utf16le"));
1286
+ }
1287
+ if (p === "cwa" || startsWord(p, "command", 1) || startsWord(p, "commandwithargs", 8)) {
1288
+ const rest = args.slice(i + 1);
1289
+ if (!rest.length || rest[0] === "-") return null;
1290
+ return found(rest.includes(null) ? null : rest.join(" "));
1291
+ }
1292
+ if (p === "f" || startsWord(p, "file", 2)) return null;
1293
+ if (POWERSHELL_VALUE_ALIASES.includes(p) || POWERSHELL_VALUE_PARAMS.some(([full, min]) => startsWord(p, full, min))) {
1294
+ if (p === "wd" || startsWord(p, "workingdirectory", 2)) moved = true;
1295
+ i++;
1296
+ }
1297
+ }
1298
+ return null;
1299
+ }
1300
+ function nestedScript({ name: name2, args }) {
1301
+ if (name2 === "sh" || name2 === "bash") return bashScript(args);
1302
+ if (name2 === "powershell" || name2 === "pwsh") return powerShellScript(name2, args);
1303
+ if (name2 === "cmd") {
1304
+ const at = args.findIndex((a) => a !== null && CMD_RUN.test(a));
1305
+ if (at === -1) return null;
1306
+ const glued = CMD_RUN.exec(args[at])[1];
1307
+ const rest = [...glued ? [glued] : [], ...args.slice(at + 1)];
1308
+ return rest.includes(null) ? { shell: "cmd", script: null } : { shell: "cmd", script: cmdLine(rest) };
1309
+ }
1310
+ return null;
1311
+ }
1312
+ function readsStdin({ name: name2, args }) {
1313
+ if (args.includes(null)) return false;
1314
+ const given = args;
1315
+ if (name2 === "sh" || name2 === "bash") {
1316
+ for (let i = 0; i < given.length; i++) {
1317
+ const a = given[i];
1318
+ if (a === "--") return i + 1 === given.length;
1319
+ if (!/^[-+]/.test(a)) return false;
1320
+ if (/^-[A-Za-z]*s[A-Za-z]*$/.test(a)) return true;
1321
+ if (/^[-+][oO]$/.test(a) || a === "--rcfile" || a === "--init-file") i++;
1322
+ }
1323
+ return true;
1324
+ }
1325
+ if (name2 === "powershell" || name2 === "pwsh") {
1326
+ for (let i = 0; i < given.length; i++) {
1327
+ const a = given[i];
1328
+ if (a === "-") return true;
1329
+ if (!isParam(a)) return false;
1330
+ const p = paramName(a);
1331
+ if (p === "cwa" || startsWord(p, "command", 1) || startsWord(p, "commandwithargs", 8)) return given[i + 1] === void 0 || given[i + 1] === "-";
1332
+ if (p === "f" || startsWord(p, "file", 2) || p === "e" || p === "ec" || startsWord(p, "encodedcommand", 2)) return false;
1333
+ if (POWERSHELL_VALUE_ALIASES.includes(p) || POWERSHELL_VALUE_PARAMS.some(([full, min]) => startsWord(p, full, min))) i++;
1334
+ }
1335
+ return true;
1336
+ }
1337
+ return name2 === "cmd" && !given.some((a) => CMD_RUN.test(a));
1338
+ }
1339
+ function readShell(shell, script, ctx, run) {
1340
+ if (shell === "bash") return readBash(script, ctx);
1341
+ if (shell === "cmd") return readCmd(script, ctx);
1342
+ return readPowerShell(script, ctx, run);
1343
+ }
1344
+ async function readNested(shell, script, ctx, run, depth) {
1345
+ const result = await readShell(shell, script, ctx, run);
1346
+ if (result.deny !== null) return result;
1347
+ const merged = { deny: null, targets: [...result.targets], failed: [...result.failed], shells: [] };
1348
+ for (const call of result.shells) {
1349
+ const nested = nestedScript(call);
1350
+ if (nested === null) {
1351
+ if ((call.piped || readsStdin(call)) && DELETE_WORDS.test(script)) return { ...merged, deny: denyReason(shell, `a shell reading its script from stdin (${call.raw})`) };
1352
+ continue;
1353
+ }
1354
+ if (nested.script === null || depth >= MAX_NESTING) {
1355
+ if (!DELETE_WORDS.test(call.raw) && !DELETE_WORDS.test(nested.script ?? "")) continue;
1356
+ const why = nested.script === null ? `a ${call.name} script that cannot be worked out (${call.raw})` : `shells nested more than ${MAX_NESTING} deep (${call.raw})`;
1357
+ return { ...merged, deny: denyReason(shell, why) };
1358
+ }
1359
+ const inner = await readNested(nested.shell, nested.script, { ...ctx, cwd: nested.moved ? null : call.cwd }, run, depth + 1);
1360
+ if (inner.deny !== null) return { ...merged, deny: inner.deny };
1361
+ merged.targets.push(...inner.targets);
1362
+ merged.failed.push(...inner.failed);
1363
+ }
1364
+ return merged;
1365
+ }
1366
+ async function decideTool(tool, command, ctx, run) {
1367
+ return decisionOf(await readNested(tool === "Bash" ? "bash" : "powershell", command, ctx, run, 0));
1368
+ }
1369
+
1370
+ // src/rm/facts.ts
1371
+ import fs7 from "fs";
1372
+ import path8 from "path";
1373
+ import { spawnSync as spawnSync2 } from "child_process";
1374
+
1375
+ // src/context/transcript.ts
1376
+ import fs6 from "fs";
1377
+ var TAIL_START_BYTES = 1024 * 1024;
1378
+ var TAIL_MAX_BYTES = 8 * 1024 * 1024;
1379
+ function isRecord3(value) {
1380
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1381
+ }
1382
+ function parseLines(text) {
1383
+ const entries = [];
1384
+ for (const line of text.split("\n")) {
1385
+ if (!line.trim()) continue;
1386
+ try {
1387
+ const value = JSON.parse(line);
1388
+ if (isRecord3(value)) entries.push(value);
1389
+ } catch {
1390
+ }
1391
+ }
1392
+ return entries;
1393
+ }
1394
+
1395
+ // src/rm/facts.ts
1396
+ var SESSION_LOG_MAX_BYTES = 32 * 1024 * 1024;
1397
+ var CALLS_PER_TARGET = 3;
1398
+ var win4 = path8.win32;
1399
+ var DELETING = /\b(?:rm|rmdir|del|erase|rd|ri|Remove-Item|xargs)\b/i;
1400
+ var GIT_LOCATION_ENV = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"];
1401
+ var MOVES = ["cd", "pushd", "popd", "chdir", "set-location", "sl", "push-location", "pop-location"];
1402
+ var CHANGES_FOLDER = new RegExp(`(^|[\\s;&|(])(?:${MOVES.join("|")})(?=[\\s;&|)]|$)`, "i");
1403
+ var MOVING_TOOLS = /* @__PURE__ */ new Set(["EnterWorktree", "ExitWorktree"]);
1404
+ function isRecord4(value) {
1405
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1406
+ }
1407
+ function callOf(tool, input, result, cwd) {
1408
+ const text = (key) => typeof input[key] === "string" ? input[key] : "";
1409
+ switch (tool) {
1410
+ case "Write":
1411
+ return { tool, file: text("file_path"), text: text("content"), kind: result === "create" ? "create" : "change", cwd };
1412
+ case "Edit":
1413
+ return { tool, file: text("file_path"), text: text("new_string"), kind: "change", cwd };
1414
+ case "MultiEdit": {
1415
+ const edits = Array.isArray(input.edits) ? input.edits : [];
1416
+ return { tool, file: text("file_path"), text: edits.map((e) => isRecord4(e) && typeof e.new_string === "string" ? e.new_string : "").join("\n"), kind: "change", cwd };
1417
+ }
1418
+ case "NotebookEdit":
1419
+ return { tool, file: text("notebook_path"), text: text("new_source"), kind: "change", cwd };
1420
+ case "Read":
1421
+ return { tool, file: text("file_path"), text: "", kind: "read", cwd };
1422
+ case "Bash":
1423
+ case "PowerShell":
1424
+ return { tool, file: null, text: text("command"), kind: "shell", cwd };
1425
+ default:
1426
+ return null;
1427
+ }
1428
+ }
1429
+ function resultTypes(entries) {
1430
+ const types = /* @__PURE__ */ new Map();
1431
+ for (const entry of entries) {
1432
+ if (entry.type !== "user" || !isRecord4(entry.toolUseResult) || typeof entry.toolUseResult.type !== "string" || !isRecord4(entry.message)) continue;
1433
+ const content = entry.message.content;
1434
+ const block = Array.isArray(content) ? content[0] : void 0;
1435
+ if (isRecord4(block) && typeof block.tool_use_id === "string") types.set(block.tool_use_id, entry.toolUseResult.type);
1436
+ }
1437
+ return types;
1438
+ }
1439
+ function toolCalls(entries) {
1440
+ const results = resultTypes(entries);
1441
+ const calls = [];
1442
+ let message;
1443
+ let moved = false;
1444
+ for (const entry of entries) {
1445
+ if (entry.type !== "assistant" || entry.isSidechain === true || !isRecord4(entry.message) || !Array.isArray(entry.message.content)) continue;
1446
+ if (entry.message.id === void 0 || entry.message.id !== message) moved = false;
1447
+ message = entry.message.id;
1448
+ const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
1449
+ for (const block of entry.message.content) {
1450
+ if (!isRecord4(block) || block.type !== "tool_use" || typeof block.name !== "string" || !isRecord4(block.input)) continue;
1451
+ if (MOVING_TOOLS.has(block.name)) moved = true;
1452
+ const call = callOf(block.name, block.input, typeof block.id === "string" ? results.get(block.id) : void 0, moved ? null : cwd);
1453
+ if (!call) continue;
1454
+ calls.push(call);
1455
+ if (call.kind === "shell" && CHANGES_FOLDER.test(call.text)) moved = true;
1456
+ }
1457
+ }
1458
+ return calls;
1459
+ }
1460
+ function readSessionLog(file, maxBytes = SESSION_LOG_MAX_BYTES) {
1461
+ let text;
1462
+ try {
1463
+ if (fs7.statSync(file).size > maxBytes) return null;
1464
+ text = fs7.readFileSync(file, "utf8");
1465
+ } catch {
1466
+ return null;
1467
+ }
1468
+ const entries = parseLines(text);
1469
+ const first = entries.find((e) => typeof e.timestamp === "string" && !Number.isNaN(Date.parse(e.timestamp)));
1470
+ return first ? { startedAt: Date.parse(first.timestamp), calls: toolCalls(entries) } : null;
1471
+ }
1472
+ function normalized(text) {
1473
+ return text.replace(/\\/g, "/").replace(/(^|[\s"'=(>])\/([A-Za-z])\//g, "$1$2:/").toLowerCase();
1474
+ }
1475
+ var escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1476
+ function shellRefs(target, cwd) {
1477
+ const refs = [`(?<![\\w.-])${escaped(normalized(target))}(?![\\w.-])`];
1478
+ const relative = cwd === null ? "" : win4.relative(cwd, target);
1479
+ if (relative && !relative.startsWith("..") && !win4.isAbsolute(relative)) {
1480
+ refs.push(`(?<![\\w.\\-/:])(?:\\./)?${escaped(normalized(relative))}(?![\\w.-])`);
1481
+ }
1482
+ return refs;
1483
+ }
1484
+ function namesTarget(call, target, folder) {
1485
+ const wanted = normalized(target);
1486
+ if (call.file !== null) {
1487
+ const file = normalized(call.file);
1488
+ return file === wanted || folder && file.startsWith(`${wanted}/`);
1489
+ }
1490
+ if (DELETING.test(call.text)) return false;
1491
+ const text = normalized(call.text);
1492
+ return shellRefs(target, call.cwd).some((ref) => new RegExp(ref).test(text));
1493
+ }
1494
+ function shellCommands(text) {
1495
+ const commands = [];
1496
+ let current = { words: [], writes: [] };
1497
+ let word = null;
1498
+ let redirect = null;
1499
+ const endWord = () => {
1500
+ if (word === null) return;
1501
+ if (redirect === null) current.words.push(word);
1502
+ else if (redirect === ">" || redirect === "&>") current.writes.push(word);
1503
+ word = null;
1504
+ redirect = null;
1505
+ };
1506
+ const endCommand = () => {
1507
+ endWord();
1508
+ commands.push(current);
1509
+ current = { words: [], writes: [] };
1510
+ };
1511
+ for (let i = 0; i < text.length; i++) {
1512
+ const c = text[i];
1513
+ if (c === "'" || c === '"') {
1514
+ const close = text.indexOf(c, i + 1);
1515
+ const stop = close === -1 ? text.length : close;
1516
+ word = (word ?? "") + text.slice(i + 1, stop);
1517
+ i = stop;
1518
+ } else if (c === "\\") {
1519
+ word = (word ?? "") + (text[i + 1] ?? "");
1520
+ i += 1;
1521
+ } else if (c === "#" && word === null) {
1522
+ while (i + 1 < text.length && text[i + 1] !== "\n") i += 1;
1523
+ } else if (c === " " || c === " " || c === "\r") {
1524
+ endWord();
1525
+ } else if (c === "\n" || c === ";" || c === "|" || c === "(" || c === ")" || c === "&" && text[i + 1] !== ">") {
1526
+ endCommand();
1527
+ } else if (c === ">" || c === "<" || c === "&") {
1528
+ if (word !== null && /^\d+$/.test(word)) word = null;
1529
+ endWord();
1530
+ let op = c;
1531
+ while (text[i + 1] === ">" || text[i + 1] === "&" || text[i + 1] === "|") op += text[++i];
1532
+ redirect = op;
1533
+ } else {
1534
+ word = (word ?? "") + c;
1535
+ }
1536
+ }
1537
+ endCommand();
1538
+ return commands;
1539
+ }
1540
+ var pathWord = (word) => word.replace(/\\/g, "/").replace(/^\/([A-Za-z])\//, "$1:/").replace(/^\.\//, "").replace(/\/+$/, "").toLowerCase();
1541
+ function createsTarget(call, target) {
1542
+ if (call.kind !== "shell") return call.kind === "create";
1543
+ const full = pathWord(target);
1544
+ const relative = call.cwd === null ? "" : win4.relative(call.cwd, target);
1545
+ const short = relative && !relative.startsWith("..") && !win4.isAbsolute(relative) ? pathWord(relative) : null;
1546
+ let moved = false;
1547
+ const isTarget = (word) => word !== void 0 && (pathWord(word) === full || !moved && pathWord(word) === short);
1548
+ for (const { words, writes } of shellCommands(stripHeredocs(call.text))) {
1549
+ if (writes.some(isTarget)) return true;
1550
+ const start = words.findIndex((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
1551
+ const args = start === -1 ? [] : words.slice(start);
1552
+ const name2 = win4.basename((args[0] ?? "").replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
1553
+ if ((name2 === "touch" || name2 === "mkdir") && args.slice(1).some(isTarget)) return true;
1554
+ if (name2 === "curl" && args.some((w, i) => (w === "-o" || w === "--output") && isTarget(args[i + 1]))) return true;
1555
+ if (MOVES.includes(name2)) moved = true;
1556
+ }
1557
+ return false;
1558
+ }
1559
+ function gatherFacts(target, session, probe, maxFiles) {
1560
+ const info = probe.info(target);
1561
+ if (!info) return null;
1562
+ const naming = session.calls.filter((call) => namesTarget(call, target, info.folder));
1563
+ return {
1564
+ path: target,
1565
+ folder: info.folder,
1566
+ bornAt: info.bornAt,
1567
+ listing: info.folder ? probe.list(target, maxFiles) : null,
1568
+ tracked: probe.tracked(target),
1569
+ ignored: info.folder && probe.ignored(target),
1570
+ // Write, Edit and sed -i replace a file, which resets its Windows birth time, so the birth time alone cannot tell
1571
+ // a new file from an edited one: the first call that named the target must be the one that visibly created it.
1572
+ existedBefore: naming.length === 0 || !createsTarget(naming[0], target),
1573
+ createdBy: naming.length <= CALLS_PER_TARGET ? naming : [naming[0], ...naming.slice(1 - CALLS_PER_TARGET)]
1574
+ };
1575
+ }
1576
+ function candidateKind(facts, startedAt) {
1577
+ const fresh = (bornAt2) => bornAt2 >= startedAt;
1578
+ const freshInside = !facts.folder || facts.listing !== null && !facts.listing.more && facts.listing.entries.every((e) => fresh(e.bornAt));
1579
+ const repo = facts.folder && (facts.listing === null || facts.listing.repo);
1580
+ if (!facts.tracked && !facts.existedBefore && facts.createdBy.length > 0 && fresh(facts.bornAt) && freshInside && !repo) return "session";
1581
+ if (facts.folder && facts.ignored && !repo) return "ignored";
1582
+ return null;
1583
+ }
1584
+ var bornAt = (stat) => stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
1585
+ function git(args, extraEnv = {}) {
1586
+ const env = { ...process.env, LC_ALL: "C", ...extraEnv };
1587
+ for (const name2 of GIT_LOCATION_ENV) delete env[name2];
1588
+ return spawnSync2("git", args, { encoding: "utf8", timeout: 3e3, windowsHide: true, env });
1589
+ }
1590
+ var realFactProbe = {
1591
+ info(p) {
1592
+ try {
1593
+ const stat = fs7.lstatSync(p);
1594
+ return { folder: stat.isDirectory(), bornAt: bornAt(stat) };
1595
+ } catch {
1596
+ return null;
1597
+ }
1598
+ },
1599
+ list(dir, limit) {
1600
+ const entries = [];
1601
+ let seen = 0;
1602
+ let more = false;
1603
+ let repo = false;
1604
+ const walk = (folder, prefix) => {
1605
+ for (const item of fs7.readdirSync(folder, { withFileTypes: true })) {
1606
+ if (more) return;
1607
+ if (seen >= limit) {
1608
+ more = true;
1609
+ return;
1610
+ }
1611
+ seen += 1;
1612
+ if (item.name === ".git") repo = true;
1613
+ const full = path8.join(folder, item.name);
1614
+ const name2 = prefix ? `${prefix}/${item.name}` : item.name;
1615
+ if (item.isDirectory()) walk(full, name2);
1616
+ else entries.push({ name: name2, bornAt: bornAt(fs7.lstatSync(full)) });
1617
+ }
1618
+ };
1619
+ try {
1620
+ walk(dir, "");
1621
+ if (!repo) {
1622
+ repo = fs7.readdirSync(dir, { withFileTypes: true }).some((item) => item.name === ".git" || item.isDirectory() && fs7.existsSync(path8.join(dir, item.name, ".git")));
1623
+ }
1624
+ return { entries, more, repo };
1625
+ } catch {
1626
+ return null;
1627
+ }
1628
+ },
1629
+ tracked(p) {
1630
+ if (fs7.existsSync(path8.join(p, ".git"))) return true;
1631
+ const r = git(["-C", win4.dirname(p), "ls-files", "--", p], { GIT_ICASE_PATHSPECS: "1" });
1632
+ if (r.status === 0) return r.stdout.trim() !== "";
1633
+ return !(r.status === 128 && /not a git repository/i.test(r.stderr ?? ""));
1634
+ },
1635
+ ignored(p) {
1636
+ if (fs7.existsSync(path8.join(p, ".git"))) return false;
1637
+ return git(["-C", win4.dirname(p), "check-ignore", "-q", "--", p]).status === 0;
1638
+ }
1639
+ };
1640
+
1641
+ // src/rm/judge.ts
1642
+ import path9 from "path";
1643
+ var CALL_EXCERPT_CHARS = 600;
1644
+ var SAMPLE_FILES = 10;
1645
+ var RM_QUESTIONS = {
1646
+ session: {
1647
+ type: "noul",
1648
+ instructions: "Is `target` a throwaway file or folder that Claude made during this session only to try something out, so that deleting it with `command` loses nothing the user wants? `created_by` lists the tool calls of this session that name it; `git` says it is not tracked by git.",
1649
+ criteria: {
1650
+ true: [
1651
+ "A scratch or probe file for a one-off check: captured output, a dump or log, a quick test script, a sample input made up for a trial.",
1652
+ "Nothing in the tool calls treats it as part of the work the user asked for."
1653
+ ],
1654
+ false: [
1655
+ "Part of the work: source code, tests in the project test folders, documentation, configuration, migrations, or data or output the user asked for.",
1656
+ "It is unclear what it is for, or it may still be wanted."
1657
+ ]
1658
+ }
1659
+ },
1660
+ ignored: {
1661
+ type: "noul",
1662
+ instructions: "Is `target` a folder of generated output or cache that a build, install or tool run makes again, so that deleting it with `command` loses nothing? `git` says git ignores it; `sample` lists some of its entries.",
1663
+ criteria: {
1664
+ true: "Build output, compiled files, bundles, a dependency or package cache, test or coverage output, or a tool cache that is recreated on the next run.",
1665
+ false: "Hand-made or downloaded content, user data, local configuration, secrets, logs worth keeping, or anything that cannot simply be made again; or it is unclear."
1666
+ }
1667
+ }
1668
+ };
1669
+ var PASS = {
1670
+ ko: {
1671
+ session: "Jev\uAC00 \uC774 \uC138\uC158\uC758 \uC2DC\uD5D8 \uD30C\uC77C\uB85C \uD310\uB2E8\uD574 \uBB3B\uC9C0 \uC54A\uACE0 \uC9C0\uC6C1\uB2C8\uB2E4",
1672
+ ignored: "Jev\uAC00 \uB2E4\uC2DC \uB9CC\uB4E4 \uC218 \uC788\uB294 \uD3F4\uB354\uB85C \uD310\uB2E8\uD574 \uBB3B\uC9C0 \uC54A\uACE0 \uC9C0\uC6C1\uB2C8\uB2E4",
1673
+ mixed: "Jev\uAC00 \uC9C0\uC6CC\uB3C4 \uB418\uB294 \uB300\uC0C1\uC73C\uB85C \uD310\uB2E8\uD574 \uBB3B\uC9C0 \uC54A\uACE0 \uC9C0\uC6C1\uB2C8\uB2E4"
1674
+ },
1675
+ en: {
1676
+ session: "Jev judged it a throwaway test file of this session, so it is deleted without asking",
1677
+ ignored: "Jev judged it a folder that is made again, so it is deleted without asking",
1678
+ mixed: "Jev judged these safe to delete, so they are deleted without asking"
1679
+ }
1680
+ };
1681
+ var NOTE = {
1682
+ ko: { session: "\uC2DC\uD5D8\uC6A9 \uD30C\uC77C\uC77C \uD655\uB960", ignored: "\uB2E4\uC2DC \uB9CC\uB4E4 \uC218 \uC788\uC744 \uD655\uB960", none: "\uD310\uB2E8 \uBABB \uD568" },
1683
+ en: { session: "chance it is a test file", ignored: "chance it can be made again", none: "no answer" }
1684
+ };
1685
+ function rmJevRequest(kind, facts, command, description, clean = (text) => text) {
1686
+ const target = { path: facts.path, kind: facts.folder ? "folder" : "file" };
1687
+ if (facts.listing) {
1688
+ target.files = facts.listing.more ? `more than ${facts.listing.entries.length}` : facts.listing.entries.length;
1689
+ target.sample = facts.listing.entries.slice(0, SAMPLE_FILES).map((e) => e.name);
1690
+ }
1691
+ const state = { target, command: clean(command), description: clean(description) };
1692
+ if (kind === "session") {
1693
+ state.created_by = facts.createdBy.map((c) => ({ tool: c.tool, input: clean(c.file ? `${c.file}
1694
+ ${c.text}` : c.text).slice(0, CALL_EXCERPT_CHARS) }));
1695
+ state.git = "untracked";
1696
+ } else {
1697
+ state.git = "ignored";
1698
+ }
1699
+ return { state, questions: { ok: RM_QUESTIONS[kind] } };
1700
+ }
1701
+ var name = (item) => path9.win32.basename(item.path);
1702
+ var prob = (p) => p.toFixed(2);
1703
+ function lifts(items, threshold) {
1704
+ return items.length > 0 && items.every((item) => item.p !== null && item.p >= threshold);
1705
+ }
1706
+ function passMessage(lang, items) {
1707
+ const kinds = new Set(items.map((i) => i.kind));
1708
+ const phrase = PASS[lang][kinds.size === 1 ? items[0].kind : "mixed"];
1709
+ return `[jev-advisor] ${phrase}: ${items.map((i) => `${name(i)} (${prob(i.p ?? 0)})`).join(", ")}`;
1710
+ }
1711
+ function askNote(lang, items) {
1712
+ if (items.length === 1) {
1713
+ const [item] = items;
1714
+ return ` \xB7 Jev: ${item.p === null ? NOTE[lang].none : `${NOTE[lang][item.kind]} ${prob(item.p)}`}`;
1715
+ }
1716
+ return ` \xB7 Jev: ${items.map((i) => `${name(i)} ${i.p === null ? "?" : prob(i.p)}`).join(", ")}`;
1717
+ }
1718
+
415
1719
  // src/rm/run.ts
416
- function runRmHook(raw, deps) {
417
- if (!readConfig(deps.home).rm.enabled) return null;
1720
+ var MAX_JEV_TARGETS = 5;
1721
+ var READ_BUDGET_MS = 9e3;
1722
+ var FACTS_BUDGET_MS = 5e3;
1723
+ var JEV_PATH_BUDGET_MS = 12e3;
1724
+ var MIN_JEV_MS = 1e3;
1725
+ async function judgeTargets(targets, input, command, key, keys, config, deps, elapsed) {
1726
+ if (targets.length === 0 || targets.length > MAX_JEV_TARGETS || targets.some((t) => t.path === null)) return null;
1727
+ if (typeof input.transcript_path !== "string") return null;
1728
+ const session = readSessionLog(input.transcript_path);
1729
+ if (!session) return null;
1730
+ const found = [];
1731
+ for (const target of targets) {
1732
+ if (elapsed() > FACTS_BUDGET_MS) return null;
1733
+ const facts = gatherFacts(target.path, session, deps.facts ?? realFactProbe, config.rm.maxDirFiles);
1734
+ const kind = facts ? candidateKind(facts, session.startedAt) : null;
1735
+ if (!facts || !kind) return null;
1736
+ found.push({ target, facts, kind });
1737
+ }
1738
+ const timeoutMs = Math.min(JEV_TIMEOUT_MS, JEV_PATH_BUDGET_MS - elapsed());
1739
+ if (timeoutMs < MIN_JEV_MS) return null;
1740
+ const description = typeof input.tool_input?.description === "string" ? input.tool_input.description : "";
1741
+ const answered = await Promise.all(
1742
+ found.map(async ({ kind, facts }) => {
1743
+ const request = rmJevRequest(kind, facts, command, description, (text) => withoutKeys(text, keys));
1744
+ return { request, result: await callJev(request, { key, fetchFn: deps.fetchFn, timeoutMs }) };
1745
+ })
1746
+ );
1747
+ return {
1748
+ items: found.map(({ target, kind }, i) => {
1749
+ const { result } = answered[i];
1750
+ const p = "error" in result ? null : result.answers.ok;
1751
+ return { path: target.path, shown: target.shown, kind, p: typeof p === "number" ? p : null };
1752
+ }),
1753
+ calls: answered.map(({ request, result }) => ({ state: request.state, ...result }))
1754
+ };
1755
+ }
1756
+ async function runRmHook(raw, deps) {
1757
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1758
+ const started = now().getTime();
1759
+ const config = readConfig(deps.home);
1760
+ if (!config.rm.enabled) return null;
418
1761
  const input = JSON.parse(raw);
419
1762
  if (typeof input !== "object" || input === null) return null;
420
1763
  const command = input.tool_input?.command;
421
- if (input.tool_name !== "Bash" || typeof command !== "string") return null;
422
- const tmpdirs = [...new Set([deps.tmpdir, deps.env.TEMP, deps.env.TMP].filter((t) => Boolean(t)).map((t) => path4.win32.resolve(t)))];
1764
+ const tool = input.tool_name;
1765
+ if (tool !== "Bash" && tool !== "PowerShell" || typeof command !== "string") return null;
1766
+ const tmpdirs = [...new Set([deps.tmpdir, deps.env.TEMP, deps.env.TMP].filter((t) => Boolean(t)).map((t) => path10.win32.resolve(t)))];
423
1767
  const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : deps.cwd;
424
- const result = decide({ command, cwd, home: deps.home, tmpdirs, env: deps.env, probe: deps.probe });
425
- return result ? hookOutput(result) : null;
1768
+ const ctx = { cwd, home: deps.home, tmpdirs, env: deps.env, probe: deps.probe };
1769
+ const powershell = deps.powershell ?? realPowerShell(deps.env);
1770
+ const read = (source) => now().getTime() - started >= READ_BUDGET_MS ? Promise.resolve(null) : powershell(source);
1771
+ const result = await decideTool(tool, command, ctx, read);
1772
+ if (!result) return null;
1773
+ if (result.decision !== "ask" || !result.targets || !config.rm.jev) return hookOutput(result);
1774
+ const key = readJevKey(deps.env, config.keyFile);
1775
+ if (!key) return hookOutput(result);
1776
+ const keys = jevKeys(deps.env, config.keyFile);
1777
+ let judged = null;
1778
+ try {
1779
+ judged = await judgeTargets(result.targets, input, command, key, keys, config, deps, () => now().getTime() - started);
1780
+ } catch {
1781
+ judged = null;
1782
+ }
1783
+ if (!judged) return hookOutput(result);
1784
+ const pass = lifts(judged.items, config.rm.throwawayYes);
1785
+ try {
1786
+ appendLog(
1787
+ deps.home,
1788
+ now(),
1789
+ {
1790
+ helper: "rm",
1791
+ event: "pre_tool_use",
1792
+ sessionId: typeof input.session_id === "string" ? input.session_id : null,
1793
+ transcriptPath: input.transcript_path,
1794
+ tool,
1795
+ command,
1796
+ cwd,
1797
+ decision: pass ? "pass" : "ask",
1798
+ targets: judged.items,
1799
+ jev: judged.calls
1800
+ },
1801
+ keys
1802
+ );
1803
+ } catch {
1804
+ }
1805
+ if (pass) return JSON.stringify({ systemMessage: passMessage(config.lang, judged.items) });
1806
+ return hookOutput({ ...result, reason: `${result.reason}${askNote(config.lang, judged.items)}` });
426
1807
  }
427
1808
 
428
1809
  // src/rm/hook.ts
@@ -430,7 +1811,7 @@ async function main() {
430
1811
  let raw = "";
431
1812
  process.stdin.setEncoding("utf8");
432
1813
  for await (const chunk of process.stdin) raw += chunk;
433
- const out = runRmHook(raw, { home: os.homedir(), env: process.env, tmpdir: os.tmpdir(), cwd: process.cwd(), probe: realProbe });
1814
+ const out = await runRmHook(raw, { home: os.homedir(), env: process.env, tmpdir: os.tmpdir(), cwd: process.cwd(), probe: realProbe });
434
1815
  if (out) process.stdout.write(out);
435
1816
  }
436
1817
  main().catch(() => {