eminiar 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eminiar",
3
3
  "displayName": "Eminiar",
4
- "version": "0.9.0",
4
+ "version": "0.9.1",
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"
package/dist/cli.js CHANGED
@@ -30645,10 +30645,10 @@ async function search(db, query, opts = {}) {
30645
30645
  }
30646
30646
  }
30647
30647
  const fused = rrf(lists);
30648
- const top = [...fused.entries()].sort((a, b) => b[1] - a[1]).slice(0, Math.max(limit * 4, 20));
30649
- const chunks = db.getChunks(top.map(([id]) => id));
30648
+ const top2 = [...fused.entries()].sort((a, b) => b[1] - a[1]).slice(0, Math.max(limit * 4, 20));
30649
+ const chunks = db.getChunks(top2.map(([id]) => id));
30650
30650
  const byId = new Map(chunks.map((c) => [c.id, c]));
30651
- const scored = top.map(([id, score]) => ({ chunk: byId.get(id), score, matched: [] })).filter((s) => !!s.chunk);
30651
+ const scored = top2.map(([id, score]) => ({ chunk: byId.get(id), score, matched: [] })).filter((s) => !!s.chunk);
30652
30652
  const allTerms = [...terms, ...parsed.phrases];
30653
30653
  const ranked = rerank(scored, allTerms);
30654
30654
  const seenHash = /* @__PURE__ */ new Set();
