eminiar 0.13.2 → 0.13.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eminiar",
3
3
  "displayName": "Eminiar",
4
- "version": "0.13.2",
4
+ "version": "0.13.3",
5
5
  "description": "Local-first context, memory and knowledge graph for Claude Code: sandboxed execution, FTS5 + vector search, session continuity, code graph.",
6
6
  "author": {
7
7
  "name": "Ryan Dick"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eminiar",
3
3
  "displayName": "Eminiar",
4
- "version": "0.13.2",
4
+ "version": "0.13.3",
5
5
  "description": "Local-first context, memory and knowledge graph for coding agents: sandboxed execution, FTS5 + vector search, session continuity, code graph.",
6
6
  "author": {
7
7
  "name": "Ryan Dick"
package/README.md CHANGED
@@ -60,14 +60,17 @@ Everything runs locally in one SQLite database per project.
60
60
 
61
61
  `em_execute`, `em_batch_execute` and `em_execute_file` run shell, JavaScript and TypeScript, plus
62
62
  Python, Ruby, Go, PHP, Perl, R, Elixir, Rust and C# when they are installed. Output over 5 KB is
63
- indexed. Claude gets back only the sections that match its `intent`. Snippets obey your Claude
64
- Code `Bash` deny rules, and `em_execute_file` refuses a `path` that one of your `Read` deny rules
65
- matches ([which rule forms](docs/how-it-works.md#sandbox)).
63
+ indexed. Claude gets back only the sections that match its `intent`. Shell snippets are checked
64
+ against your Claude Code `Bash` deny and ask rules, and `em_execute_file` refuses a `path` that
65
+ one of your `Read` deny rules matches. Snippets in other languages are not rule-checked, and
66
+ the sandbox is not an isolation boundary
67
+ ([what is enforced](docs/how-it-works.md#what-the-sandbox-enforces)).
66
68
 
67
69
  Hooks route noisy commands for Claude. A Bash line is judged one command at a time: output that
68
- is already bounded (`| head`, `| wc`, `grep -l`, `cat` of a small file) and commands that change
69
- files stay native. `git log`, `git blame` and `git show` go to `em_history`, and a `Grep` for a
70
- symbol the graph knows goes to `em_symbols`. `em_stats` shows each routing decision and the rule
70
+ is already bounded (`| head`, `| wc`, `grep -l`, `cat` of a small file) and commands it
71
+ recognises as changing something (files, a cluster, a remote API) stay native. `git log`,
72
+ `git blame` and `git show` go to `em_history`, and a `Grep` for a symbol the graph knows goes to
73
+ `em_symbols`. `em_stats` shows each routing decision and the rule
71
74
  behind it, so you can see what was sent where.
72
75
 
73
76
  ### 🔎 Hybrid search — keyword and meaning together
package/dist/hook.js CHANGED
@@ -2197,9 +2197,9 @@ var preCompact = (input) => {
2197
2197
 
2198
2198
  // src/hooks/routing.ts
2199
2199
  init_esm_shims();
2200
- import { realpathSync as realpathSync2, statSync as statSync3 } from "fs";
2200
+ import { existsSync as existsSync7, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
2201
2201
  import { homedir as homedir2 } from "os";
2202
- import { extname, join as join7, relative, resolve as resolve2 } from "path";
2202
+ import { dirname as dirname3, extname, join as join7, relative as relative2, resolve as resolve3 } from "path";
2203
2203
 
2204
2204
  // src/permissions/settings.ts
2205
2205
  init_esm_shims();
@@ -2364,6 +2364,36 @@ function checkReadPath(rules, abs) {
2364
2364
  return more.length ? checkRead(rules, more) : asWritten;
2365
2365
  }
2366
2366
 
2367
+ // src/sandbox/file.ts
2368
+ init_esm_shims();
2369
+ import { existsSync as existsSync4, realpathSync as realpathSync2 } from "fs";
2370
+ import { isAbsolute, relative, resolve as resolve2 } from "path";
2371
+ function confinePath(root, target, given) {
2372
+ const abs = resolve2(root, target);
2373
+ const rules = given.root ? given : { ...given, root };
2374
+ refuse(checkRead(rules, [abs]), target);
2375
+ let real;
2376
+ try {
2377
+ real = realpathSync2(abs);
2378
+ } catch {
2379
+ throw new Error(`file not found: ${target}`);
2380
+ }
2381
+ refuse(checkReadPath(rules, abs), target);
2382
+ const rel = relative(root, real);
2383
+ const inside = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
2384
+ if (inside) return real;
2385
+ if (readMatcher(rules.allow, rules.root).test(real)) return real;
2386
+ throw new Error(
2387
+ `${target} is outside the project root; add a Read(/${real}) rule to permissions.allow to opt it in`
2388
+ );
2389
+ }
2390
+ function refusal(check, target) {
2391
+ return `refused by permission rule ${check.rule} (${check.kind}) on path: ${target}`;
2392
+ }
2393
+ function refuse(check, target) {
2394
+ if (!check.allowed) throw new Error(refusal(check, target));
2395
+ }
2396
+
2367
2397
  // src/session/routing-stats.ts
2368
2398
  init_esm_shims();
2369
2399
  var ROUTING_CATEGORY = "routing";
@@ -2386,7 +2416,7 @@ function recordRouting(db, sessionId, e) {
2386
2416
 
2387
2417
  // src/session/server-alive.ts
2388
2418
  init_esm_shims();
2389
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2419
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2390
2420
  import { join as join4 } from "path";
2391
2421
  function markersDir(ctx) {
2392
2422
  return join4(projectDir(ctx.id), "servers");
@@ -2401,7 +2431,7 @@ function processAlive(pid) {
2401
2431
  }
2402
2432
  function liveServers(ctx) {
2403
2433
  const dir = markersDir(ctx);
2404
- if (!existsSync4(dir)) return [];
2434
+ if (!existsSync5(dir)) return [];
2405
2435
  const live = [];
2406
2436
  for (const name of readdirSync2(dir)) {
2407
2437
  const pid = Number(name);
@@ -2424,7 +2454,7 @@ import { join as join6 } from "path";
2424
2454
  init_esm_shims();
2425
2455
  import {
2426
2456
  copyFileSync,
2427
- existsSync as existsSync5,
2457
+ existsSync as existsSync6,
2428
2458
  mkdirSync as mkdirSync3,
2429
2459
  readdirSync as readdirSync3,
2430
2460
  readFileSync as readFileSync3,
@@ -2469,7 +2499,11 @@ function literalOf(pattern) {
2469
2499
  function symbolHints(db, pattern) {
2470
2500
  const lit = literalOf(pattern);
2471
2501
  if (!lit) return void 0;
2472
- const nodes = db.graphFindNodes({ query: lit, kinds: SYMBOL_KINDS, limit: 5 });
2502
+ const nodes = db.graphFindNodes({
2503
+ query: lit,
2504
+ kinds: SYMBOL_KINDS,
2505
+ limit: 5
2506
+ });
2473
2507
  if (!nodes.length) return void 0;
2474
2508
  const list = nodes.map((n) => `${n.kind} ${n.name} (${n.path}:${n.start_line ?? "?"})`).join("; ");
2475
2509
  return `eminiar graph: symbols matching "${lit}": ${list}. em_symbols {name_path, include_body: true} reads one; {mode:"references"} lists its callers.`;
@@ -2479,13 +2513,13 @@ function coverageHint(db, rel) {
2479
2513
  if (!row || row.status === "indexed") return void 0;
2480
2514
  return `eminiar graph: ${rel} is ${row.status}${row.reason ? ` (${row.reason})` : ""}; graph answers about it may be incomplete.`;
2481
2515
  }
2482
- var GIT_HISTORY = /(?:^|[;&|]\s*)git\s+(?:(?:-C|--git-dir|--work-tree)[= ]\S+\s+|--no-pager\s+)*(log|blame|shortlog|show|whatchanged|reflog)\b([^;&|]*)/;
2516
+ var GIT_HISTORY = /(?:^|[;&|]\s*)git\s+((?:(?:-C|--git-dir|--work-tree)[= ]\S+\s+|--no-pager\s+)*)(log|blame|shortlog|show|whatchanged|reflog)\b([^;&|]*)/;
2483
2517
  var ONE_COMMIT = /(^|\s)(-1|-n\s*1|-n1|--max-count[= ]1)(?=\s|$)/;
2484
2518
  function historyCall(cmd) {
2485
2519
  const m = GIT_HISTORY.exec(cmd);
2486
2520
  if (!m) return void 0;
2487
- const sub = m[1];
2488
- const rest = (m[2] ?? "").trim();
2521
+ const sub = m[2];
2522
+ const rest = (m[3] ?? "").trim();
2489
2523
  const args = rest.split(/\s+/).filter(Boolean);
2490
2524
  const q = (s) => JSON.stringify(s.replace(/^['"]|['"]$/g, ""));
2491
2525
  const positional = (a) => !a.startsWith("-");
@@ -2520,6 +2554,66 @@ function historyCall(cmd) {
2520
2554
  if (sub === "shortlog") return `{action:"timeline"}`;
2521
2555
  return `{action:"overview"}`;
2522
2556
  }
2557
+ function historyDir(segments, cwd) {
2558
+ let dir = cwd;
2559
+ let steps = 0;
2560
+ const step = (arg) => {
2561
+ dir = ++steps > MAX_STEPS ? void 0 : dir && changeDir(dir, arg);
2562
+ };
2563
+ for (const segment of segments) {
2564
+ const m = GIT_HISTORY.exec(segment);
2565
+ if (m) {
2566
+ const opts = m[1] ?? "";
2567
+ if (/--(git-dir|work-tree)\b/.test(opts)) return void 0;
2568
+ for (const c of opts.matchAll(/-C[= ](\S+)/g)) {
2569
+ step(c[1]);
2570
+ if (!dir) return void 0;
2571
+ }
2572
+ return dir;
2573
+ }
2574
+ const cd = CD.exec(segment);
2575
+ if (cd) step(cd[1]);
2576
+ if (!dir) return void 0;
2577
+ }
2578
+ return dir;
2579
+ }
2580
+ var CD = /^\(?\s*(?:cd|pushd)\b(.*)$/;
2581
+ var MAX_STEPS = 16;
2582
+ var MAX_RAW = 64 * 1024;
2583
+ var MAX_CLASSIFIED = 8 * 1024;
2584
+ var MAX_OPERANDS = 64;
2585
+ var MAX_SEGMENTS = 64;
2586
+ function changeDir(dir, arg) {
2587
+ const a = arg.trim().replace(/^(['"])(.*)\1$/, "$2") || "~";
2588
+ if (/^-|^~[^/]|[\s$`*?[\]{}()\\]/.test(a)) return void 0;
2589
+ return a === "~" || a.startsWith("~/") ? join7(homedir2(), a.slice(1)) : resolve3(dir, a);
2590
+ }
2591
+ function inRootRepo(root, dir) {
2592
+ try {
2593
+ const top = realpathSync3(root);
2594
+ let inside = false;
2595
+ for (let d = realpathSync3(dir); ; d = dirname3(d)) {
2596
+ if (d === top) inside = true;
2597
+ if (existsSync7(join7(d, ".git"))) return inside;
2598
+ if (dirname3(d) === d) return false;
2599
+ }
2600
+ } catch {
2601
+ return false;
2602
+ }
2603
+ }
2604
+ function repoOf(dir) {
2605
+ try {
2606
+ for (let d = realpathSync3(dir); ; d = dirname3(d)) {
2607
+ if (existsSync7(join7(d, ".git"))) return d;
2608
+ if (dirname3(d) === d) return void 0;
2609
+ }
2610
+ } catch {
2611
+ return void 0;
2612
+ }
2613
+ }
2614
+ function sameRepo(root, dir) {
2615
+ return repoOf(root) === repoOf(dir);
2616
+ }
2523
2617
  function docsSources(root) {
2524
2618
  try {
2525
2619
  return readManifestAt(join7(root, ".eminiar", "docs.json"), root).sources;
@@ -2562,7 +2656,34 @@ var BIG_TEXT_EXT = /* @__PURE__ */ new Set([
2562
2656
  ]);
2563
2657
  var VISUAL_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".pdf"]);
2564
2658
  function withoutHeredocs(cmd) {
2565
- return cmd.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?=\n|$)/g, "<<$2");
2659
+ if (!cmd.includes("<<")) return cmd;
2660
+ const head = /<<-?\s*(['"]?)(\w+)\1/y;
2661
+ const seen = /* @__PURE__ */ new Map();
2662
+ const terminator = (word, from) => {
2663
+ const c = seen.get(word);
2664
+ if (c && from >= c.from && (c.at === -1 || from <= c.at)) return c.end;
2665
+ const re = new RegExp(String.raw`\n[^\S\n]*${word}\s*(?=\n|$)`, "g");
2666
+ re.lastIndex = from;
2667
+ const m = re.exec(cmd);
2668
+ const rec = m ? { from, at: m.index, end: m.index + m[0].length } : { from, at: -1, end: -1 };
2669
+ seen.set(word, rec);
2670
+ return rec.end;
2671
+ };
2672
+ let out = "";
2673
+ let copied = 0;
2674
+ for (let k = cmd.indexOf("<<"); k !== -1; k = cmd.indexOf("<<", Math.max(k + 1, copied))) {
2675
+ head.lastIndex = k;
2676
+ const m = head.exec(cmd);
2677
+ if (!m) continue;
2678
+ const word = m[2];
2679
+ const eol = cmd.indexOf("\n", head.lastIndex);
2680
+ if (eol === -1) break;
2681
+ const end = terminator(word, eol + 1);
2682
+ if (end === -1) continue;
2683
+ out += `${cmd.slice(copied, k)}<<${word}${cmd.slice(head.lastIndex, eol)}`;
2684
+ copied = end;
2685
+ }
2686
+ return out + cmd.slice(copied);
2566
2687
  }
2567
2688
  function splitUnquoted(s, sep2) {
2568
2689
  const out = [];
@@ -2570,13 +2691,23 @@ function splitUnquoted(s, sep2) {
2570
2691
  let quote;
2571
2692
  for (let i = 0; i < s.length; i++) {
2572
2693
  const ch = s[i];
2694
+ if (quote === "'") {
2695
+ cur += ch;
2696
+ if (ch === "'") quote = void 0;
2697
+ continue;
2698
+ }
2699
+ if (ch === "\\") {
2700
+ cur += ch + (s[i + 1] ?? "");
2701
+ i++;
2702
+ continue;
2703
+ }
2573
2704
  if (quote) {
2574
2705
  cur += ch;
2575
- if (ch === quote && s[i - 1] !== "\\") quote = void 0;
2706
+ if (ch === quote[quote.length - 1]) quote = void 0;
2576
2707
  continue;
2577
2708
  }
2578
2709
  if (ch === "'" || ch === '"') {
2579
- quote = ch;
2710
+ quote = ch === "'" && s[i - 1] === "$" ? "$'" : ch;
2580
2711
  cur += ch;
2581
2712
  continue;
2582
2713
  }
@@ -2597,9 +2728,44 @@ function commandSegments(cmd) {
2597
2728
  const ch = s[i];
2598
2729
  if (ch === ";" || ch === "\n") return 1;
2599
2730
  if ((ch === "&" || ch === "|") && s[i + 1] === ch) return 2;
2731
+ if (ch === "&" && !/[&<>|]/.test(s[i - 1] ?? "") && !/[&>]/.test(s[i + 1] ?? "")) return 1;
2600
2732
  return 0;
2601
2733
  });
2602
2734
  }
2735
+ function shellWords(s) {
2736
+ const words = [];
2737
+ let cur = "";
2738
+ let inWord = false;
2739
+ for (let i = 0; i < s.length; i++) {
2740
+ const ch = s[i];
2741
+ if (ch === " " || ch === " " || ch === "\n") {
2742
+ if (inWord) words.push(cur);
2743
+ cur = "";
2744
+ inWord = false;
2745
+ continue;
2746
+ }
2747
+ inWord = true;
2748
+ if (ch === "\\") {
2749
+ cur += s[++i] ?? "";
2750
+ } else if (ch === "'" && s[i - 1] !== "$") {
2751
+ const end = s.indexOf("'", i + 1);
2752
+ const stop2 = end === -1 ? s.length : end;
2753
+ cur += s.slice(i + 1, stop2);
2754
+ i = stop2;
2755
+ } else if (ch === '"' || ch === "$" && s[i + 1] === "'") {
2756
+ const close = ch === '"' ? '"' : "'";
2757
+ if (ch === "$") i++;
2758
+ for (i++; i < s.length && s[i] !== close; i++) {
2759
+ if (s[i] === "\\" && i + 1 < s.length) i++;
2760
+ cur += s[i];
2761
+ }
2762
+ } else {
2763
+ cur += ch;
2764
+ }
2765
+ }
2766
+ if (inWord) words.push(cur);
2767
+ return words;
2768
+ }
2603
2769
  function pipeStages(segment) {
2604
2770
  return splitUnquoted(segment, (s, i) => s[i] === "|" && s[i + 1] !== "|" ? 1 : 0);
2605
2771
  }
@@ -2613,10 +2779,416 @@ function bounds(stage) {
2613
2779
  if (!/^\s*(head|tail)\b/.test(stage) || /(^|\s)-c\b|--bytes\b/.test(stage)) return false;
2614
2780
  return headLines(stage) <= BOUNDED_LINES;
2615
2781
  }
2616
- var MUTATING = /^\s*(?:(?:sudo|env|[A-Z_][A-Z0-9_]*=\S*)\s+)*(git\s+(add|commit|checkout|switch|push|pull|fetch|merge|rebase|stash|reset|restore|cherry-pick|revert|tag|init|clone|worktree|mv|rm|am|apply|submodule)|npm\s+(install|i|ci|uninstall|un|link|publish|version|update|up|dedupe)|pnpm\s+(install|i|add|remove|rm|update|up|link)|yarn\s+(add|remove|install|upgrade)|pip3?\s+(install|uninstall)|cargo\s+(add|install)|brew\s+(install|uninstall|upgrade)|gh\s+(pr|issue|release|repo)\s+(create|edit|merge|close|comment|delete|ready)|rm|mv|cp|mkdir|rmdir|touch|chmod|chown|ln|sed\s+(?:-\S+\s+)*-i|tee)\b/;
2617
- var WRITES_FILE = /(?:^|\s|\d)>{1,2}\s*(?!&)(?!\/dev\/null)\S/;
2618
- function mutates(segment) {
2619
- return MUTATING.test(segment) || WRITES_FILE.test(segment);
2782
+ var GIT_MUTATING = /^(git\s+(?:(?:-[Cc]\s*[^-\s]\S*|--(?:git-dir|work-tree)[= ]\S+|--no-pager)\s+)*(add|commit|checkout|switch|push|pull|fetch|merge|rebase|stash|reset|restore|cherry-pick|revert|tag|init|clone|worktree|mv|rm|am|apply|submodule|clean|branch\s(?:.*\s)?(-[a-zA-Z]*[dDmM][a-zA-Z]*|--delete|--move)|config(?:\s+--(?:global|local|system|worktree))*\s+(?:set|unset|--(?:unset|unset-all|add|replace-all|rename-section|remove-section|edit)|-e|\w[\w.-]*\s+\S+)))(?![\w-])/;
2783
+ var OTHER_MUTATING = /^(npm\s+(install|i|ci|uninstall|un|link|publish|version|update|up|dedupe|audit\s+fix)|pnpm\s+(install|i|add|remove|rm|update|up|link|audit\s.*--fix)|yarn\s+(add|remove|install|upgrade)|pip3?\s+(install|uninstall)|cargo\s+(add|install)|brew\s+(install|uninstall|upgrade)|gh\s+(pr|issue|release|repo)\s+(create|edit|merge|close|comment|delete|ready)|terraform\s+(?:-\S+\s+)*(apply|destroy|import|taint|untaint|state\s+(rm|mv|push))|helm\s+(?:-\S+\s+(?:[^-\s]\S*\s+)?)*(install|upgrade|uninstall|rollback|delete)|mvnw?(?:\s.*)?\s(deploy|install|release:\S*)|gradlew?(?:\s.*)?[\s:](publish|upload)\w*|rm|mv|cp|mkdir|rmdir|touch|chmod|chown|ln|sed\s+(?:-\S+\s+)*-i|tee|eval|sh|bash|zsh|find\s.*\s-(delete|exec|execdir|ok|okdir))(?![\w-])/;
2784
+ var MUTATING_WORDS = /* @__PURE__ */ new Set([
2785
+ "npm",
2786
+ "pnpm",
2787
+ "yarn",
2788
+ "pip",
2789
+ "pip3",
2790
+ "cargo",
2791
+ "brew",
2792
+ "gh",
2793
+ "terraform",
2794
+ "helm",
2795
+ "mvn",
2796
+ "mvnw",
2797
+ "gradle",
2798
+ "gradlew",
2799
+ "rm",
2800
+ "mv",
2801
+ "cp",
2802
+ "mkdir",
2803
+ "rmdir",
2804
+ "touch",
2805
+ "chmod",
2806
+ "chown",
2807
+ "ln",
2808
+ "sed",
2809
+ "tee",
2810
+ "eval",
2811
+ "sh",
2812
+ "bash",
2813
+ "zsh",
2814
+ "find"
2815
+ ]);
2816
+ var MUTATING = {
2817
+ test(cmd) {
2818
+ const word = /^[A-Za-z0-9]*/.exec(cmd)[0];
2819
+ if (word === "git") return GIT_MUTATING.test(cmd);
2820
+ return MUTATING_WORDS.has(word) && OTHER_MUTATING.test(cmd);
2821
+ }
2822
+ };
2823
+ var longOpts = (valued) => String.raw`--(?:${valued})\s+[^-\s]\S*|--(?!(?:${valued})(?:\s|$))\S*`;
2824
+ var RUNNERS = [
2825
+ String.raw`sudo(?:\s+(?:-[ugCDhpRrtTU]\s*[^-\s]\S*|-(?![ugCDhpRrtTU-])\S+|${longOpts("user|group|chdir|host|prompt|close-from|role|type|other-user|chroot|command-timeout")}))*`,
2826
+ String.raw`env(?:\s+(?:-[uC]\s*[^-\s]\S*|-(?![uC-])\S+|${longOpts("unset|chdir|split-string")}))*`,
2827
+ String.raw`(?:xargs|parallel)(?:\s+(?:-[nILPsdEaj]\s*[^-\s]\S*|-(?![nILPsdEaj-])\S+|${longOpts("max-args|max-procs|max-lines|max-chars|delimiter|arg-file|eof|jobs")}))*`,
2828
+ String.raw`timeout(?:\s+(?:-[sk]\s*[^-\s]\S*|-(?![sk-])\S+|${longOpts("signal|kill-after")}))*\s+\d\S*`,
2829
+ String.raw`nice(?:\s+-n\s*\S+|\s+-\d+|\s+--adjustment(?:=|\s+)\S+)?`,
2830
+ String.raw`stdbuf(?:\s+-[ioe]\s*\S+)*`,
2831
+ String.raw`(?:time|command)(?:\s+-p)?`,
2832
+ "nohup",
2833
+ String.raw`[A-Z_][A-Z0-9_]*=\S*`
2834
+ ];
2835
+ var SHELL_WORDS = /^(?:\s*(?:[({!]|(?:then|do|else|exec|if|elif|while|until)\s))*\s*/;
2836
+ var RUNNER = new RegExp(String.raw`^\s*(?:(?:${RUNNERS.join("|")})\s+)*`);
2837
+ var RUNNER_HEAD = /^\s*(?:sudo|env|xargs|parallel|timeout|nice|stdbuf|time|command|nohup|[A-Z_])/;
2838
+ var stripRunners = (s) => RUNNER_HEAD.test(s) ? s.replace(RUNNER, "") : s.replace(/^\s+/, "");
2839
+ var WRITES_FILE = /(?:^|[\s\d&])>{1,2}\s*(?!&)(?!\/dev\/null)\S/;
2840
+ var GLUED_WRITE = /(?<![-=>])>{1,2}\s*(?!&)(?!\/dev\/null)\S/;
2841
+ var RUNS_STDIN = /^(python3?|node|perl|ruby|deno|bun)(\s+-\S*)*$/;
2842
+ var FOLLOWS = /^((kubectl|docker)(\s.*)?\s(-f|-w|--follow)(?=[\s=]|$)|tail(\s.*)?\s(-[a-zA-Z]*[fF]|--follow)|.*\s--watch(All)?(?=[\s=]|$))/;
2843
+ var CURL_SENDS = /\s(?:-(?:(?!X)[a-zA-Z])*[dFTOoKDc]|(?:-[a-zA-Z]*X|--request)(?![\s=]*['"]?(?:GET|HEAD)\b)|--(?:data|json|form|upload-file|output|remote-name|config|dump-header|cookie-jar|trace|stderr|create-dirs|libcurl|etag-save|hsts|alt-svc))/;
2844
+ var WGET_PRINTS = /\s(?:-[a-zA-Z]*O\s*-|--output-document[= ]-|--spider)(?=\s|$)/;
2845
+ var WGET_SENDS = /\s(?:--(post-data|post-file|body-data|body-file|method|output-file|append-output|execute|config)\b|-[a-zA-Z]*[oae])/;
2846
+ var KUBECTL_READS = /^(get|describe|logs|top|explain|api-resources|api-versions|events|version|cluster-info|config\s+(view|get-contexts|current-context)|rollout\s+(status|history)|auth\s+can-i)\b/;
2847
+ var DOCKER_READS = /^(ps|logs|images|inspect|history|version|info|stats\s.*--no-stream|(container|image|network|volume)\s+(ls|inspect)|compose\s+(ps|logs|config|images|ls)|system\s+df)\b/;
2848
+ var CHECK_SCRIPT = /^(t|test|lint|type-?check|check)(?=$|[:.-])(?![\w:.-]*(fix|write|update|install|deploy|publish|push|release|clean))/;
2849
+ var FIX_ARGS = /(?:^|\s)(--fix|--write|--apply(-unsafe)?|-u|--update(-?[sS]napshots?)?)(?=[\s=]|$)/;
2850
+ var NPX_CHECKS = /^(vitest|jest|tsc|eslint|biome|prettier)(?:@[\w.^~-]*)?$/;
2851
+ var PM_READS = /^(ls|list|outdated|why|info|view|audit|licenses|bin|root|help)$/;
2852
+ var VALUE_OPTS = {
2853
+ kubectl: /* @__PURE__ */ new Set([
2854
+ "-n",
2855
+ "--namespace",
2856
+ "--context",
2857
+ "--cluster",
2858
+ "--kubeconfig",
2859
+ "--user",
2860
+ "-s",
2861
+ "--server",
2862
+ "--token",
2863
+ "--as",
2864
+ "--as-group",
2865
+ "--as-uid",
2866
+ "--cache-dir",
2867
+ "--certificate-authority",
2868
+ "--client-certificate",
2869
+ "--client-key",
2870
+ "--password",
2871
+ "--username",
2872
+ "--profile",
2873
+ "--profile-output",
2874
+ "--request-timeout",
2875
+ "--tls-server-name",
2876
+ "--log-dir",
2877
+ "--log-file",
2878
+ "--log-file-max-size",
2879
+ "--vmodule",
2880
+ "-v",
2881
+ "--v"
2882
+ ]),
2883
+ docker: /* @__PURE__ */ new Set([
2884
+ "-H",
2885
+ "--host",
2886
+ "-c",
2887
+ "--context",
2888
+ "--config",
2889
+ "-l",
2890
+ "--log-level",
2891
+ "--tlscacert",
2892
+ "--tlscert",
2893
+ "--tlskey"
2894
+ ]),
2895
+ npm: /* @__PURE__ */ new Set(["--prefix", "-w", "--workspace", "--userconfig", "--registry", "--cache"]),
2896
+ pnpm: /* @__PURE__ */ new Set(["-C", "--dir", "--filter", "-F", "--workspace-dir"]),
2897
+ yarn: /* @__PURE__ */ new Set(["--cwd", "--cache-folder", "--registry"])
2898
+ };
2899
+ var FLAG_OPTS = {
2900
+ kubectl: /* @__PURE__ */ new Set([
2901
+ "--insecure-skip-tls-verify",
2902
+ "--match-server-version",
2903
+ "--warnings-as-errors",
2904
+ "--disable-compression",
2905
+ "--alsologtostderr",
2906
+ "--logtostderr",
2907
+ "--add-dir-header",
2908
+ "--skip-headers",
2909
+ "--skip-log-headers",
2910
+ "--one-output",
2911
+ "-h",
2912
+ "--help"
2913
+ ]),
2914
+ docker: /* @__PURE__ */ new Set(["-D", "--debug", "--tls", "--tlsverify", "-v", "--version", "--help"])
2915
+ };
2916
+ var MAX_READINGS = 4;
2917
+ function afterOptions(tool, args) {
2918
+ const words = args.trim().split(/\s+/);
2919
+ const valued = VALUE_OPTS[tool];
2920
+ let i = 0;
2921
+ while (words[i]?.startsWith("-")) i += valued?.has(words[i]) ? 2 : 1;
2922
+ return words.slice(i);
2923
+ }
2924
+ function subcommands(tool, args) {
2925
+ const words = args.trim().split(/\s+/);
2926
+ const valued = VALUE_OPTS[tool];
2927
+ const flags = FLAG_OPTS[tool];
2928
+ const starts = /* @__PURE__ */ new Set();
2929
+ const seen = /* @__PURE__ */ new Set();
2930
+ const todo = [0];
2931
+ while (todo.length) {
2932
+ const i = todo.pop();
2933
+ if (seen.has(i)) continue;
2934
+ seen.add(i);
2935
+ const w = words[i];
2936
+ if (w === void 0 || !w.startsWith("-")) {
2937
+ starts.add(i);
2938
+ if (starts.size > MAX_READINGS) return void 0;
2939
+ } else if (valued?.has(w)) todo.push(i + 2);
2940
+ else if (flags?.has(w) || w.includes("=") || /^-[^-]./.test(w)) todo.push(i + 1);
2941
+ else todo.push(i + 1, i + 2);
2942
+ }
2943
+ const tail = /\s--no-stream\b/.test(args) ? " --no-stream" : "";
2944
+ return [...starts].map((i) => words.slice(i, i + 3).join(" ") + tail);
2945
+ }
2946
+ function verbChanges(tool, reads, args) {
2947
+ const subs = subcommands(tool, args);
2948
+ return !subs || subs.some((s) => !reads.test(s));
2949
+ }
2950
+ function runsScript(tool, args) {
2951
+ const words = afterOptions(tool, args);
2952
+ let script = words.shift();
2953
+ if (script === "run" || script === "run-script") script = words.shift();
2954
+ else if (script === void 0 || PM_READS.test(script) || tool === "npm" && script !== "test")
2955
+ return false;
2956
+ if (script === void 0) return false;
2957
+ return !CHECK_SCRIPT.test(script) || FIX_ARGS.test(words.join(" "));
2958
+ }
2959
+ function makeChanges(args) {
2960
+ const words = shellWords(args);
2961
+ const targets = [];
2962
+ for (let i = 0; i < words.length; i++) {
2963
+ const w = words[i];
2964
+ if (/^(-E|--eval)/.test(w)) return true;
2965
+ if (w.startsWith("-")) {
2966
+ if (/^(-[CfIoW]|--(directory|file|makefile|include-dir|old-file|assume-old|what-if|new-file|assume-new))$/.test(
2967
+ w
2968
+ ) || /^(-[jl]|--jobs|--load-average|--max-load)$/.test(w) && /^\d+(\.\d+)?$/.test(words[i + 1] ?? ""))
2969
+ i++;
2970
+ continue;
2971
+ }
2972
+ if (!w.includes("=")) targets.push(w);
2973
+ }
2974
+ return !targets.length || !targets.every((t) => CHECK_SCRIPT.test(t)) || FIX_ARGS.test(args);
2975
+ }
2976
+ var GH_API_VALUED = /^(-[FfHXpqt]|--(method|field|raw-field|input|header|jq|template|preview|hostname|cache))$/;
2977
+ function ghApiSends(args) {
2978
+ const words = shellWords(args);
2979
+ if (words[0] !== "api") return false;
2980
+ let method;
2981
+ let fields = false;
2982
+ for (let i = 1; i < words.length; i++) {
2983
+ const w = words[i];
2984
+ const long = /^--(method|field|raw-field|input)(?:=|$)/.exec(w)?.[1];
2985
+ if (long) {
2986
+ if (long === "method")
2987
+ method = w.includes("=") ? w.slice(w.indexOf("=") + 1) : words[i + 1] ?? "";
2988
+ else fields = true;
2989
+ } else if (/^-[a-zA-Z]/.test(w)) {
2990
+ const c = w.slice(1).replace(/^i+/, "")[0];
2991
+ const value = w.slice(w.indexOf(c ?? "") + 1) || (words[i + 1] ?? "");
2992
+ if (c === "X") method = value;
2993
+ else if (c === "f" || c === "F") fields = true;
2994
+ }
2995
+ if (GH_API_VALUED.test(w)) i++;
2996
+ }
2997
+ if (method !== void 0) return method.toUpperCase() !== "GET";
2998
+ if (words.includes("graphql")) return /\bmutation\b/.test(words.join(" "));
2999
+ return fields;
3000
+ }
3001
+ function npxChanges(args, cwd) {
3002
+ const words = shellWords(args);
3003
+ let i = 0;
3004
+ for (; words[i]?.startsWith("-"); i++)
3005
+ if (/^(-p|--package|-c|--call)(=|$)/.test(words[i])) return true;
3006
+ const [name = "", ...rest] = words.slice(i);
3007
+ const tool = NPX_CHECKS.exec(name)?.[1];
3008
+ if (!tool || FIX_ARGS.test(args)) return true;
3009
+ const has = (re) => rest.some((w) => re.test(w));
3010
+ if (has(/^--(output-?file|outputFile|out-?dir|outFile|generateTrace)(=|$)/i)) return true;
3011
+ if (tool === "eslint" && has(/^-o/)) return true;
3012
+ if (tool === "prettier" && has(/^-[a-zA-Z]*w$/)) return true;
3013
+ return tool === "tsc" && !tscEmitsNothing(rest, cwd);
3014
+ }
3015
+ function tscEmitsNothing(words, cwd) {
3016
+ if (words.some((w) => /^(-b|--build|--generateTrace)(=|$)/i.test(w))) return false;
3017
+ const noEmit = words.findIndex((w) => /^--noEmit(=true)?$/i.test(w));
3018
+ if (noEmit !== -1) return words[noEmit + 1] !== "false";
3019
+ let project = "tsconfig.json";
3020
+ for (let i = 0; i < words.length; i++) {
3021
+ const w = words[i];
3022
+ if (/^(-p|--project)$/i.test(w)) project = words[++i] ?? "";
3023
+ else if (!w.startsWith("-")) return false;
3024
+ }
3025
+ try {
3026
+ let file2 = resolve3(cwd, project);
3027
+ if (statSync3(file2).isDirectory()) file2 = join7(file2, "tsconfig.json");
3028
+ const st = statSync3(file2);
3029
+ if (st.size > 256 * 1024) return false;
3030
+ return /"noEmit"\s*:\s*true\b/.test(readFileSync5(file2, "utf8"));
3031
+ } catch {
3032
+ return false;
3033
+ }
3034
+ }
3035
+ var CHANGES = /* @__PURE__ */ new Map([
3036
+ ["curl", (a) => CURL_SENDS.test(a)],
3037
+ ["wget", (a) => !WGET_PRINTS.test(a) || WGET_SENDS.test(a)],
3038
+ ["kubectl", (a) => verbChanges("kubectl", KUBECTL_READS, a)],
3039
+ ["docker", (a) => verbChanges("docker", DOCKER_READS, a)],
3040
+ ["gh", ghApiSends],
3041
+ ["make", makeChanges],
3042
+ ["npx", npxChanges],
3043
+ ["npm", (a) => runsScript("npm", a)],
3044
+ ["pnpm", (a) => runsScript("pnpm", a)],
3045
+ ["yarn", (a) => runsScript("yarn", a)]
3046
+ ]);
3047
+ function operands(words, valued) {
3048
+ let n = 0;
3049
+ for (let i = 0; i < words.length; i++) {
3050
+ const w = words[i];
3051
+ if (w === "--") return n + words.length - i - 1;
3052
+ if (w.startsWith("-") && w.length > 1) {
3053
+ if (valued.test(w)) i++;
3054
+ } else n++;
3055
+ }
3056
+ return n;
3057
+ }
3058
+ function awkRuns(words) {
3059
+ let program;
3060
+ for (let i = 0; i < words.length && program === void 0; i++) {
3061
+ const w = words[i];
3062
+ if (w === "--") program = words[i + 1];
3063
+ else if (/^(-[fiEl]|--(file|include|load|exec))/.test(w)) return true;
3064
+ else if (/^(-e|--source)$/.test(w)) program = words[i + 1];
3065
+ else if (/^(-[FvW]|--(field-separator|assign))$/.test(w)) i++;
3066
+ else if (!w.startsWith("-")) program = w;
3067
+ }
3068
+ return program !== void 0 && /\bsystem\s*\(|\|\s*&?\s*getline|\bprintf?\b[^;{}\n]*[>|]|@(load|include)\b/.test(program);
3069
+ }
3070
+ function sedWrites(words) {
3071
+ const scripts = [];
3072
+ let first;
3073
+ for (let i = 0; i < words.length; i++) {
3074
+ const w = words[i];
3075
+ if (w === "--") {
3076
+ first ??= words[i + 1];
3077
+ break;
3078
+ }
3079
+ if (w.startsWith("--")) {
3080
+ const [name, value] = w.split(/=(.*)/s);
3081
+ if (name === "--in-place" || name === "--file") return true;
3082
+ if (name === "--expression") scripts.push(value ?? words[++i] ?? "");
3083
+ else if (name === "--line-length" && value === void 0) i++;
3084
+ } else if (/^-./.test(w)) {
3085
+ for (let k = 1; k < w.length; k++) {
3086
+ const c = w[k];
3087
+ if (c === "i" || c === "f") return true;
3088
+ if (c === "e" || c === "l") {
3089
+ const value = w.slice(k + 1) || words[++i] || "";
3090
+ if (c === "e") scripts.push(value);
3091
+ break;
3092
+ }
3093
+ }
3094
+ } else if (first === void 0) first = w;
3095
+ }
3096
+ if (!scripts.length && first !== void 0) scripts.push(first);
3097
+ return scripts.some(sedScriptWrites);
3098
+ }
3099
+ function sedScriptWrites(script) {
3100
+ if (script.length > 2048) return true;
3101
+ let flagged = false;
3102
+ const t = script.replace(
3103
+ /(^|[^a-zA-Z\\])([sy])([^\\\n\sa-zA-Z0-9])((?:\\[\s\S]|(?!\3)[^\\])*)\3((?:\\[\s\S]|(?!\3)[^\\])*)\3([a-zA-Z0-9]*)/g,
3104
+ (_m, pre, cmd, _d, _a, _b, flags) => {
3105
+ if (cmd === "s" && /[we]/.test(flags)) flagged = true;
3106
+ return `${pre};`;
3107
+ }
3108
+ ).replace(/\/(?:\\[\s\S]|[^/\\\n])*\//g, ";");
3109
+ return flagged || /(?:^|[^a-zA-Z_.-])(?:[wW]|e(?![a-zA-Z]))/.test(t);
3110
+ }
3111
+ var FLAG_WRITES = /* @__PURE__ */ new Map([
3112
+ // `--pre cmd` runs cmd on every file searched
3113
+ ["rg", (w) => w.some((x) => /^--pre(-glob)?(=|$)/.test(x))],
3114
+ ["git", gitRunsOrWrites],
3115
+ ["find", (w) => w.some((x) => /^-(delete|exec|execdir|ok|okdir|fprint0?|fprintf|fls)$/.test(x))],
3116
+ ["tree", (w) => w.some((x) => /^(-[a-zA-Z]*o|--output)/.test(x))],
3117
+ ["sed", sedWrites],
3118
+ ["gsed", sedWrites],
3119
+ ["awk", awkRuns],
3120
+ ["gawk", awkRuns],
3121
+ ["mawk", awkRuns],
3122
+ ["nawk", awkRuns],
3123
+ ["sort", (w) => w.some((x) => /^(-[a-zA-Z]*o|--output(=|$))/.test(x))],
3124
+ // `uniq IN OUT` and `xxd IN OUT` write OUT
3125
+ ["uniq", (w) => operands(w, /^(-[fsw]|--(skip-fields|skip-chars|check-chars))$/) > 1],
3126
+ ["xxd", (w) => operands(w, /^-(c|g|l|o|s|n|cols|groupsize|len|offset|seek|name)$/) > 1],
3127
+ ["yq", (w) => w.some((x) => /^(-[a-zA-Z]*i[a-zA-Z]*|--inplace)(=|$)/.test(x))]
3128
+ ]);
3129
+ function gitRunsOrWrites(words) {
3130
+ let i = 0;
3131
+ for (; i < words.length && words[i].startsWith("-"); i++) {
3132
+ const w = words[i];
3133
+ if (/^(-c|--config-env|--exec-path=)/.test(w)) return true;
3134
+ if (/^(-C|--git-dir|--work-tree|--namespace|--super-prefix)$/.test(w)) i++;
3135
+ }
3136
+ const sub = words[i];
3137
+ const rest = words.slice(i + 1);
3138
+ if (sub === "difftool" || sub === "mergetool") return true;
3139
+ if (rest.some((w) => /^--(output|ext-diff)(=|$)/.test(w))) return true;
3140
+ return sub === "grep" && rest.some((w) => /^(-[a-zA-Z]*O|--open-files-in-pager)/.test(w));
3141
+ }
3142
+ var GIT_ENV = /(?:^|\s)GIT_(?:EXTERNAL_DIFF|PAGER|EDITOR|SEQUENCE_EDITOR|SSH|SSH_COMMAND|ASKPASS|PROXY_COMMAND|EXEC_PATH|CONFIG\w*|TRACE\w*|DIFF_OPTS|TEMPLATE_DIR)=/;
3143
+ var ARITHMETIC = /\$\(\(([^()`]*)\)\)/g;
3144
+ function substitutions(segment) {
3145
+ const found = [];
3146
+ const inner = /[$<>]\(([^()]*)\)|`([^`]*)`/g;
3147
+ let s = segment.replace(ARITHMETIC, "_");
3148
+ for (let level = 0; ; level++) {
3149
+ const next = s.replace(inner, (_, a, b) => {
3150
+ found.push(a ?? b ?? "");
3151
+ return "_";
3152
+ });
3153
+ if (next === s) break;
3154
+ if (level >= MAX_STEPS) return void 0;
3155
+ s = next;
3156
+ }
3157
+ return /[$<>]\(/.test(s) ? void 0 : found;
3158
+ }
3159
+ function staysNative(segment, cwd) {
3160
+ if (WRITES_FILE.test(segment)) return true;
3161
+ if (GLUED_WRITE.test(segment.replace(/'[^']*'|"(?:[^"\\]|\\.)*"/g, "''"))) return true;
3162
+ return pipeStages(segment).some((stage, i) => {
3163
+ const plain = stage.replace(SHELL_WORDS, "");
3164
+ if (plain.includes("GIT_") && GIT_ENV.test(plain)) return true;
3165
+ const cmd = stripRunners(plain).replace(/^\S*\//, "");
3166
+ const [, quoted = "", args = ""] = /^(\S+)([\s\S]*)$/.exec(cmd) ?? [];
3167
+ const tool = quoted.replace(/['"\\]/g, "");
3168
+ const named = tool + args;
3169
+ const bare = /^(npm|pnpm|yarn)$/.test(tool) ? `${tool} ${afterOptions(tool, args).join(" ")}` : named;
3170
+ return MUTATING.test(named) || MUTATING.test(bare) || (/^(?:kubectl|docker|tail)(?![^\s])/.test(named) || named.includes("--watch")) && FOLLOWS.test(named) || i > 0 && RUNS_STDIN.test(named) || CHANGES.get(tool)?.(args, cwd) === true || FLAG_WRITES.get(tool)?.(shellWords(args)) === true;
3171
+ });
3172
+ }
3173
+ var READ_ONLY = /^(?:(?:cd|pushd|popd|pwd|true|false|:|test|\[|sleep|echo|printf|ls|tree|cat|head|tail|wc|grep|egrep|fgrep|rg|sort|uniq|cut|tr|nl|column|awk|sed|jq|yq|diff|cmp|du|df|stat|file|which|type|date|basename|dirname|realpath|readlink|ps|lsof|strings|xxd|od|find)|git\s+(?:(?:-[Cc]\s*[^-\s]\S*|--no-pager)\s+)*(?:diff|log|show|status|ls-files|ls-tree|grep|blame|branch(?=\s*$|\s+(?:-[avr]+|--(?:list|show-current|merged|no-merged|contains))(?:\s|$))|rev-list|rev-parse|for-each-ref|cat-file|describe|shortlog|reflog|merge-base|name-rev|config\s+(?:--(?:global|local|system|worktree)\s+)*(?:--get\S*|--list|-l)))(?![\w-])/;
3174
+ var TOOL_READS = {
3175
+ gh: (a) => /^\s+(?:api|(?:pr|issue|run|search|repo|release)\s+(?:list|view|diff|checks|status))\b/.test(a),
3176
+ npm: (a) => /^(?:test|t|run|run-script|ls|ll|list|outdated|view|show|info|why|explain|audit|root|bin|prefix|help)$/.test(
3177
+ afterOptions("npm", a)[0] ?? ""
3178
+ ),
3179
+ pnpm: (a) => afterOptions("pnpm", a).length > 0,
3180
+ yarn: (a) => afterOptions("yarn", a).length > 0
3181
+ // bare `yarn` installs
3182
+ };
3183
+ function readsOnly(segment, cwd) {
3184
+ return pipeStages(segment).every((stage) => {
3185
+ const cmd = stripRunners(stage.replace(SHELL_WORDS, "")).replace(/^\S*\//, "");
3186
+ const [, tool = "", args = ""] = /^(\S+)([\s\S]*)$/.exec(cmd) ?? [];
3187
+ if (/(?:^|\s)--output(?=[\s=]|$)/.test(args) || tool === "sort" && /\s-\w*o\b/.test(args))
3188
+ return false;
3189
+ if (CHANGES.has(tool)) return TOOL_READS[tool]?.(args) ?? !CHANGES.get(tool)(args, cwd);
3190
+ return READ_ONLY.test(cmd);
3191
+ });
2620
3192
  }
2621
3193
  var READ_LIMIT = 200 * 1024;
2622
3194
  var BIG_TEXT_LIMIT = 20 * 1024;
@@ -2665,13 +3237,13 @@ function fileOperands(stage, cwd) {
2665
3237
  }
2666
3238
  files.push(t);
2667
3239
  }
2668
- if (!files.length) return void 0;
3240
+ if (!files.length || files.length > MAX_OPERANDS) return void 0;
2669
3241
  const out = [];
2670
3242
  for (let f of files) {
2671
3243
  if (/[*?[\]{}$`\\]/.test(f)) return void 0;
2672
3244
  f = f.replace(/^['"]|['"]$/g, "");
2673
3245
  if (f.startsWith("~/")) f = join7(homedir2(), f.slice(2));
2674
- out.push(resolve2(cwd, f));
3246
+ out.push(resolve3(cwd, f));
2675
3247
  }
2676
3248
  return out;
2677
3249
  }
@@ -2709,66 +3281,99 @@ function firstStageBound(first, cwd) {
2709
3281
  const bytes = stageBytes(first, cwd);
2710
3282
  if (bytes === void 0) return void 0;
2711
3283
  if (bytes <= READ_LIMIT) return true;
2712
- return { rule: "big-read", text: `reads about ${Math.round(bytes / 1024)} KB` };
3284
+ return {
3285
+ rule: "big-read",
3286
+ text: `reads about ${Math.round(bytes / 1024)} KB`
3287
+ };
2713
3288
  }
2714
3289
  return void 0;
2715
3290
  }
3291
+ var READ_SOURCE = /^\s*(ls|cat|sed|awk|cut|sort|uniq|tr|nl|echo|printf|ps|lsof|jq|yq|diff|strings|xxd|od|git\s+(diff|ls-files|ls-tree|grep|status|show|log|blame|branch|rev-list|for-each-ref|cat-file)|gh\s+(api|pr\s+(list|view|diff|checks)|issue\s+(list|view)|run\s+(list|view)|search))\b/;
3292
+ var FILTERS = /^\s*(grep|egrep|fgrep|rg|sort|uniq|jq|head|tail|wc|cut|tr|column|less|awk|sed|python3?\s+-m\s+json\.tool)\b/;
3293
+ var UNBOUNDED_FILTERS = /^\s*(grep|rg|sort|uniq|jq|less)\b/;
2716
3294
  function heavySegment(segment, cwd) {
2717
3295
  if (TINY_BASH.test(segment)) return void 0;
2718
3296
  const stages = pipeStages(segment);
2719
3297
  const first = stages[0];
2720
3298
  if (!first) return void 0;
2721
3299
  if (stages.length > 1 && bounds(stages[stages.length - 1])) return void 0;
3300
+ const rest = stages.slice(1);
3301
+ if (!rest.every((s) => FILTERS.test(s))) return void 0;
2722
3302
  const bound = firstStageBound(first, cwd);
2723
3303
  if (bound === true) return void 0;
2724
3304
  if (bound) return bound;
2725
3305
  if (HEAVY_BASH.test(first)) return { rule: "heavy", text: "usually prints a lot" };
2726
- if (stages.slice(1).some((s) => /^\s*(grep|rg|sort|uniq|jq|less)\b/.test(s)))
2727
- return { rule: "pipe", text: "streams an unbounded output through a filter" };
3306
+ if (READ_SOURCE.test(first) && rest.some((s) => UNBOUNDED_FILTERS.test(s)))
3307
+ return {
3308
+ rule: "pipe",
3309
+ text: "streams an unbounded output through a filter"
3310
+ };
2728
3311
  return void 0;
2729
3312
  }
2730
3313
  function headOf(segment, n = 60) {
2731
3314
  const one = segment.replace(/\s+/g, " ").trim();
2732
3315
  return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
2733
3316
  }
2734
- function bashSuggestion(raw, cwd) {
3317
+ function bashSuggestion(raw, root, cwd) {
3318
+ if (raw.length > MAX_RAW) return void 0;
2735
3319
  const cmd = withoutHeredocs(raw);
2736
3320
  if (!cmd.trim()) return void 0;
3321
+ if (cmd.length > MAX_CLASSIFIED) return void 0;
2737
3322
  const segments = commandSegments(cmd);
2738
- if (segments.some(mutates)) return void 0;
3323
+ if (segments.length > MAX_SEGMENTS) return void 0;
3324
+ const subs = substitutions(cmd);
3325
+ if (!subs) return void 0;
3326
+ if ([...segments, ...subs.flatMap(commandSegments)].some((s) => staysNative(s, cwd)))
3327
+ return void 0;
3328
+ if (segments.length > 1 && !segments.every((s) => readsOnly(s, cwd))) return void 0;
2739
3329
  const history = historyCall(cmd);
2740
- if (history) {
3330
+ const dir = history && historyDir(segments, cwd);
3331
+ if (history && dir && !sameRepo(root, dir)) return void 0;
3332
+ if (history && dir && inRootRepo(root, dir)) {
2741
3333
  return {
2742
3334
  tool: "em_history",
2743
3335
  rule: "history",
2744
3336
  reason: `the git history is already classified (commit kinds, fixes traced to the change that caused them, risk per file); em_history ${history} answers this without the raw log`
2745
3337
  };
2746
3338
  }
3339
+ let at = cwd;
3340
+ let steps = 0;
2747
3341
  for (const segment of segments) {
2748
- const heavy = heavySegment(segment, cwd);
2749
- if (!heavy) continue;
2750
- return {
2751
- tool: "em_execute",
2752
- rule: heavy.rule,
2753
- reason: `\`${headOf(segment)}\` ${heavy.text}; em_execute {language:"shell", code, intent} keeps the output out of context and returns only matching sections`
2754
- };
3342
+ const heavy = heavySegment(segment, at ?? cwd);
3343
+ if (heavy)
3344
+ return {
3345
+ tool: "em_execute",
3346
+ rule: heavy.rule,
3347
+ reason: `\`${headOf(segment)}\` ${heavy.text}; em_execute {language:"shell", code, intent} keeps the output out of context and returns only matching sections`
3348
+ };
3349
+ const cd = CD.exec(segment);
3350
+ if (cd) at = ++steps > MAX_STEPS ? void 0 : at && changeDir(at, cd[1]);
2755
3351
  }
2756
3352
  return void 0;
2757
3353
  }
3354
+ function executeFileTakes(root, fp) {
3355
+ try {
3356
+ const real = realpathSync3(root);
3357
+ confinePath(real, resolve3(root, fp), loadPermissionRules(real));
3358
+ return true;
3359
+ } catch {
3360
+ return false;
3361
+ }
3362
+ }
2758
3363
  function suggest(toolName, input, root, extra = {}) {
2759
3364
  switch (toolName) {
2760
3365
  case "Bash":
2761
- return bashSuggestion(String(input.command ?? ""), extra.cwd ?? root);
3366
+ return bashSuggestion(String(input.command ?? ""), root, extra.cwd ?? root);
2762
3367
  case "Read": {
2763
3368
  const fp = String(input.file_path ?? "");
2764
3369
  if (!fp) return void 0;
3370
+ if (input.offset != null || input.limit != null) return void 0;
2765
3371
  const ext = extname(fp).toLowerCase();
2766
3372
  if (VISUAL_EXT.has(ext)) return void 0;
2767
3373
  try {
2768
- const abs = resolve2(root, fp);
2769
- const st = statSync3(abs);
3374
+ const st = statSync3(resolve3(root, fp));
2770
3375
  if (st.size > 200 * 1024 || BIG_TEXT_EXT.has(ext) && st.size > 20 * 1024) {
2771
- if (!checkReadPath(loadPermissionRules(root), abs).allowed) return void 0;
3376
+ if (!executeFileTakes(root, fp)) return void 0;
2772
3377
  return {
2773
3378
  tool: "em_execute_file",
2774
3379
  rule: "big-read",
@@ -2835,12 +3440,12 @@ function subagentPrefix(shell) {
2835
3440
  }
2836
3441
  function projectRel(ctx, fp) {
2837
3442
  if (!fp) return void 0;
2838
- let abs = resolve2(ctx.root, fp);
3443
+ let abs = resolve3(ctx.root, fp);
2839
3444
  try {
2840
- abs = realpathSync2(abs);
3445
+ abs = realpathSync3(abs);
2841
3446
  } catch {
2842
3447
  }
2843
- const rel = relative(ctx.root, abs).split("\\").join("/");
3448
+ const rel = relative2(ctx.root, abs).split("\\").join("/");
2844
3449
  return rel.startsWith("..") ? void 0 : rel;
2845
3450
  }
2846
3451
  function graphContext(ctx, input) {
@@ -2871,7 +3476,11 @@ function fileKnowledge(ctx, input) {
2871
3476
  const repeatAfter = ctx.config.recall.repeatAfter;
2872
3477
  const edit = EDIT_TOOLS.has(input.tool_name ?? "");
2873
3478
  const results = [
2874
- memoriesForFile(db, rel, { sessionId, symbols: fileSymbols(db, rel), repeatAfter }),
3479
+ memoriesForFile(db, rel, {
3480
+ sessionId,
3481
+ symbols: fileSymbols(db, rel),
3482
+ repeatAfter
3483
+ }),
2875
3484
  fileContext(db, rel, { sessionId, edit, repeatAfter })
2876
3485
  ];
2877
3486
  const shown = results.filter((r) => r.text);
@@ -2921,7 +3530,11 @@ async function webKnowledge(ctx, input) {
2921
3530
  if (!text.trim()) return void 0;
2922
3531
  try {
2923
3532
  const db = getStorage(ctx, { vectors: false });
2924
- const r = await recallForText(db, text, { maxMemories: 0, maxDocs: 3, path: "web" });
3533
+ const r = await recallForText(db, text, {
3534
+ maxMemories: 0,
3535
+ maxDocs: 3,
3536
+ path: "web"
3537
+ });
2925
3538
  if (!r.text) return void 0;
2926
3539
  const pages = r.text.split("\n").filter((l) => l.startsWith("- docs:"));
2927
3540
  return [
@@ -2939,7 +3552,12 @@ function joinHints(...hints) {
2939
3552
  function withContext(out, extra) {
2940
3553
  if (!extra) return out;
2941
3554
  if (!out)
2942
- return { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: extra } };
3555
+ return {
3556
+ hookSpecificOutput: {
3557
+ hookEventName: "PreToolUse",
3558
+ additionalContext: extra
3559
+ }
3560
+ };
2943
3561
  const cur = out.hookSpecificOutput?.additionalContext;
2944
3562
  return {
2945
3563
  ...out,
@@ -2981,7 +3599,10 @@ async function route(ctx, input, swarmWorker) {
2981
3599
  const graph = graphContext(ctx, input);
2982
3600
  const hint = joinHints(graph, memo);
2983
3601
  const symbol = input.tool_name === "Grep" && graph ? literalOf(String(input.tool_input.pattern ?? "")) : void 0;
2984
- const s = suggest(input.tool_name, input.tool_input, ctx.root, { symbol, cwd: input.cwd });
3602
+ const s = suggest(input.tool_name, input.tool_input, ctx.root, {
3603
+ symbol,
3604
+ cwd: input.cwd
3605
+ });
2985
3606
  const record = routingRecorder(ctx, input, s);
2986
3607
  if (!s) {
2987
3608
  record("allow");
@@ -3101,12 +3722,12 @@ function annotate(s, note = "") {
3101
3722
 
3102
3723
  // src/hooks/session-start.ts
3103
3724
  init_esm_shims();
3104
- import { appendFileSync, existsSync as existsSync6, readdirSync as readdirSync4 } from "fs";
3725
+ import { appendFileSync, existsSync as existsSync8, readdirSync as readdirSync4 } from "fs";
3105
3726
  import { join as join9 } from "path";
3106
3727
 
3107
3728
  // src/session/current.ts
3108
3729
  init_esm_shims();
3109
- import { readFileSync as readFileSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
3730
+ import { readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
3110
3731
  import { join as join8 } from "path";
3111
3732
  function file(ctx) {
3112
3733
  return join8(projectDir(ctx.id), "current-session");
@@ -3220,7 +3841,7 @@ ${guide}`);
3220
3841
  }
3221
3842
  try {
3222
3843
  const onboarding = join9(memoriesDir(ctx), "onboarding");
3223
- const onboarded = existsSync6(onboarding) && readdirSync4(onboarding).some((f) => f.endsWith(".md"));
3844
+ const onboarded = existsSync8(onboarding) && readdirSync4(onboarding).some((f) => f.endsWith(".md"));
3224
3845
  if (!onboarded)
3225
3846
  parts.push(
3226
3847
  "No onboarding memories yet: run /eminiar:em-onboard once so future sessions start with the project's structure, build and test commands."
@@ -3234,7 +3855,7 @@ ${guide}`);
3234
3855
  } catch {
3235
3856
  }
3236
3857
  try {
3237
- if (existsSync6(join9(ctx.root, ".git")) && !db.metaGet("history_files"))
3858
+ if (existsSync8(join9(ctx.root, ".git")) && !db.metaGet("history_files"))
3238
3859
  parts.push(
3239
3860
  'Git history is not classified yet: em_history {action:"overview"} builds it (the server also does this in the background shortly after start); bug-prone files are flagged before you read or edit them once it exists.'
3240
3861
  );
@@ -3279,8 +3900,8 @@ function ranLate() {
3279
3900
  }
3280
3901
  async function runWithin(handler, input, ms) {
3281
3902
  let timer;
3282
- const deadline = new Promise((resolve3) => {
3283
- timer = setTimeout(() => resolve3("late"), ms);
3903
+ const deadline = new Promise((resolve4) => {
3904
+ timer = setTimeout(() => resolve4("late"), ms);
3284
3905
  });
3285
3906
  try {
3286
3907
  const r = await Promise.race([handler(input), deadline]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eminiar",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "Local-first context, memory and knowledge-graph plugin for Claude Code and Cursor. Sandboxed execution, FTS5 + vector search, session continuity, and a code graph — nothing leaves your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",