eminiar 0.8.1 → 0.9.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/hook.js CHANGED
@@ -5917,7 +5917,9 @@ var CATEGORIES = {
5917
5917
  intent: 4,
5918
5918
  data: 4,
5919
5919
  tool_call: 4,
5920
- session_start: 4
5920
+ session_start: 4,
5921
+ /** A proactive recall decision (fired or quiet) and what it showed; never in the guide. */
5922
+ recall: 4
5921
5923
  };
5922
5924
  var GUIDE_ORDER = [
5923
5925
  { title: "Last Request", categories: ["user_prompt"] },
@@ -26082,7 +26084,12 @@ var ConfigSchema = external_exports.object({
26082
26084
  /** Before WebSearch / WebFetch: the indexed doc pages that already answer it. */
26083
26085
  onWeb: external_exports.boolean().default(true),
26084
26086
  maxMemories: external_exports.number().int().min(0).max(10).default(3),
26085
- maxDocs: external_exports.number().int().min(0).max(10).default(2)
26087
+ maxDocs: external_exports.number().int().min(0).max(10).default(2),
26088
+ /**
26089
+ * Prompts after which an item already shown in this context window may be injected
26090
+ * again when it matches (0 = once per context window only).
26091
+ */
26092
+ repeatAfter: external_exports.number().int().min(0).max(1e3).default(10)
26086
26093
  }).prefault({}),
26087
26094
  docs: external_exports.object({
26088
26095
  /** Project-relative manifest of documentation sources (`em_docs`); commit it. */
@@ -26975,6 +26982,8 @@ var PROMPT_FILLER = /* @__PURE__ */ new Set([
26975
26982
  ]);
26976
26983
  var MAX_TERMS = 16;
26977
26984
  var RECALL_CATEGORY = "recall";
26985
+ var DEFAULT_REPEAT_AFTER = 10;
26986
+ var NAME_MATCH_RATIO = 0.3;
26978
26987
  function promptTerms(prompt) {
26979
26988
  const body = prompt.replace(/^\s*\/\S+/, " ");
26980
26989
  return parseQuery(body).terms.filter((t) => t.length >= 3 && !PROMPT_FILLER.has(t) && !/^\d+$/.test(t)).slice(0, MAX_TERMS);
@@ -26982,26 +26991,50 @@ function promptTerms(prompt) {
26982
26991
  function requiredMatches(termCount) {
26983
26992
  return Math.min(3, Math.max(2, Math.ceil(termCount * 0.2)));
26984
26993
  }
26985
- function shownKeys(db, sessionId) {
26994
+ function shownKeys(db, sessionId, repeatAfter = DEFAULT_REPEAT_AFTER) {
26986
26995
  const events = db.eventsBySession(sessionId, {
26987
- categories: [RECALL_CATEGORY, "session_start"]
26996
+ categories: [RECALL_CATEGORY, "session_start", "user_prompt"]
26988
26997
  });
26989
- const out = /* @__PURE__ */ new Set();
26998
+ const shownAt = /* @__PURE__ */ new Map();
26999
+ let prompts = 0;
26990
27000
  for (const e of events) {
26991
27001
  if (e.category === "session_start") {
26992
- out.clear();
27002
+ shownAt.clear();
27003
+ prompts = 0;
27004
+ continue;
27005
+ }
27006
+ if (e.category === "user_prompt") {
27007
+ prompts++;
26993
27008
  continue;
26994
27009
  }
26995
27010
  try {
26996
- for (const k of JSON.parse(e.payload).keys ?? []) out.add(k);
27011
+ for (const k of JSON.parse(e.payload).keys ?? [])
27012
+ shownAt.set(k, prompts);
26997
27013
  } catch {
26998
27014
  }
26999
27015
  }
27016
+ const out = /* @__PURE__ */ new Set();
27017
+ for (const [k, at] of shownAt) if (repeatAfter <= 0 || prompts - at < repeatAfter) out.add(k);
27000
27018
  return out;
27001
27019
  }
27002
- function recordShown(db, sessionId, keys) {
27003
- if (!keys.length) return;
27004
- db.eventInsert({ sessionId, category: RECALL_CATEGORY, priority: 4, payload: { keys } });
27020
+ function recordShown(db, sessionId, keys, info = { path: "prompt" }) {
27021
+ if (!keys.length && !info.trace) return;
27022
+ const t = info.trace;
27023
+ db.eventInsert({
27024
+ sessionId,
27025
+ category: RECALL_CATEGORY,
27026
+ priority: 4,
27027
+ payload: {
27028
+ keys,
27029
+ path: info.path,
27030
+ ...t ? {
27031
+ terms: t.terms,
27032
+ need: t.need,
27033
+ candidates: t.candidates.length,
27034
+ gated: t.candidates.filter((c) => c.outcome !== "shown").slice(0, 6).map((c) => `${c.key} ${c.outcome}${c.why ? ` (${c.why})` : ""}`)
27035
+ } : {}
27036
+ }
27037
+ });
27005
27038
  }
27006
27039
  function clip2(s, max) {
27007
27040
  const t = s.replace(/\s+/g, " ").trim();
@@ -27024,16 +27057,53 @@ function kindRank(kind) {
27024
27057
  const i = KIND_ORDER.indexOf(kind);
27025
27058
  return i === -1 ? KIND_ORDER.length : i;
27026
27059
  }
27060
+ function sharedTerms(terms, ...texts) {
27061
+ const hay = texts.join("\n").toLowerCase();
27062
+ return terms.filter((t) => {
27063
+ const base = t.replace(/\*$/, "").toLowerCase();
27064
+ return !!base && new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRe(base)}`, "u").test(hay);
27065
+ });
27066
+ }
27067
+ function nameTerms(m) {
27068
+ const slug = (m.name ?? "").split("/").pop() ?? "";
27069
+ let tags = [];
27070
+ try {
27071
+ tags = m.tags ? JSON.parse(m.tags) : [];
27072
+ } catch {
27073
+ }
27074
+ return [...slug.toLowerCase().split(/[-._]+/), ...tags.map((t) => t.toLowerCase())].filter(
27075
+ (t) => t.length >= 4
27076
+ );
27077
+ }
27078
+ function namesMemory(term, names) {
27079
+ const t = term.replace(/\*$/, "");
27080
+ if (t.length < 4) return false;
27081
+ return names.some((n) => t === n || t.includes(n) || n.includes(t));
27082
+ }
27083
+ function qualifies(matched, need, termCount, m, text) {
27084
+ if (matched.length >= need) return { ok: true };
27085
+ if (m?.name && text?.toLowerCase().includes(m.name)) return { ok: true, why: "named" };
27086
+ if (m && matched.length >= 1 && matched.length / Math.max(1, termCount) >= NAME_MATCH_RATIO) {
27087
+ const names = nameTerms(m);
27088
+ const hit = matched.find((t) => namesMemory(t, names));
27089
+ if (hit) return { ok: true, why: `slug/tag "${hit}"` };
27090
+ }
27091
+ return { ok: false, why: `${matched.length}/${need} terms` };
27092
+ }
27027
27093
  async function recallForText(db, text, opts = {}) {
27028
27094
  const maxMemories = opts.maxMemories ?? 3;
27029
27095
  const maxDocs = opts.maxDocs ?? 2;
27030
27096
  const terms = promptTerms(text);
27031
- if (terms.length < 2 || maxMemories <= 0 && maxDocs <= 0) return { text: "", keys: [] };
27097
+ const trace = opts.trace ? { path: opts.path ?? "prompt", terms, need: 0, candidates: [] } : void 0;
27098
+ const quiet = () => ({ text: "", keys: [], trace });
27099
+ if (terms.length < 2 || maxMemories <= 0 && maxDocs <= 0) return quiet();
27032
27100
  const need = opts.minMatches ?? requiredMatches(terms.length);
27101
+ if (trace) trace.need = need;
27033
27102
  const query = terms.join(" ");
27034
- const seen = opts.sessionId ? shownKeys(db, opts.sessionId) : /* @__PURE__ */ new Set();
27103
+ const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
27035
27104
  const lines = [];
27036
27105
  const keys = [];
27106
+ const note = (c) => trace?.candidates.push(c);
27037
27107
  if (maxMemories > 0) {
27038
27108
  const r = await search(db, query, {
27039
27109
  mode: "lexical",
@@ -27042,14 +27112,38 @@ async function recallForText(db, text, opts = {}) {
27042
27112
  filters: { source: "memory" }
27043
27113
  });
27044
27114
  let n = 0;
27115
+ const considered = /* @__PURE__ */ new Set();
27045
27116
  for (const h of r.hits) {
27046
- if (n >= maxMemories) break;
27047
- if (h.matched.length < need) continue;
27048
27117
  const uri = h.chunk.document.uri;
27049
27118
  const name = uri?.startsWith("memory:") ? uri.slice(7) : void 0;
27050
27119
  const m = name ? db.memoryGetByName(name) : void 0;
27051
27120
  const key = `memory:${name}`;
27052
- if (!m?.name || !live(m) || !specific(m) || seen.has(key) || keys.includes(key)) continue;
27121
+ if (!m?.name || considered.has(key)) continue;
27122
+ considered.add(key);
27123
+ const label = `${m.name} [${m.kind}]`;
27124
+ const matched = sharedTerms(terms, m.name, m.content, m.tags ?? "");
27125
+ if (!live(m)) {
27126
+ note({ key, label, matched, outcome: "skipped", why: m.status ?? "retired" });
27127
+ continue;
27128
+ }
27129
+ if (!specific(m)) {
27130
+ note({ key, label, matched, outcome: "skipped", why: "onboarding" });
27131
+ continue;
27132
+ }
27133
+ const q = qualifies(matched, need, terms.length, m, text);
27134
+ if (!q.ok) {
27135
+ note({ key, label, matched, outcome: "gated", why: q.why });
27136
+ continue;
27137
+ }
27138
+ if (seen.has(key)) {
27139
+ note({ key, label, matched, outcome: "seen", why: q.why });
27140
+ continue;
27141
+ }
27142
+ if (n >= maxMemories) {
27143
+ note({ key, label, matched, outcome: "over-limit", why: q.why });
27144
+ continue;
27145
+ }
27146
+ note({ key, label, matched, outcome: "shown", why: q.why });
27053
27147
  lines.push(`- memory ${m.name} [${m.kind}]: ${gist(m)}`);
27054
27148
  keys.push(key);
27055
27149
  n++;
@@ -27064,28 +27158,62 @@ async function recallForText(db, text, opts = {}) {
27064
27158
  let n = 0;
27065
27159
  const docsSeen = /* @__PURE__ */ new Set();
27066
27160
  for (const h of r.hits) {
27067
- if (n >= maxDocs) break;
27068
- if (h.matched.length < need) continue;
27069
27161
  const d = h.chunk.document;
27070
27162
  const key = `doc:${d.id}`;
27071
- if (docsSeen.has(d.id) || seen.has(key)) continue;
27163
+ if (docsSeen.has(d.id)) continue;
27072
27164
  docsSeen.add(d.id);
27165
+ const label = `${d.source} \u203A ${clip2(d.title, 60)}`;
27166
+ const matched = sharedTerms(terms, h.chunk.title, h.chunk.body);
27167
+ if (matched.length < need) {
27168
+ note({ key, label, matched, outcome: "gated", why: `${matched.length}/${need} terms` });
27169
+ continue;
27170
+ }
27171
+ if (seen.has(key)) {
27172
+ note({ key, label, matched, outcome: "seen" });
27173
+ continue;
27174
+ }
27175
+ if (n >= maxDocs) {
27176
+ note({ key, label, matched, outcome: "over-limit" });
27177
+ continue;
27178
+ }
27179
+ note({ key, label, matched, outcome: "shown" });
27073
27180
  lines.push(`- ${d.source} \u203A ${clip2(d.title, 80)}: ${clip2(h.snippet, 160)}`);
27074
27181
  keys.push(key);
27075
27182
  n++;
27076
27183
  }
27077
27184
  }
27078
- if (!lines.length) return { text: "", keys: [] };
27185
+ if (!lines.length) return quiet();
27079
27186
  return {
27080
27187
  text: [
27081
27188
  "eminiar recall \u2014 project memories and indexed docs that match this request (check them before deciding; superseded ones are omitted):",
27082
27189
  ...lines,
27083
27190
  'Full text: em_memory {action:"read", name} for a memory, em_docs {action:"lookup", query} for docs; em_recall {query} searches further.'
27084
27191
  ].join("\n"),
27085
- keys
27192
+ keys,
27193
+ trace
27086
27194
  };
27087
27195
  }
27088
- function mentionsFile(m, rel) {
27196
+ var SYMBOL_KINDS = [
27197
+ "function",
27198
+ "class",
27199
+ "method",
27200
+ "interface",
27201
+ "trait",
27202
+ "type",
27203
+ "enum",
27204
+ "variable"
27205
+ ];
27206
+ function fileSymbols(db, rel) {
27207
+ try {
27208
+ return db.graphFindNodes({ path: rel, kinds: SYMBOL_KINDS, limit: 400 }).filter((n) => n.path === rel).map((n) => n.name);
27209
+ } catch {
27210
+ return [];
27211
+ }
27212
+ }
27213
+ function escapeRe(s) {
27214
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27215
+ }
27216
+ function linksFile(m, rel) {
27089
27217
  if (!m.node_ids) return false;
27090
27218
  try {
27091
27219
  return JSON.parse(m.node_ids).some((id) => fileOf(id) === rel);
@@ -27093,23 +27221,50 @@ function mentionsFile(m, rel) {
27093
27221
  return false;
27094
27222
  }
27095
27223
  }
27224
+ function bodyNamesPath(body, rel) {
27225
+ return new RegExp(`(?:^|[^\\w/.-]|\\./)${escapeRe(rel)}(?![\\w-])`, "m").test(body);
27226
+ }
27227
+ function symbolMentionPattern(symbols) {
27228
+ const names = [...new Set(symbols)].filter((s) => /^[A-Za-z_$][\w$]{4,}$/.test(s));
27229
+ if (!names.length) return void 0;
27230
+ const alt = names.map(escapeRe).join("|");
27231
+ return new RegExp(`(?:[\`#.](?:${alt})(?!\\w)|(?<!\\w)(?:${alt})\\()`);
27232
+ }
27096
27233
  function memoriesForFile(db, rel, opts = {}) {
27097
- const linked = db.memoryList({ limit: 1e3 }).filter((m) => live(m) && specific(m) && mentionsFile(m, rel)).sort((a, b) => kindRank(a.kind) - kindRank(b.kind));
27098
- if (!linked.length) return { text: "", keys: [] };
27099
- const seen = opts.sessionId ? shownKeys(db, opts.sessionId) : /* @__PURE__ */ new Set();
27234
+ const symRe = opts.symbols?.length ? symbolMentionPattern(opts.symbols) : void 0;
27235
+ const how = (m) => {
27236
+ if (linksFile(m, rel)) return "related";
27237
+ if (bodyNamesPath(m.content, rel)) return "names the path";
27238
+ if (symRe?.test(m.content)) return "names a symbol";
27239
+ return void 0;
27240
+ };
27241
+ const trace = opts.trace ? { path: "file", terms: [rel], need: 0, candidates: [] } : void 0;
27242
+ const linked = db.memoryList({ limit: 1e3 }).map((m) => ({ m, why: how(m) })).filter((x) => !!x.why && live(x.m) && specific(x.m)).sort((a, b) => kindRank(a.m.kind) - kindRank(b.m.kind));
27243
+ if (!linked.length) return { text: "", keys: [], trace };
27244
+ const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
27100
27245
  const lines = [];
27101
27246
  const keys = [];
27102
- for (const m of linked) {
27103
- if (lines.length >= (opts.max ?? 3)) break;
27247
+ for (const { m, why } of linked) {
27104
27248
  const key = `memory:${m.name}`;
27105
- if (!m.name || seen.has(key)) continue;
27249
+ const label = `${m.name} [${m.kind}]`;
27250
+ if (!m.name) continue;
27251
+ if (seen.has(key)) {
27252
+ trace?.candidates.push({ key, label, matched: [rel], outcome: "seen", why });
27253
+ continue;
27254
+ }
27255
+ if (lines.length >= (opts.max ?? 3)) {
27256
+ trace?.candidates.push({ key, label, matched: [rel], outcome: "over-limit", why });
27257
+ continue;
27258
+ }
27259
+ trace?.candidates.push({ key, label, matched: [rel], outcome: "shown", why });
27106
27260
  lines.push(`${m.name} [${m.kind}]: ${gist(m, 140)}`);
27107
27261
  keys.push(key);
27108
27262
  }
27109
- if (!lines.length) return { text: "", keys: [] };
27263
+ if (!lines.length) return { text: "", keys: [], trace };
27110
27264
  return {
27111
27265
  text: `eminiar memory: ${rel} is covered by ${lines.join(" \xB7 ")} \u2014 respect it (em_memory {action:"read", name} for the full text) or update the memory if this change overrides it.`,
27112
- keys
27266
+ keys,
27267
+ trace
27113
27268
  };
27114
27269
  }
27115
27270
  var RISKY = 0.3;
@@ -27125,22 +27280,32 @@ function riskLine(path2, f) {
27125
27280
  return `${path2} is bug-prone (${parts.join("; ")})`;
27126
27281
  }
27127
27282
  function fileContext(db, rel, opts = {}) {
27128
- const key = `file:${rel}`;
27129
- if (opts.sessionId && shownKeys(db, opts.sessionId).has(key)) return { text: "", keys: [] };
27283
+ const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
27130
27284
  const lines = [];
27131
- const f = readFileRisk(db)[rel];
27132
- const risk = f ? riskLine(rel, f) : void 0;
27133
- if (risk)
27134
- lines.push(
27135
- `eminiar history: ${risk}. em_history {action:"file", path:"${rel}"} shows those fixes; keep the behaviour they restored covered by a test.`
27136
- );
27137
- const fanIn = db.metricsGet([fileId(rel)])[0]?.fan_in ?? 0;
27138
- if (fanIn >= FAN_IN)
27139
- lines.push(
27140
- `eminiar graph: ${rel} is imported by ${fanIn} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
27141
- );
27285
+ const keys = [];
27286
+ const histKey = `hist:${rel}`;
27287
+ if (!seen.has(histKey)) {
27288
+ const f = readFileRisk(db)[rel];
27289
+ const risk = f ? riskLine(rel, f) : void 0;
27290
+ if (risk) {
27291
+ lines.push(
27292
+ `eminiar history: ${risk}. em_history {action:"file", path:"${rel}"} shows those fixes; keep the behaviour they restored covered by a test.`
27293
+ );
27294
+ keys.push(histKey);
27295
+ }
27296
+ }
27297
+ const fanKey = `fanin:${rel}`;
27298
+ if (opts.edit && !seen.has(fanKey)) {
27299
+ const fanIn = db.metricsGet([fileId(rel)])[0]?.fan_in ?? 0;
27300
+ if (fanIn >= FAN_IN) {
27301
+ lines.push(
27302
+ `eminiar graph: ${rel} is imported by ${fanIn} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
27303
+ );
27304
+ keys.push(fanKey);
27305
+ }
27306
+ }
27142
27307
  if (!lines.length) return { text: "", keys: [] };
27143
- return { text: lines.join("\n"), keys: [key] };
27308
+ return { text: lines.join("\n"), keys };
27144
27309
  }
27145
27310
  function commitContext(db, files) {
27146
27311
  if (!files.length) return "";
@@ -27197,7 +27362,7 @@ async function recallForError(db, error62, opts = {}) {
27197
27362
  const terms = promptTerms(error62.slice(0, 600)).filter((t) => !ERROR_FILLER.has(t));
27198
27363
  if (terms.length < 3) return { text: "", keys: [] };
27199
27364
  const need = Math.max(3, Math.ceil(terms.length * 0.4));
27200
- const seen = opts.sessionId ? shownKeys(db, opts.sessionId) : /* @__PURE__ */ new Set();
27365
+ const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
27201
27366
  const past = [];
27202
27367
  const keys = [];
27203
27368
  const r = await search(db, terms.join(" "), {
@@ -27223,8 +27388,10 @@ async function recallForError(db, error62, opts = {}) {
27223
27388
  maxMemories: opts.maxMemories ?? 2,
27224
27389
  maxDocs: 0,
27225
27390
  sessionId: opts.sessionId,
27391
+ repeatAfter: opts.repeatAfter,
27226
27392
  // error text is wordy (paths, module names): the same proportional bar as past errors
27227
- minMatches: need
27393
+ minMatches: need,
27394
+ path: "error"
27228
27395
  });
27229
27396
  const memLines = mem.text.split("\n").filter((l) => l.startsWith("- memory "));
27230
27397
  if (!past.length && !memLines.length) return { text: "", keys: [] };
@@ -28720,10 +28887,13 @@ var userPromptSubmit = async (input2) => {
28720
28887
  const r = await recallForText(db, input2.prompt, {
28721
28888
  maxMemories: recall.maxMemories,
28722
28889
  maxDocs: recall.maxDocs,
28723
- sessionId
28890
+ sessionId,
28891
+ repeatAfter: recall.repeatAfter,
28892
+ trace: true,
28893
+ path: "prompt"
28724
28894
  });
28895
+ recordShown(db, sessionId, r.keys, { path: "prompt", trace: r.trace });
28725
28896
  if (!r.text) return void 0;
28726
- recordShown(db, sessionId, r.keys);
28727
28897
  return {
28728
28898
  hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: r.text }
28729
28899
  };
@@ -28773,11 +28943,12 @@ var postToolUseFailure = async (input2) => {
28773
28943
  try {
28774
28944
  const r = await recallForError(db, input2.error, {
28775
28945
  sessionId,
28776
- maxMemories: Math.min(2, recall.maxMemories)
28946
+ maxMemories: Math.min(2, recall.maxMemories),
28947
+ repeatAfter: recall.repeatAfter
28777
28948
  });
28778
28949
  if (r.text) {
28779
28950
  recalled = r.text;
28780
- recordShown(db, sessionId, r.keys);
28951
+ recordShown(db, sessionId, r.keys, { path: "error" });
28781
28952
  }
28782
28953
  } catch {
28783
28954
  }
@@ -29011,7 +29182,7 @@ function parseRule(raw) {
29011
29182
  if (!m) return void 0;
29012
29183
  return { raw, tool: m[1], pattern: m[2] };
29013
29184
  }
29014
- function escapeRe(s) {
29185
+ function escapeRe2(s) {
29015
29186
  return s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
29016
29187
  }
29017
29188
  var readRegexCache = /* @__PURE__ */ new Map();
@@ -29020,7 +29191,7 @@ function readRegex(pattern) {
29020
29191
  if (!re) {
29021
29192
  if (readRegexCache.size >= 1024) readRegexCache.clear();
29022
29193
  re = new RegExp(
29023
- `^${pattern.split("**").map((seg) => seg.split("*").map(escapeRe).join("[^/]*")).join(".*")}$`
29194
+ `^${pattern.split("**").map((seg) => seg.split("*").map(escapeRe2).join("[^/]*")).join(".*")}$`
29024
29195
  );
29025
29196
  readRegexCache.set(pattern, re);
29026
29197
  }
@@ -29326,7 +29497,7 @@ function hookLog(level, msg) {
29326
29497
  }
29327
29498
 
29328
29499
  // src/hooks/routing.ts
29329
- var SYMBOL_KINDS = [
29500
+ var SYMBOL_KINDS2 = [
29330
29501
  "function",
29331
29502
  "class",
29332
29503
  "method",
@@ -29344,7 +29515,7 @@ function literalOf(pattern) {
29344
29515
  function symbolHints(db, pattern) {
29345
29516
  const lit = literalOf(pattern);
29346
29517
  if (!lit) return void 0;
29347
- const nodes = db.graphFindNodes({ query: lit, kinds: SYMBOL_KINDS, limit: 5 });
29518
+ const nodes = db.graphFindNodes({ query: lit, kinds: SYMBOL_KINDS2, limit: 5 });
29348
29519
  if (!nodes.length) return void 0;
29349
29520
  const list = nodes.map((n) => `${n.kind} ${n.name} (${n.path}:${n.start_line ?? "?"})`).join("; ");
29350
29521
  return `eminiar graph: symbols matching "${lit}": ${list}. em_symbols {name_path, include_body: true} reads one; {mode:"references"} lists its callers.`;
@@ -29354,6 +29525,47 @@ function coverageHint(db, rel) {
29354
29525
  if (!row || row.status === "indexed") return void 0;
29355
29526
  return `eminiar graph: ${rel} is ${row.status}${row.reason ? ` (${row.reason})` : ""}; graph answers about it may be incomplete.`;
29356
29527
  }
29528
+ var GIT_HISTORY = /(?:^|[;&|]\s*)git\s+(?:(?:-C|--git-dir|--work-tree)[= ]\S+\s+|--no-pager\s+)*(log|blame|shortlog|show|whatchanged|reflog)\b([^;&|]*)/;
29529
+ var ONE_COMMIT = /(^|\s)(-1|-n\s*1|-n1|--max-count[= ]1)(?=\s|$)/;
29530
+ function historyCall(cmd) {
29531
+ const m = GIT_HISTORY.exec(cmd);
29532
+ if (!m) return void 0;
29533
+ const sub = m[1];
29534
+ const rest = (m[2] ?? "").trim();
29535
+ const args = rest.split(/\s+/).filter(Boolean);
29536
+ const q = (s) => JSON.stringify(s.replace(/^['"]|['"]$/g, ""));
29537
+ const positional = (a) => !a.startsWith("-");
29538
+ if (sub === "show") {
29539
+ if (/\S+:\S+/.test(rest)) return void 0;
29540
+ const sha = args.find(positional);
29541
+ return `{action:"commit", sha:${q(sha ?? "HEAD")}}`;
29542
+ }
29543
+ if (sub === "blame") {
29544
+ const path3 = args.filter(positional).pop();
29545
+ return path3 ? `{action:"file", path:${q(path3)}}` : void 0;
29546
+ }
29547
+ if (sub === "log" && ONE_COMMIT.test(rest) && !/(^|\s)(-p|--patch|--stat)\b/.test(rest))
29548
+ return void 0;
29549
+ const lineRange = /-L\s*:?([^:\s]+):(\S+)/.exec(rest);
29550
+ if (lineRange) {
29551
+ const [, fn, file3] = lineRange;
29552
+ return /^[A-Za-z_$]/.test(fn) ? `{action:"symbol", name_path:${q(`${file3}#${fn}`)}}` : `{action:"file", path:${q(file3)}}`;
29553
+ }
29554
+ const grep = /--grep[= ]("[^"]*"|'[^']*'|\S+)/.exec(rest);
29555
+ if (grep) return `{action:"search", query:${q(grep[1])}}`;
29556
+ const author = /--author[= ]("[^"]*"|'[^']*'|\S+)/.exec(rest);
29557
+ const range = args.find((a) => positional(a) && /\.\.\.?/.test(a));
29558
+ if (range) {
29559
+ const [a, b] = range.split(/\.\.\.?/);
29560
+ return `{action:"branch", name:${q(b || a || "HEAD")}}`;
29561
+ }
29562
+ const dash = args.indexOf("--");
29563
+ const path2 = dash !== -1 ? args[dash + 1] : args.find((a) => positional(a) && /[/.]/.test(a) && !/^[0-9a-f]{7,40}$/i.test(a));
29564
+ if (path2) return `{action:"file", path:${q(path2)}}`;
29565
+ if (author) return `{action:"timeline", author:${q(author[1])}}`;
29566
+ if (sub === "shortlog") return `{action:"timeline"}`;
29567
+ return `{action:"overview"}`;
29568
+ }
29357
29569
  function docsSources(root) {
29358
29570
  try {
29359
29571
  return readManifestAt(join11(root, ".eminiar", "docs.json")).sources;
@@ -29397,12 +29609,21 @@ var BIG_TEXT_EXT = /* @__PURE__ */ new Set([
29397
29609
  function withoutHeredocs(cmd) {
29398
29610
  return cmd.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?=\n|$)/g, "<<$2");
29399
29611
  }
29400
- function suggest(toolName, input2, root) {
29612
+ function suggest(toolName, input2, root, extra = {}) {
29401
29613
  switch (toolName) {
29402
29614
  case "Bash": {
29403
29615
  const cmd = withoutHeredocs(String(input2.command ?? ""));
29404
29616
  if (!cmd || TINY_BASH.test(cmd)) return void 0;
29405
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;
29406
29627
  if (HEAVY_BASH.test(cmd) || /\|\s*(head|tail|grep|wc|sort|uniq|jq|less)\b/.test(cmd)) {
29407
29628
  return {
29408
29629
  tool: "em_execute",
@@ -29430,10 +29651,18 @@ function suggest(toolName, input2, root) {
29430
29651
  return void 0;
29431
29652
  }
29432
29653
  case "Grep": {
29433
- if (input2.output_mode === "content" && !input2.head_limit) {
29654
+ const content = input2.output_mode === "content" && !input2.head_limit;
29655
+ if (extra.symbol) {
29656
+ return {
29657
+ tool: "em_symbols",
29658
+ annotateOnly: !content,
29659
+ 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
+ };
29661
+ }
29662
+ if (content) {
29434
29663
  return {
29435
29664
  tool: "em_execute",
29436
- reason: `content-mode grep can be large; em_execute with rg and an intent returns only matching sections`
29665
+ 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)`
29437
29666
  };
29438
29667
  }
29439
29668
  return void 0;
@@ -29461,7 +29690,7 @@ function suggest(toolName, input2, root) {
29461
29690
  return {
29462
29691
  tool: "em_*",
29463
29692
  reason: "subagents should prefer em_execute / em_batch_execute / em_search over Bash, Read and Grep",
29464
- prefixPrompt: 'Use the eminiar tools (em_execute, em_batch_execute, em_execute_file, em_fetch_and_index, em_search) instead of Bash/Read/Grep/WebFetch whenever output may be large; return only derived answers. Check em_recall {query} for project decisions and constraints before proposing changes, and em_docs {action:"lookup", query} before WebFetch/WebSearch for anything the project depends on.\n\n'
29693
+ prefixPrompt: 'Use the eminiar tools (em_execute, em_batch_execute, em_execute_file, em_fetch_and_index, em_search, em_symbols, em_history) instead of Bash/Read/Grep/WebFetch whenever output may be large; return only derived answers. Check em_recall {query} for project decisions and constraints before proposing changes, em_history {action:"file", path} before editing a file, em_symbols / em_search {source:"code"} before grepping for an identifier, and em_docs {action:"lookup", query} before WebFetch/WebSearch for anything the project depends on.\n\n'
29465
29694
  };
29466
29695
  default:
29467
29696
  return void 0;
@@ -29502,15 +29731,20 @@ function fileKnowledge(ctx, input2) {
29502
29731
  if (!rel) return void 0;
29503
29732
  const db = getStorage(ctx, { vectors: false });
29504
29733
  const sessionId = input2.session_id || void 0;
29505
- const results = [memoriesForFile(db, rel, { sessionId })];
29506
- if (EDIT_TOOLS2.has(input2.tool_name ?? "")) results.push(fileContext(db, rel, { sessionId }));
29734
+ const repeatAfter = ctx.config.recall.repeatAfter;
29735
+ const edit = EDIT_TOOLS2.has(input2.tool_name ?? "");
29736
+ const results = [
29737
+ memoriesForFile(db, rel, { sessionId, symbols: fileSymbols(db, rel), repeatAfter }),
29738
+ fileContext(db, rel, { sessionId, edit, repeatAfter })
29739
+ ];
29507
29740
  const shown = results.filter((r) => r.text);
29508
29741
  if (!shown.length) return void 0;
29509
29742
  if (sessionId)
29510
29743
  recordShown(
29511
29744
  db,
29512
29745
  sessionId,
29513
- shown.flatMap((r) => r.keys)
29746
+ shown.flatMap((r) => r.keys),
29747
+ { path: "file" }
29514
29748
  );
29515
29749
  return shown.map((r) => r.text).join("\n");
29516
29750
  } catch {
@@ -29550,7 +29784,7 @@ async function webKnowledge(ctx, input2) {
29550
29784
  if (!text.trim()) return void 0;
29551
29785
  try {
29552
29786
  const db = getStorage(ctx, { vectors: false });
29553
- const r = await recallForText(db, text, { maxMemories: 0, maxDocs: 3 });
29787
+ const r = await recallForText(db, text, { maxMemories: 0, maxDocs: 3, path: "web" });
29554
29788
  if (!r.text) return void 0;
29555
29789
  const pages = r.text.split("\n").filter((l) => l.startsWith("- docs:"));
29556
29790
  return [
@@ -29593,8 +29827,10 @@ var preToolUse = async (input2) => {
29593
29827
  const coexisting = contextModeInstalled() && ctx.config.routing.deferToContextMode;
29594
29828
  if (coexisting && ["Bash", "Read", "Grep", "WebFetch"].includes(input2.tool_name))
29595
29829
  return withContext(void 0, memo2);
29596
- const hint = joinHints(graphContext(ctx, input2), memo2);
29597
- const s = suggest(input2.tool_name, input2.tool_input, ctx.root);
29830
+ const graph = graphContext(ctx, input2);
29831
+ const hint = joinHints(graph, memo2);
29832
+ 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 });
29598
29834
  if (!s) return withContext(void 0, hint);
29599
29835
  if (s.annotateOnly) return withContext(annotate(s), hint);
29600
29836
  if (mode === "enforce") {
@@ -29651,7 +29887,8 @@ Verify structure with em_symbols / em_graph_impact before reading files; check e
29651
29887
  if (recall.onAgent && task) {
29652
29888
  const r = await recallForText(db, task, {
29653
29889
  maxMemories: recall.maxMemories,
29654
- maxDocs: recall.maxDocs
29890
+ maxDocs: recall.maxDocs,
29891
+ path: "agent"
29655
29892
  });
29656
29893
  if (r.text) parts.push(r.text);
29657
29894
  }
@@ -29708,7 +29945,8 @@ function routingGuidance(mode) {
29708
29945
  '- em_search {queries, sort:"timeline"}: everything indexed plus session memory.',
29709
29946
  "- em_remember {content, kind, name?, status?}: memories are markdown under .eminiar/memories (survive compaction, shareable via git); em_memory {action: list|read|edit|rename|delete|status} manages them.",
29710
29947
  '- em_architecture {aspects?}: the project map (folders, entry points, routes, hotspots, decisions) in one call; em_symbols {name_path | mode:"overview", file} for a symbol or a file outline; em_graph_impact {node} before a refactor; em_graph_changes for what the current diff reaches; em_graph_query {pattern | preset} to walk the graph; em_index_status for coverage. em_stats shows savings.',
29711
- "- em_history {action: overview | file {path} | risk | commit {sha} | branch {name} | symbol {name_path} | search {query}}: the git history classified (feat/fix/refactor\u2026), branches vs base, and each fix traced back to the commit that caused it \u2014 ask it before editing a file or committing instead of running git log/blame; commit messages are searchable via em_search (source git).",
29948
+ "- em_history {action: overview | file {path} | risk | commit {sha} | branch {name} | symbol {name_path} | search {query}}: the git history classified (feat/fix/refactor\u2026), branches vs base, and each fix traced back to the commit that caused it \u2014 ask it before editing a file or committing instead of running git log/blame/show (those are routed to it); commit messages are searchable via em_search (source git).",
29949
+ '- A Grep for an identifier the graph knows is routed to em_symbols {name_path, mode:"references"} / em_search {queries, source:"code"}: symbol-level answers instead of raw matches.',
29712
29950
  "Plain Bash/Read/Grep remain right for short fixed output and files you are about to edit."
29713
29951
  ].join("\n");
29714
29952
  }
@@ -29775,11 +30013,22 @@ ${guide}`);
29775
30013
  );
29776
30014
  } catch {
29777
30015
  }
30016
+ try {
30017
+ if (existsSync9(join13(ctx.root, ".git")) && !db.metaGet("history_files"))
30018
+ parts.push(
30019
+ '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.'
30020
+ );
30021
+ } catch {
30022
+ }
29778
30023
  const docs = db.countDocuments();
29779
30024
  const latest = db.sessionLatest();
29780
30025
  parts.push(
29781
30026
  `eminiar index: ${docs} documents${latest && latest.id !== sessionId && latest.started_at ? `; previous session ${new Date(latest.started_at).toISOString().slice(0, 16)}` : ""}. Use em_search {sort:"timeline"} or em_recall to pick up where you left off.`
29782
30027
  );
30028
+ hookLog(
30029
+ "info",
30030
+ `session_start source=${input2.source ?? "?"} session=${sessionId.slice(0, 8)} root=${ctx.root} db=${ctx.dbPath} documents=${docs} memories=${db.memoryCount()} history=${db.metaGet("history_files") ? "yes" : "no"}`
30031
+ );
29783
30032
  return {
29784
30033
  hookSpecificOutput: {
29785
30034
  hookEventName: "SessionStart",