@@ -30866,10 +30866,10 @@ function cycles(nodes, edges) {
30866
30866
  stack.push(start);
30867
30867
  onStack.add(start);
30868
30868
  while (work.length) {
30869
- const top = work[work.length - 1];
30870
- const succ = adj.get(top.n);
30871
- if (top.i < succ.length) {
30872
- const m = succ[top.i++];
30869
+ const top2 = work[work.length - 1];
30870
+ const succ = adj.get(top2.n);
30871
+ if (top2.i < succ.length) {
30872
+ const m = succ[top2.i++];
30873
30873
  if (!idx.has(m)) {
30874
30874
  idx.set(m, counter);
30875
30875
  low.set(m, counter);
@@ -30878,20 +30878,20 @@ function cycles(nodes, edges) {
30878
30878
  onStack.add(m);
30879
30879
  work.push({ n: m, i: 0 });
30880
30880
  } else if (onStack.has(m)) {
30881
- low.set(top.n, Math.min(low.get(top.n), idx.get(m)));
30881
+ low.set(top2.n, Math.min(low.get(top2.n), idx.get(m)));
30882
30882
  }
30883
30883
  } else {
30884
30884
  work.pop();
30885
30885
  const parent = work[work.length - 1];
30886
- if (parent) low.set(parent.n, Math.min(low.get(parent.n), low.get(top.n)));
30887
- if (low.get(top.n) === idx.get(top.n)) {
30886
+ if (parent) low.set(parent.n, Math.min(low.get(parent.n), low.get(top2.n)));
30887
+ if (low.get(top2.n) === idx.get(top2.n)) {
30888
30888
  const comp = [];
30889
30889
  let m;
30890
30890
  do {
30891
30891
  m = stack.pop();
30892
30892
  onStack.delete(m);
30893
30893
  comp.push(m);
30894
- } while (m !== top.n);
30894
+ } while (m !== top2.n);
30895
30895
  if (comp.length > 1) out.push(comp.sort());
30896
30896
  }
30897
30897
  }
@@ -31046,8 +31046,8 @@ function computeMetrics(db) {
31046
31046
  function topFolder(paths2) {
31047
31047
  const counts = /* @__PURE__ */ new Map();
31048
31048
  for (const p of paths2) {
31049
- const top = p.includes("/") ? `${p.split("/")[0]}/` : "(root)";
31050
- counts.set(top, (counts.get(top) ?? 0) + 1);
31049
+ const top2 = p.includes("/") ? `${p.split("/")[0]}/` : "(root)";
31050
+ counts.set(top2, (counts.get(top2) ?? 0) + 1);
31051
31051
  }
31052
31052
  return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0] ?? "";
31053
31053
  }
@@ -31560,7 +31560,7 @@ function chunkBySymbols(code, rel, symbols, opts = DEFAULT_CHUNKER) {
31560
31560
  const valid = symbols.filter(
31561
31561
  (s) => s.startLine >= 1 && s.endLine >= s.startLine && s.endLine <= total
31562
31562
  );
31563
- const top = valid.filter((s) => depthOf(s.name) === 1).sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
31563
+ const top2 = valid.filter((s) => depthOf(s.name) === 1).sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
31564
31564
  const membersOf = (parent) => valid.filter(
31565
31565
  (s) => depthOf(s.name) === depthOf(parent.name) + 1 && s.name.startsWith(`${parent.name}.`) && s.startLine >= parent.startLine && s.endLine <= parent.endLine
31566
31566
  ).sort((a, b) => a.startLine - b.startLine);
@@ -31589,7 +31589,7 @@ function chunkBySymbols(code, rel, symbols, opts = DEFAULT_CHUNKER) {
31589
31589
  }
31590
31590
  tiny = [];
31591
31591
  };
31592
- for (const sym of top) {
31592
+ for (const sym of top2) {
31593
31593
  if (sym.startLine < cursor) continue;
31594
31594
  const a = extendUp(sym.startLine, cursor - 1);
31595
31595
  const b = sym.endLine;
@@ -32691,11 +32691,11 @@ async function parsePhp(root, rel, code) {
32691
32691
  if (file3) {
32692
32692
  if (file3 !== rel) addImport(fileId(file3), label, fqn, line, 0.95);
32693
32693
  } else {
32694
- const top = fqn.split("\\")[0];
32695
- importedPkgs.add(top);
32696
- if (!imports.has(pkgId(top)))
32697
- nodes.push({ id: pkgId(top), kind: "package", name: top, source: "tree-sitter" });
32698
- addImport(pkgId(top), label, fqn, line, 1);
32694
+ const top2 = fqn.split("\\")[0];
32695
+ importedPkgs.add(top2);
32696
+ if (!imports.has(pkgId(top2)))
32697
+ nodes.push({ id: pkgId(top2), kind: "package", name: top2, source: "tree-sitter" });
32698
+ addImport(pkgId(top2), label, fqn, line, 1);
32699
32699
  }
32700
32700
  if (kind) scope.set(local, { id: pkgId(fqn.split("\\")[0]), confidence: 0.6 });
32701
32701
  else classUses.set(local, fqn);
@@ -32802,8 +32802,8 @@ async function parsePhp(root, rel, code) {
32802
32802
  return;
32803
32803
  }
32804
32804
  }
32805
- function insideDecl(n, top) {
32806
- for (let cur = n.parent; cur && cur.id !== top.id; cur = cur.parent) {
32805
+ function insideDecl(n, top2) {
32806
+ for (let cur = n.parent; cur && cur.id !== top2.id; cur = cur.parent) {
32807
32807
  if (cur.type === "function_definition" || cur.type === "method_declaration") return true;
32808
32808
  if (CLASS_KINDS[cur.type]) return true;
32809
32809
  }
@@ -35950,8 +35950,8 @@ function gitignoreHint(ctx) {
35950
35950
  const file3 = join22(ctx.root, ".gitignore");
35951
35951
  if (!existsSync17(file3)) return void 0;
35952
35952
  const dir = memoriesRel(ctx);
35953
- const top = dir.split("/")[0];
35954
- const ignored = readFileSync16(file3, "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, "").replace(/\/$/, "")).some((l) => l === top || l === dir || l === `${top}/**` || l === `${dir}/**`);
35953
+ const top2 = dir.split("/")[0];
35954
+ const ignored = readFileSync16(file3, "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, "").replace(/\/$/, "")).some((l) => l === top2 || l === dir || l === `${top2}/**` || l === `${dir}/**`);
35955
35955
  return ignored ? `note: .gitignore ignores ${dir}, so memories stay local to this machine (remove that line to share them with the team)` : void 0;
35956
35956
  }
35957
35957
  var import_fast_glob3, MEMORY_KINDS, SEGMENT, NAME_RE, MEM_LINK_RE, PATH_RE, hinted;
@@ -36614,11 +36614,11 @@ function folderRows(files, languages2) {
36614
36614
  score: Number((a.score / a.files).toFixed(3)),
36615
36615
  languages: a.langs
36616
36616
  }));
36617
- const top = new Map(rows.filter((r) => r.path.split("/").length <= 2).map((r) => [r.path, r]));
36617
+ const top2 = new Map(rows.filter((r) => r.path.split("/").length <= 2).map((r) => [r.path, r]));
36618
36618
  return rows.filter((r) => {
36619
36619
  const segs = r.path.split("/");
36620
36620
  if (segs.length <= 2) return true;
36621
- return (top.get(`${segs[0]}/`)?.files ?? 0) >= 12;
36621
+ return (top2.get(`${segs[0]}/`)?.files ?? 0) >= 12;
36622
36622
  }).sort((a, b) => a.path.localeCompare(b.path));
36623
36623
  }
36624
36624
  function entryPoints(root, db) {
@@ -42255,6 +42255,63 @@ var init_recall = __esm({
42255
42255
  }
42256
42256
  });
42257
42257
 
42258
+ // src/session/routing-stats.ts
42259
+ function routingStats(events) {
42260
+ const byTool = {};
42261
+ let decisions = 0;
42262
+ for (const e of events) {
42263
+ if (e.category !== ROUTING_CATEGORY) continue;
42264
+ let p;
42265
+ try {
42266
+ p = JSON.parse(e.payload);
42267
+ } catch {
42268
+ continue;
42269
+ }
42270
+ if (!p.tool) continue;
42271
+ decisions++;
42272
+ let row = byTool[p.tool];
42273
+ if (!row) {
42274
+ row = { calls: 0, denied: 0, annotated: 0, targets: {}, rules: {} };
42275
+ byTool[p.tool] = row;
42276
+ }
42277
+ row.calls++;
42278
+ if (p.decision === "deny") {
42279
+ row.denied++;
42280
+ if (p.target) row.targets[p.target] = (row.targets[p.target] ?? 0) + 1;
42281
+ if (p.rule) row.rules[p.rule] = (row.rules[p.rule] ?? 0) + 1;
42282
+ } else if (p.decision === "annotate") row.annotated++;
42283
+ }
42284
+ return { decisions, byTool };
42285
+ }
42286
+ function formatRoutingStats(title, s) {
42287
+ if (!s.decisions) return `${title}: no routing decisions recorded yet`;
42288
+ const tools = Object.entries(s.byTool).sort((a, b) => b[1].calls - a[1].calls).map(([tool, r]) => {
42289
+ const pct = Math.round(100 * r.denied / r.calls);
42290
+ const parts = [`${tool} ${r.denied}/${r.calls} denied (${pct}%)`];
42291
+ if (r.annotated) parts.push(`${r.annotated} annotated`);
42292
+ return parts.join(", ");
42293
+ });
42294
+ const lines = [`${title}: ${s.decisions} native call(s) \u2014 ${tools.join(" \xB7 ")}`];
42295
+ const rules = {};
42296
+ const targets = {};
42297
+ for (const r of Object.values(s.byTool)) {
42298
+ for (const [k, v] of Object.entries(r.rules)) rules[k] = (rules[k] ?? 0) + v;
42299
+ for (const [k, v] of Object.entries(r.targets)) targets[k] = (targets[k] ?? 0) + v;
42300
+ }
42301
+ if (Object.keys(targets).length)
42302
+ lines.push(` denied \u2192 ${top(targets)}; by rule: ${top(rules, 6)}`);
42303
+ return lines.join("\n");
42304
+ }
42305
+ var ROUTING_CATEGORY, top;
42306
+ var init_routing_stats = __esm({
42307
+ "src/session/routing-stats.ts"() {
42308
+ "use strict";
42309
+ init_esm_shims();
42310
+ ROUTING_CATEGORY = "routing";
42311
+ top = (m, n = 4) => Object.entries(m).sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => `${k} \xD7${v}`).join(", ");
42312
+ }
42313
+ });
42314
+
42258
42315
  // src/tools/em_stats.ts
42259
42316
  var em_stats_exports = {};
42260
42317
  __export(em_stats_exports, {
@@ -42282,13 +42339,14 @@ var init_em_stats = __esm({
42282
42339
  init_zod();
42283
42340
  init_current();
42284
42341
  init_recall();
42342
+ init_routing_stats();
42285
42343
  init_global();
42286
42344
  init_record();
42287
42345
  init_storage();
42288
42346
  init_types2();
42289
42347
  emStats = defineTool({
42290
42348
  name: "em_stats",
42291
- description: "Show how much context eminiar has kept out of the conversation: tokens seen vs returned per tool for this session, this project, and lifetime across all projects, plus index/graph sizes and how often proactive recall (memories, docs, history before prompts, reads and edits) fired versus stayed quiet, with the reasons.",
42349
+ description: "Show how much context eminiar has kept out of the conversation: tokens seen vs returned per tool for this session, this project, and lifetime across all projects, plus index/graph sizes, how often routing intercepted native Bash/Read/Grep calls (and by which rule), and how often proactive recall (memories, docs, history before prompts, reads and edits) fired versus stayed quiet, with the reasons.",
42292
42350
  schema: {
42293
42351
  scope: external_exports.enum(["session", "project", "lifetime", "all"]).optional().describe("default all")
42294
42352
  },
@@ -42299,6 +42357,12 @@ var init_em_stats = __esm({
42299
42357
  const parts = [];
42300
42358
  if (scope === "session" || scope === "all") {
42301
42359
  parts.push(formatStats(`session ${sessionId.slice(0, 8)}`, db.statsBySession(sessionId)));
42360
+ parts.push(
42361
+ formatRoutingStats(
42362
+ "routing (session)",
42363
+ routingStats(db.eventsBySession(sessionId, { categories: [ROUTING_CATEGORY] }))
42364
+ )
42365
+ );
42302
42366
  parts.push(
42303
42367
  formatRecallStats(
42304
42368
  "recall (session)",
@@ -42308,6 +42372,12 @@ var init_em_stats = __esm({
42308
42372
  }
42309
42373
  if (scope === "project" || scope === "all") {
42310
42374
  parts.push(formatStats(`project ${ctx.root}`, mergeByTool(db.statsAll())));
42375
+ parts.push(
42376
+ formatRoutingStats(
42377
+ "routing (project, last 2000 calls)",
42378
+ routingStats(db.eventsRecent(2e3, [ROUTING_CATEGORY]))
42379
+ )
42380
+ );
42311
42381
  parts.push(
42312
42382
  formatRecallStats(
42313
42383
  "recall (project, last 2000 decisions)",
@@ -45351,7 +45421,7 @@ Use
45351
45421
  docs add [--pick recommended|local|service|infra|catalog|api|registry|all] [--name N] [--url U ...] [--path P]
45352
45422
  [--glob G ...] [--provider npm:x|pypi:x|go:x|env:X|image:x|api:host] [--depth N] [--max N] [--no-sync]
45353
45423
  docs list | sync [name ...] [--force] | remove <name> | lookup "<q>" [--name N] | resolve <provider>
45354
- stats [--lifetime] Context savings and how often proactive recall fired
45424
+ stats [--lifetime] Context savings, routing interceptions, how often recall fired
45355
45425
  recall ["<q>"] Memories and session history
45356
45426
  recall --explain "<prompt>" [--session ID] Why hook recall would (not) fire for a prompt
45357
45427
  recall --file <path> What a Read/Edit of the file would attach (memories, history)
package/dist/hook.js CHANGED
@@ -5919,7 +5919,9 @@ var CATEGORIES = {
5919
5919
  tool_call: 4,
5920
5920
  session_start: 4,
5921
5921
  /** A proactive recall decision (fired or quiet) and what it showed; never in the guide. */
5922
- recall: 4
5922
+ recall: 4,
5923
+ /** A routing decision (allow / annotate / deny) for one native call; never in the guide. */
5924
+ routing: 4
5923
5925
  };
5924
5926
  var GUIDE_ORDER = [
5925
5927
  { title: "Last Request", categories: ["user_prompt"] },
@@ -29119,6 +29121,7 @@ var preCompact = (input2) => {
29119
29121
  // src/hooks/routing.ts
29120
29122
  init_esm_shims();
29121
29123
  import { realpathSync as realpathSync4, statSync as statSync4 } from "fs";
29124
+ import { homedir as homedir4 } from "os";
29122
29125
  import { extname as extname2, join as join11, relative as relative4, resolve as resolve3 } from "path";
29123
29126
 
29124
29127
  // src/docs/manifest.ts
@@ -29416,6 +29419,26 @@ function fitBudget(lines, budget) {
29416
29419
  return out.length > budget ? `${out.slice(0, budget - 1)}\u2026` : out;
29417
29420
  }
29418
29421
 
29422
+ // src/session/routing-stats.ts
29423
+ init_esm_shims();
29424
+ var ROUTING_CATEGORY = "routing";
29425
+ function recordRouting(db, sessionId, e) {
29426
+ db.eventInsert({
29427
+ sessionId,
29428
+ category: ROUTING_CATEGORY,
29429
+ priority: 4,
29430
+ tool: e.tool,
29431
+ payload: {
29432
+ tool: e.tool,
29433
+ decision: e.decision,
29434
+ ...e.target ? { target: e.target } : {},
29435
+ ...e.rule ? { rule: e.rule } : {},
29436
+ ...e.why ? { why: e.why } : {},
29437
+ ...e.head ? { head: e.head.slice(0, 120) } : {}
29438
+ }
29439
+ });
29440
+ }
29441
+
29419
29442
  // src/session/server-alive.ts
29420
29443
  init_esm_shims();
29421
29444
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
@@ -29609,29 +29632,201 @@ var BIG_TEXT_EXT = /* @__PURE__ */ new Set([
29609
29632
  function withoutHeredocs(cmd) {
29610
29633
  return cmd.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?=\n|$)/g, "<<$2");
29611
29634
  }
29635
+ function splitUnquoted(s, sep2) {
29636
+ const out = [];
29637
+ let cur = "";
29638
+ let quote2;
29639
+ for (let i = 0; i < s.length; i++) {
29640
+ const ch = s[i];
29641
+ if (quote2) {
29642
+ cur += ch;
29643
+ if (ch === quote2 && s[i - 1] !== "\\") quote2 = void 0;
29644
+ continue;
29645
+ }
29646
+ if (ch === "'" || ch === '"') {
29647
+ quote2 = ch;
29648
+ cur += ch;
29649
+ continue;
29650
+ }
29651
+ const n = sep2(s, i);
29652
+ if (n) {
29653
+ out.push(cur);
29654
+ cur = "";
29655
+ i += n - 1;
29656
+ continue;
29657
+ }
29658
+ cur += ch;
29659
+ }
29660
+ out.push(cur);
29661
+ return out.map((p) => p.trim()).filter(Boolean);
29662
+ }
29663
+ function commandSegments(cmd) {
29664
+ return splitUnquoted(cmd, (s, i) => {
29665
+ const ch = s[i];
29666
+ if (ch === ";" || ch === "\n") return 1;
29667
+ if ((ch === "&" || ch === "|") && s[i + 1] === ch) return 2;
29668
+ return 0;
29669
+ });
29670
+ }
29671
+ function pipeStages(segment) {
29672
+ return splitUnquoted(segment, (s, i) => s[i] === "|" && s[i + 1] !== "|" ? 1 : 0);
29673
+ }
29674
+ function headLines(stage) {
29675
+ const m = /(?:^|\s)(?:-n\s*=?\s*|-)(\d+)(?=\s|$)|--lines[= ](\d+)/.exec(stage);
29676
+ return m ? Number(m[1] ?? m[2]) : 10;
29677
+ }
29678
+ var BOUNDED_LINES = 400;
29679
+ function bounds(stage) {
29680
+ if (/^\s*wc\b/.test(stage)) return true;
29681
+ if (!/^\s*(head|tail)\b/.test(stage) || /(^|\s)-c\b|--bytes\b/.test(stage)) return false;
29682
+ return headLines(stage) <= BOUNDED_LINES;
29683
+ }
29684
+ 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/;
29685
+ var WRITES_FILE = /(?:^|\s|\d)>{1,2}\s*(?!&)(?!\/dev\/null)\S/;
29686
+ function mutates(segment) {
29687
+ return MUTATING.test(segment) || WRITES_FILE.test(segment);
29688
+ }
29689
+ var READ_LIMIT = 200 * 1024;
29690
+ var BIG_TEXT_LIMIT = 20 * 1024;
29691
+ var GREP_VALUE_FLAGS = /* @__PURE__ */ new Set([
29692
+ "-e",
29693
+ "-f",
29694
+ "-m",
29695
+ "-A",
29696
+ "-B",
29697
+ "-C",
29698
+ "-d",
29699
+ "-g",
29700
+ "-t",
29701
+ "-T",
29702
+ "--regexp",
29703
+ "--file",
29704
+ "--max-count",
29705
+ "--after-context",
29706
+ "--before-context",
29707
+ "--context",
29708
+ "--include",
29709
+ "--exclude",
29710
+ "--glob",
29711
+ "--type"
29712
+ ]);
29713
+ function fileOperands(stage, cwd) {
29714
+ const tokens = stage.trim().split(/\s+/);
29715
+ const cmd = tokens[0];
29716
+ const isGrep = /^(grep|egrep|fgrep|rg)$/.test(cmd);
29717
+ const files = [];
29718
+ let patternGiven = !isGrep;
29719
+ for (let i = 1; i < tokens.length; i++) {
29720
+ const t = tokens[i];
29721
+ if (t === "--") {
29722
+ files.push(...tokens.slice(i + 1));
29723
+ break;
29724
+ }
29725
+ if (t.startsWith("-") && t.length > 1) {
29726
+ if (/^-[ef]$|^--(regexp|file)$/.test(t)) patternGiven = true;
29727
+ if (GREP_VALUE_FLAGS.has(t)) i++;
29728
+ continue;
29729
+ }
29730
+ if (!patternGiven) {
29731
+ patternGiven = true;
29732
+ continue;
29733
+ }
29734
+ files.push(t);
29735
+ }
29736
+ if (!files.length) return void 0;
29737
+ const out = [];
29738
+ for (let f of files) {
29739
+ if (/[*?[\]{}$`\\]/.test(f)) return void 0;
29740
+ f = f.replace(/^['"]|['"]$/g, "");
29741
+ if (f.startsWith("~/")) f = join11(homedir4(), f.slice(2));
29742
+ out.push(resolve3(cwd, f));
29743
+ }
29744
+ return out;
29745
+ }
29746
+ function stageBytes(stage, cwd) {
29747
+ const files = fileOperands(stage, cwd);
29748
+ if (!files) return void 0;
29749
+ let total = 0;
29750
+ for (const f of files) {
29751
+ try {
29752
+ const st = statSync4(f);
29753
+ if (st.isDirectory()) return void 0;
29754
+ if (BIG_TEXT_EXT.has(extname2(f).toLowerCase()) && st.size > BIG_TEXT_LIMIT)
29755
+ return READ_LIMIT + st.size;
29756
+ total += st.size;
29757
+ } catch {
29758
+ }
29759
+ }
29760
+ return total;
29761
+ }
29762
+ function sedRangeLines(stage) {
29763
+ const m = /^\s*sed\s+(?:-\S+\s+)*-n\s+['"]?(\d+),(\d+)p['"]?/.exec(stage);
29764
+ return m ? Number(m[2]) - Number(m[1]) + 1 : void 0;
29765
+ }
29766
+ function firstStageBound(first, cwd) {
29767
+ if (/^\s*(head|tail)\s/.test(first)) return bounds(first) || void 0;
29768
+ const sed = sedRangeLines(first);
29769
+ if (sed !== void 0) return sed <= BOUNDED_LINES || void 0;
29770
+ if (/^\s*du\s+-[a-zA-Z]*s/.test(first)) return true;
29771
+ if (/^\s*git\s+log\b/.test(first) && ONE_COMMIT.test(first)) return true;
29772
+ if (/^\s*(grep|egrep|fgrep|rg)\s/.test(first) && /(^|\s)-[a-zA-Z]*[lLcq][a-zA-Z]*(?=\s|$)|--(files-with-matches|files-without-match|count|quiet)\b/.test(
29773
+ first
29774
+ ))
29775
+ return true;
29776
+ if (/^\s*(cat|grep|egrep|fgrep|rg)\s/.test(first)) {
29777
+ const bytes = stageBytes(first, cwd);
29778
+ if (bytes === void 0) return void 0;
29779
+ if (bytes <= READ_LIMIT) return true;
29780
+ return { rule: "big-read", text: `reads about ${Math.round(bytes / 1024)} KB` };
29781
+ }
29782
+ return void 0;
29783
+ }
29784
+ function heavySegment(segment, cwd) {
29785
+ if (TINY_BASH.test(segment)) return void 0;
29786
+ const stages = pipeStages(segment);
29787
+ const first = stages[0];
29788
+ if (!first) return void 0;
29789
+ if (stages.length > 1 && bounds(stages[stages.length - 1])) return void 0;
29790
+ const bound = firstStageBound(first, cwd);
29791
+ if (bound === true) return void 0;
29792
+ if (bound) return bound;
29793
+ if (HEAVY_BASH.test(first)) return { rule: "heavy", text: "usually prints a lot" };
29794
+ if (stages.slice(1).some((s) => /^\s*(grep|rg|sort|uniq|jq|less)\b/.test(s)))
29795
+ return { rule: "pipe", text: "streams an unbounded output through a filter" };
29796
+ return void 0;
29797
+ }
29798
+ function headOf(segment, n = 60) {
29799
+ const one = segment.replace(/\s+/g, " ").trim();
29800
+ return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
29801
+ }
29802
+ function bashSuggestion(raw, cwd) {
29803
+ const cmd = withoutHeredocs(raw);
29804
+ if (!cmd.trim()) return void 0;
29805
+ const segments = commandSegments(cmd);
29806
+ if (segments.some(mutates)) return void 0;
29807
+ const history = historyCall(cmd);
29808
+ if (history) {
29809
+ return {
29810
+ tool: "em_history",
29811
+ rule: "history",
29812
+ 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`
29813
+ };
29814
+ }
29815
+ for (const segment of segments) {
29816
+ const heavy = heavySegment(segment, cwd);
29817
+ if (!heavy) continue;
29818
+ return {
29819
+ tool: "em_execute",
29820
+ rule: heavy.rule,
29821
+ reason: `\`${headOf(segment)}\` ${heavy.text}; em_execute {language:"shell", code, intent} keeps the output out of context and returns only matching sections`
29822
+ };
29823
+ }
29824
+ return void 0;
29825
+ }
29612
29826
  function suggest(toolName, input2, root, extra = {}) {
29613
29827
  switch (toolName) {
29614
- case "Bash": {
29615
- const cmd = withoutHeredocs(String(input2.command ?? ""));
29616
- if (!cmd || TINY_BASH.test(cmd)) return void 0;
29617
- if (/^\s*cat\b[^|\n]*>/.test(cmd) && !cmd.includes("|")) return void 0;
29618
- const history = historyCall(cmd);
29619
- if (history) {
29620
- return {
29621
- tool: "em_history",
29622
- 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`
29623
- };
29624
- }
29625
- if (/^\s*git\s+log\b/.test(cmd) && ONE_COMMIT.test(cmd) && !cmd.includes("|"))
29626
- return void 0;
29627
- if (HEAVY_BASH.test(cmd) || /\|\s*(head|tail|grep|wc|sort|uniq|jq|less)\b/.test(cmd)) {
29628
- return {
29629
- tool: "em_execute",
29630
- reason: `this command is likely to print a lot; em_execute {language:"shell", code, intent} keeps the output out of context and returns only matching sections`
29631
- };
29632
- }
29633
- return void 0;
29634
- }
29828
+ case "Bash":
29829
+ return bashSuggestion(String(input2.command ?? ""), extra.cwd ?? root);
29635
29830
  case "Read": {
29636
29831
  const fp = String(input2.file_path ?? "");
29637
29832
  if (!fp) return void 0;
@@ -29643,6 +29838,7 @@ function suggest(toolName, input2, root, extra = {}) {
29643
29838
  if (!checkReadPath(loadPermissionRules(root), abs).allowed) return void 0;
29644
29839
  return {
29645
29840
  tool: "em_execute_file",
29841
+ rule: "big-read",
29646
29842
  reason: `${fp} is ${Math.round(st.size / 1024)} KB; em_execute_file {path, code} processes it in the sandbox so only what you print enters context`
29647
29843
  };
29648
29844
  }
@@ -29655,6 +29851,7 @@ function suggest(toolName, input2, root, extra = {}) {
29655
29851
  if (extra.symbol) {
29656
29852
  return {
29657
29853
  tool: "em_symbols",
29854
+ rule: "symbol",
29658
29855
  annotateOnly: !content,
29659
29856
  reason: `the graph indexes "${extra.symbol}": em_symbols {name_path:"${extra.symbol}", mode:"references"} lists its uses, {include_body:true} its definition; em_search {queries:["${extra.symbol}"], source:"code"} finds the code by symbol`
29660
29857
  };
@@ -29662,6 +29859,7 @@ function suggest(toolName, input2, root, extra = {}) {
29662
29859
  if (content) {
29663
29860
  return {
29664
29861
  tool: "em_execute",
29862
+ rule: "content-grep",
29665
29863
  reason: `content-mode grep can be large; em_execute with rg and an intent returns only matching sections (em_search {queries, mode:"regex"} scans the indexed files with context)`
29666
29864
  };
29667
29865
  }
@@ -29830,9 +30028,16 @@ var preToolUse = async (input2) => {
29830
30028
  const graph = graphContext(ctx, input2);
29831
30029
  const hint = joinHints(graph, memo2);
29832
30030
  const symbol2 = input2.tool_name === "Grep" && graph ? literalOf(String(input2.tool_input.pattern ?? "")) : void 0;
29833
- const s = suggest(input2.tool_name, input2.tool_input, ctx.root, { symbol: symbol2 });
29834
- if (!s) return withContext(void 0, hint);
29835
- if (s.annotateOnly) return withContext(annotate(s), hint);
30031
+ const s = suggest(input2.tool_name, input2.tool_input, ctx.root, { symbol: symbol2, cwd: input2.cwd });
30032
+ const record2 = routingRecorder(ctx, input2, s);
30033
+ if (!s) {
30034
+ record2("allow");
30035
+ return withContext(void 0, hint);
30036
+ }
30037
+ if (s.annotateOnly) {
30038
+ record2("annotate", "annotate-only");
30039
+ return withContext(annotate(s), hint);
30040
+ }
29836
30041
  if (mode === "enforce") {
29837
30042
  if (s.prefixPrompt) {
29838
30043
  const prompt = String(input2.tool_input.prompt ?? "");
@@ -29853,6 +30058,7 @@ var preToolUse = async (input2) => {
29853
30058
  "info",
29854
30059
  `enforce \u2192 annotate for ${input2.tool_name}: no eminiar MCP server running for ${ctx.root}`
29855
30060
  );
30061
+ record2("annotate", "no-server");
29856
30062
  return withContext(
29857
30063
  annotate(
29858
30064
  s,
@@ -29861,6 +30067,8 @@ var preToolUse = async (input2) => {
29861
30067
  hint
29862
30068
  );
29863
30069
  }
30070
+ record2("deny");
30071
+ hookLog("info", `deny ${input2.tool_name} \u2192 ${s.tool} [${s.rule ?? "?"}]: ${callHead(input2)}`);
29864
30072
  return {
29865
30073
  hookSpecificOutput: {
29866
30074
  hookEventName: "PreToolUse",
@@ -29869,9 +30077,35 @@ var preToolUse = async (input2) => {
29869
30077
  }
29870
30078
  };
29871
30079
  }
30080
+ record2("annotate", "mode");
29872
30081
  const extra = s.prefixPrompt ? joinHints((await briefFor(ctx, String(input2.tool_input.prompt ?? ""))).trim(), hint) : hint;
29873
30082
  return withContext(annotate(s), extra);
29874
30083
  };
30084
+ var ROUTED_TOOLS = /* @__PURE__ */ new Set(["Bash", "Read", "Grep", "WebFetch", "WebSearch"]);
30085
+ function callHead(input2) {
30086
+ const t = input2.tool_input ?? {};
30087
+ const v = t.command ?? t.file_path ?? t.pattern ?? t.url ?? t.query ?? "";
30088
+ return headOf(String(v), 120);
30089
+ }
30090
+ function routingRecorder(ctx, input2, s) {
30091
+ const tool = input2.tool_name ?? "";
30092
+ const sessionId = input2.session_id;
30093
+ if (!sessionId || !ROUTED_TOOLS.has(tool)) return () => {
30094
+ };
30095
+ return (decision, why) => {
30096
+ try {
30097
+ recordRouting(getStorage(ctx, { vectors: false }), sessionId, {
30098
+ tool,
30099
+ decision,
30100
+ target: s?.tool,
30101
+ rule: s?.rule,
30102
+ why,
30103
+ head: callHead(input2)
30104
+ });
30105
+ } catch {
30106
+ }
30107
+ };
30108
+ }
29875
30109
  async function briefFor(ctx, task) {
29876
30110
  const parts = [];
29877
30111
  try {
package/dist/server.js CHANGED
@@ -13703,7 +13703,7 @@ function chunkBySymbols(code, rel, symbols, opts = DEFAULT_CHUNKER) {
13703
13703
  const valid = symbols.filter(
13704
13704
  (s) => s.startLine >= 1 && s.endLine >= s.startLine && s.endLine <= total
13705
13705
  );
13706
- const top = valid.filter((s) => depthOf(s.name) === 1).sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
13706
+ const top2 = valid.filter((s) => depthOf(s.name) === 1).sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
13707
13707
  const membersOf = (parent) => valid.filter(
13708
13708
  (s) => depthOf(s.name) === depthOf(parent.name) + 1 && s.name.startsWith(`${parent.name}.`) && s.startLine >= parent.startLine && s.endLine <= parent.endLine
13709
13709
  ).sort((a, b) => a.startLine - b.startLine);
@@ -13732,7 +13732,7 @@ function chunkBySymbols(code, rel, symbols, opts = DEFAULT_CHUNKER) {
13732
13732
  }
13733
13733
  tiny = [];
13734
13734
  };
13735
- for (const sym of top) {
13735
+ for (const sym of top2) {
13736
13736
  if (sym.startLine < cursor) continue;
13737
13737
  const a = extendUp(sym.startLine, cursor - 1);
13738
13738
  const b = sym.endLine;
@@ -14886,11 +14886,11 @@ async function parsePhp(root, rel, code) {
14886
14886
  if (file3) {
14887
14887
  if (file3 !== rel) addImport(fileId(file3), label, fqn, line, 0.95);
14888
14888
  } else {
14889
- const top = fqn.split("\\")[0];
14890
- importedPkgs.add(top);
14891
- if (!imports.has(pkgId(top)))
14892
- nodes.push({ id: pkgId(top), kind: "package", name: top, source: "tree-sitter" });
14893
- addImport(pkgId(top), label, fqn, line, 1);
14889
+ const top2 = fqn.split("\\")[0];
14890
+ importedPkgs.add(top2);
14891
+ if (!imports.has(pkgId(top2)))
14892
+ nodes.push({ id: pkgId(top2), kind: "package", name: top2, source: "tree-sitter" });
14893
+ addImport(pkgId(top2), label, fqn, line, 1);
14894
14894
  }
14895
14895
  if (kind) scope.set(local, { id: pkgId(fqn.split("\\")[0]), confidence: 0.6 });
14896
14896
  else classUses.set(local, fqn);
@@ -14997,8 +14997,8 @@ async function parsePhp(root, rel, code) {
14997
14997
  return;
14998
14998
  }
14999
14999
  }
15000
- function insideDecl(n, top) {
15001
- for (let cur = n.parent; cur && cur.id !== top.id; cur = cur.parent) {
15000
+ function insideDecl(n, top2) {
15001
+ for (let cur = n.parent; cur && cur.id !== top2.id; cur = cur.parent) {
15002
15002
  if (cur.type === "function_definition" || cur.type === "method_declaration") return true;
15003
15003
  if (CLASS_KINDS[cur.type]) return true;
15004
15004
  }
@@ -17428,10 +17428,10 @@ function cycles(nodes, edges) {
17428
17428
  stack.push(start);
17429
17429
  onStack.add(start);
17430
17430
  while (work.length) {
17431
- const top = work[work.length - 1];
17432
- const succ = adj.get(top.n);
17433
- if (top.i < succ.length) {
17434
- const m = succ[top.i++];
17431
+ const top2 = work[work.length - 1];
17432
+ const succ = adj.get(top2.n);
17433
+ if (top2.i < succ.length) {
17434
+ const m = succ[top2.i++];
17435
17435
  if (!idx.has(m)) {
17436
17436
  idx.set(m, counter);
17437
17437
  low.set(m, counter);
@@ -17440,20 +17440,20 @@ function cycles(nodes, edges) {
17440
17440
  onStack.add(m);
17441
17441
  work.push({ n: m, i: 0 });
17442
17442
  } else if (onStack.has(m)) {
17443
- low.set(top.n, Math.min(low.get(top.n), idx.get(m)));
17443
+ low.set(top2.n, Math.min(low.get(top2.n), idx.get(m)));
17444
17444
  }
17445
17445
  } else {
17446
17446
  work.pop();
17447
17447
  const parent = work[work.length - 1];
17448
- if (parent) low.set(parent.n, Math.min(low.get(parent.n), low.get(top.n)));
17449
- if (low.get(top.n) === idx.get(top.n)) {
17448
+ if (parent) low.set(parent.n, Math.min(low.get(parent.n), low.get(top2.n)));
17449
+ if (low.get(top2.n) === idx.get(top2.n)) {
17450
17450
  const comp = [];
17451
17451
  let m;
17452
17452
  do {
17453
17453
  m = stack.pop();
17454
17454
  onStack.delete(m);
17455
17455
  comp.push(m);
17456
- } while (m !== top.n);
17456
+ } while (m !== top2.n);
17457
17457
  if (comp.length > 1) out.push(comp.sort());
17458
17458
  }
17459
17459
  }
@@ -17608,8 +17608,8 @@ function computeMetrics(db) {
17608
17608
  function topFolder(paths2) {
17609
17609
  const counts = /* @__PURE__ */ new Map();
17610
17610
  for (const p of paths2) {
17611
- const top = p.includes("/") ? `${p.split("/")[0]}/` : "(root)";
17612
- counts.set(top, (counts.get(top) ?? 0) + 1);
17611
+ const top2 = p.includes("/") ? `${p.split("/")[0]}/` : "(root)";
17612
+ counts.set(top2, (counts.get(top2) ?? 0) + 1);
17613
17613
  }
17614
17614
  return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0] ?? "";
17615
17615
  }
@@ -18452,8 +18452,8 @@ function gitignoreHint(ctx) {
18452
18452
  const file3 = join12(ctx.root, ".gitignore");
18453
18453
  if (!existsSync8(file3)) return void 0;
18454
18454
  const dir = memoriesRel(ctx);
18455
- const top = dir.split("/")[0];
18456
- const ignored = readFileSync8(file3, "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, "").replace(/\/$/, "")).some((l) => l === top || l === dir || l === `${top}/**` || l === `${dir}/**`);
18455
+ const top2 = dir.split("/")[0];
18456
+ const ignored = readFileSync8(file3, "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, "").replace(/\/$/, "")).some((l) => l === top2 || l === dir || l === `${top2}/**` || l === `${dir}/**`);
18457
18457
  return ignored ? `note: .gitignore ignores ${dir}, so memories stay local to this machine (remove that line to share them with the team)` : void 0;
18458
18458
  }
18459
18459
  var import_fast_glob3, MEMORY_KINDS, SEGMENT, NAME_RE, MEM_LINK_RE, PATH_RE, hinted;
@@ -51219,11 +51219,11 @@ function folderRows(files, languages2) {
51219
51219
  score: Number((a.score / a.files).toFixed(3)),
51220
51220
  languages: a.langs
51221
51221
  }));
51222
- const top = new Map(rows.filter((r) => r.path.split("/").length <= 2).map((r) => [r.path, r]));
51222
+ const top2 = new Map(rows.filter((r) => r.path.split("/").length <= 2).map((r) => [r.path, r]));
51223
51223
  return rows.filter((r) => {
51224
51224
  const segs = r.path.split("/");
51225
51225
  if (segs.length <= 2) return true;
51226
- return (top.get(`${segs[0]}/`)?.files ?? 0) >= 12;
51226
+ return (top2.get(`${segs[0]}/`)?.files ?? 0) >= 12;
51227
51227
  }).sort((a, b) => a.path.localeCompare(b.path));
51228
51228
  }
51229
51229
  function entryPoints(root, db) {
@@ -52869,10 +52869,10 @@ async function search(db, query, opts = {}) {
52869
52869
  }
52870
52870
  }
52871
52871
  const fused = rrf(lists);
52872
- const top = [...fused.entries()].sort((a, b) => b[1] - a[1]).slice(0, Math.max(limit * 4, 20));
52873
- const chunks = db.getChunks(top.map(([id]) => id));
52872
+ const top2 = [...fused.entries()].sort((a, b) => b[1] - a[1]).slice(0, Math.max(limit * 4, 20));
52873
+ const chunks = db.getChunks(top2.map(([id]) => id));
52874
52874
  const byId = new Map(chunks.map((c) => [c.id, c]));
52875
- const scored = top.map(([id, score]) => ({ chunk: byId.get(id), score, matched: [] })).filter((s) => !!s.chunk);
52875
+ const scored = top2.map(([id, score]) => ({ chunk: byId.get(id), score, matched: [] })).filter((s) => !!s.chunk);
52876
52876
  const allTerms = [...terms, ...parsed.phrases];
52877
52877
  const ranked = rerank(scored, allTerms);
52878
52878
  const seenHash = /* @__PURE__ */ new Set();
@@ -57892,12 +57892,12 @@ function renderBranch(m, name, limit = 20) {
57892
57892
  );
57893
57893
  const files = /* @__PURE__ */ new Map();
57894
57894
  for (const c of own2) for (const f of c.files) files.set(f.path, (files.get(f.path) ?? 0) + 1);
57895
- const top = [...files.entries()].sort((a, b2) => b2[1] - a[1]).slice(0, 10);
57896
- if (top.length)
57895
+ const top2 = [...files.entries()].sort((a, b2) => b2[1] - a[1]).slice(0, 10);
57896
+ if (top2.length)
57897
57897
  lines.push(
57898
- `files touched: ${top.map(([p, n]) => n > 1 ? `${p} \xD7${n}` : p).join(", ")}${files.size > 10 ? ` \u2026 +${files.size - 10}` : ""}`
57898
+ `files touched: ${top2.map(([p, n]) => n > 1 ? `${p} \xD7${n}` : p).join(", ")}${files.size > 10 ? ` \u2026 +${files.size - 10}` : ""}`
57899
57899
  );
57900
- const risky = top.map(([p]) => m.files.get(p)).filter((f) => !!f && f.risk >= 0.3);
57900
+ const risky = top2.map(([p]) => m.files.get(p)).filter((f) => !!f && f.risk >= 0.3);
57901
57901
  if (risky.length)
57902
57902
  lines.push(
57903
57903
  `touches bug-prone files: ${risky.map((f) => `${f.path} (${f.risk.toFixed(2)})`).join(", ")}`
@@ -58598,10 +58598,61 @@ function formatRecallStats(title, s) {
58598
58598
  return lines.join("\n");
58599
58599
  }
58600
58600
 
58601
+ // src/session/routing-stats.ts
58602
+ init_esm_shims();
58603
+ var ROUTING_CATEGORY = "routing";
58604
+ function routingStats(events) {
58605
+ const byTool = {};
58606
+ let decisions = 0;
58607
+ for (const e of events) {
58608
+ if (e.category !== ROUTING_CATEGORY) continue;
58609
+ let p;
58610
+ try {
58611
+ p = JSON.parse(e.payload);
58612
+ } catch {
58613
+ continue;
58614
+ }
58615
+ if (!p.tool) continue;
58616
+ decisions++;
58617
+ let row = byTool[p.tool];
58618
+ if (!row) {
58619
+ row = { calls: 0, denied: 0, annotated: 0, targets: {}, rules: {} };
58620
+ byTool[p.tool] = row;
58621
+ }
58622
+ row.calls++;
58623
+ if (p.decision === "deny") {
58624
+ row.denied++;
58625
+ if (p.target) row.targets[p.target] = (row.targets[p.target] ?? 0) + 1;
58626
+ if (p.rule) row.rules[p.rule] = (row.rules[p.rule] ?? 0) + 1;
58627
+ } else if (p.decision === "annotate") row.annotated++;
58628
+ }
58629
+ return { decisions, byTool };
58630
+ }
58631
+ var top = (m, n = 4) => Object.entries(m).sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => `${k} \xD7${v}`).join(", ");
58632
+ function formatRoutingStats(title, s) {
58633
+ if (!s.decisions) return `${title}: no routing decisions recorded yet`;
58634
+ const tools2 = Object.entries(s.byTool).sort((a, b) => b[1].calls - a[1].calls).map(([tool, r]) => {
58635
+ const pct = Math.round(100 * r.denied / r.calls);
58636
+ const parts = [`${tool} ${r.denied}/${r.calls} denied (${pct}%)`];
58637
+ if (r.annotated) parts.push(`${r.annotated} annotated`);
58638
+ return parts.join(", ");
58639
+ });
58640
+ const lines = [`${title}: ${s.decisions} native call(s) \u2014 ${tools2.join(" \xB7 ")}`];
58641
+ const rules = {};
58642
+ const targets = {};
58643
+ for (const r of Object.values(s.byTool)) {
58644
+ for (const [k, v] of Object.entries(r.rules)) rules[k] = (rules[k] ?? 0) + v;
58645
+ for (const [k, v] of Object.entries(r.targets)) targets[k] = (targets[k] ?? 0) + v;
58646
+ }
58647
+ if (Object.keys(targets).length)
58648
+ lines.push(` denied \u2192 ${top(targets)}; by rule: ${top(rules, 6)}`);
58649
+ return lines.join("\n");
58650
+ }
58651
+
58601
58652
  // src/tools/em_stats.ts
58602
58653
  var emStats = defineTool({
58603
58654
  name: "em_stats",
58604
- description: "Show how much context eminiar has kept out of the conversation: tokens seen vs returned per tool for this session, this project, and lifetime across all projects, plus index/graph sizes and how often proactive recall (memories, docs, history before prompts, reads and edits) fired versus stayed quiet, with the reasons.",
58655
+ description: "Show how much context eminiar has kept out of the conversation: tokens seen vs returned per tool for this session, this project, and lifetime across all projects, plus index/graph sizes, how often routing intercepted native Bash/Read/Grep calls (and by which rule), and how often proactive recall (memories, docs, history before prompts, reads and edits) fired versus stayed quiet, with the reasons.",
58605
58656
  schema: {
58606
58657
  scope: external_exports.enum(["session", "project", "lifetime", "all"]).optional().describe("default all")
58607
58658
  },
@@ -58612,6 +58663,12 @@ var emStats = defineTool({
58612
58663
  const parts = [];
58613
58664
  if (scope === "session" || scope === "all") {
58614
58665
  parts.push(formatStats(`session ${sessionId.slice(0, 8)}`, db.statsBySession(sessionId)));
58666
+ parts.push(
58667
+ formatRoutingStats(
58668
+ "routing (session)",
58669
+ routingStats(db.eventsBySession(sessionId, { categories: [ROUTING_CATEGORY] }))
58670
+ )
58671
+ );
58615
58672
  parts.push(
58616
58673
  formatRecallStats(
58617
58674
  "recall (session)",
@@ -58621,6 +58678,12 @@ var emStats = defineTool({
58621
58678
  }
58622
58679
  if (scope === "project" || scope === "all") {
58623
58680
  parts.push(formatStats(`project ${ctx.root}`, mergeByTool(db.statsAll())));
58681
+ parts.push(
58682
+ formatRoutingStats(
58683
+ "routing (project, last 2000 calls)",
58684
+ routingStats(db.eventsRecent(2e3, [ROUTING_CATEGORY]))
58685
+ )
58686
+ );
58624
58687
  parts.push(
58625
58688
  formatRecallStats(
58626
58689
  "recall (project, last 2000 decisions)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eminiar",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Local-first context, memory and knowledge-graph plugin for Claude Code. Sandboxed execution, FTS5 + vector search, session continuity, and a code graph — nothing leaves your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",