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/server.js CHANGED
@@ -18018,6 +18018,7 @@ __export(store_exports, {
18018
18018
  autoName: () => autoName,
18019
18019
  edit: () => edit,
18020
18020
  gitignoreHint: () => gitignoreHint,
18021
+ impliedFileLinks: () => impliedFileLinks,
18021
18022
  isKind: () => isKind,
18022
18023
  isStatus: () => isStatus,
18023
18024
  isValidName: () => isValidName,
@@ -18116,6 +18117,20 @@ function memoryLinks(body, self) {
18116
18117
  }
18117
18118
  return [...out];
18118
18119
  }
18120
+ function impliedFileLinks(root, body, skipPrefix) {
18121
+ const out = /* @__PURE__ */ new Set();
18122
+ for (const m of body.matchAll(PATH_RE)) {
18123
+ const p = m[1].replace(/[.,;:)]+$/, "");
18124
+ if (!p || p.startsWith("..") || out.has(p) || skipPrefix && p.startsWith(`${skipPrefix}/`))
18125
+ continue;
18126
+ try {
18127
+ if (statSync6(join12(root, p)).isFile()) out.add(p);
18128
+ } catch {
18129
+ }
18130
+ if (out.size >= 30) break;
18131
+ }
18132
+ return [...out].map((p) => `file:${p}`);
18133
+ }
18119
18134
  function writeMemoryFile(ctx, name, meta4, body) {
18120
18135
  const abs = absOf(ctx, name);
18121
18136
  mkdirSync3(dirname7(abs), { recursive: true });
@@ -18138,13 +18153,14 @@ function syncGraph(db, name, rel, g) {
18138
18153
  ]);
18139
18154
  const edges = [];
18140
18155
  const seen = /* @__PURE__ */ new Set();
18141
- const push = (dst, kind) => {
18156
+ const push = (dst, kind, confidence = 1) => {
18142
18157
  const key = `${kind}|${dst}`;
18143
18158
  if (dst === self || seen.has(key)) return;
18144
18159
  seen.add(key);
18145
- edges.push({ src: self, dst, kind, source: "curated", confidence: 1, weight: 1 });
18160
+ edges.push({ src: self, dst, kind, source: "curated", confidence, weight: 1 });
18146
18161
  };
18147
18162
  for (const id of g.related) push(relatedTarget(id), "mentions");
18163
+ for (const id of g.implied ?? []) push(id, "mentions", 0.6);
18148
18164
  for (const other of memoryLinks(g.body, name)) push(memoryId(other), "links");
18149
18165
  if (g.supersedes) push(memoryId(g.supersedes.replace(/^mem(?:ory)?:/, "")), "supersedes");
18150
18166
  if (edges.length) db.graphUpsertEdges(edges);
@@ -18205,6 +18221,9 @@ ${parsed.body.replace(/^\s*\n/, "")}`
18205
18221
  hash2 = hashText(text2);
18206
18222
  }
18207
18223
  const body = parsed.body.trim();
18224
+ const implied = impliedFileLinks(ctx.root, body, memoriesRel(ctx)).filter(
18225
+ (id) => !related.includes(id)
18226
+ );
18208
18227
  const doc = indexContent(db, {
18209
18228
  source: "memory",
18210
18229
  uri: `memory:${name}`,
@@ -18221,7 +18240,8 @@ tags: ${tags.join(", ")}` : ""}`,
18221
18240
  kind,
18222
18241
  content: body,
18223
18242
  tags,
18224
- nodeIds: related,
18243
+ // the header's deliberate list first, then the files the body names
18244
+ nodeIds: [...related, ...implied],
18225
18245
  sessionId: opts.sessionId,
18226
18246
  documentId: doc.documentId,
18227
18247
  path: rel,
@@ -18233,7 +18253,7 @@ tags: ${tags.join(", ")}` : ""}`,
18233
18253
  status,
18234
18254
  supersedes
18235
18255
  });
18236
- syncGraph(db, name, rel, { kind, tags, related, status, supersedes, body });
18256
+ syncGraph(db, name, rel, { kind, tags, related, implied, status, supersedes, body });
18237
18257
  return { changed: true, row: db.memoryGetByName(name) };
18238
18258
  }
18239
18259
  function setMeta(ctx, db, name, patch) {
@@ -18436,7 +18456,7 @@ function gitignoreHint(ctx) {
18436
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}/**`);
18437
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;
18438
18458
  }
18439
- var import_fast_glob3, MEMORY_KINDS, SEGMENT, NAME_RE, MEM_LINK_RE, hinted;
18459
+ var import_fast_glob3, MEMORY_KINDS, SEGMENT, NAME_RE, MEM_LINK_RE, PATH_RE, hinted;
18440
18460
  var init_store = __esm({
18441
18461
  "src/memory/store.ts"() {
18442
18462
  "use strict";
@@ -18459,6 +18479,7 @@ var init_store = __esm({
18459
18479
  SEGMENT = "[a-z0-9][a-z0-9._-]*";
18460
18480
  NAME_RE = new RegExp(`^(?:${SEGMENT}/)?${SEGMENT}$`);
18461
18481
  MEM_LINK_RE = /(?<![A-Za-z0-9_./-])mem:([a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)?)/g;
18482
+ PATH_RE = /(?<![\w/.-])(?:\.\/)?((?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,8})(?![\w/-])/g;
18462
18483
  hinted = /* @__PURE__ */ new Set();
18463
18484
  }
18464
18485
  });
@@ -49189,7 +49210,12 @@ var ConfigSchema = external_exports.object({
49189
49210
  /** Before WebSearch / WebFetch: the indexed doc pages that already answer it. */
49190
49211
  onWeb: external_exports.boolean().default(true),
49191
49212
  maxMemories: external_exports.number().int().min(0).max(10).default(3),
49192
- maxDocs: external_exports.number().int().min(0).max(10).default(2)
49213
+ maxDocs: external_exports.number().int().min(0).max(10).default(2),
49214
+ /**
49215
+ * Prompts after which an item already shown in this context window may be injected
49216
+ * again when it matches (0 = once per context window only).
49217
+ */
49218
+ repeatAfter: external_exports.number().int().min(0).max(1e3).default(10)
49193
49219
  }).prefault({}),
49194
49220
  docs: external_exports.object({
49195
49221
  /** Project-relative manifest of documentation sources (`em_docs`); commit it. */
@@ -55338,7 +55364,7 @@ function categoryRank(id) {
55338
55364
  var DOCUMENTS_EDGE = "documents";
55339
55365
  var MAX_PAGES = 500;
55340
55366
  var MAX_EDGES = 400;
55341
- var PATH_RE = /(?:^|[\s(`'"[<])((?:\.{1,2}\/)*(?:[\w@.-]+\/)+[\w@.-]+\.[A-Za-z0-9]{1,8})(?=$|[\s)`'"\]>:#,;!?]|\.(?:\s|$))/gm;
55367
+ var PATH_RE2 = /(?:^|[\s(`'"[<])((?:\.{1,2}\/)*(?:[\w@.-]+\/)+[\w@.-]+\.[A-Za-z0-9]{1,8})(?=$|[\s)`'"\]>:#,;!?]|\.(?:\s|$))/gm;
55342
55368
  function manifestRel(ctx) {
55343
55369
  return relative8(ctx.root, manifestPath(ctx)).split(sep2).join("/");
55344
55370
  }
@@ -55358,7 +55384,7 @@ function packageCandidates(provider) {
55358
55384
  }
55359
55385
  function pathsIn(text2, pageRel, files) {
55360
55386
  const out = /* @__PURE__ */ new Set();
55361
- for (const m of text2.matchAll(PATH_RE)) {
55387
+ for (const m of text2.matchAll(PATH_RE2)) {
55362
55388
  const raw = m[1];
55363
55389
  const tries = [raw.replace(/^(\.\/)+/, "")];
55364
55390
  if (pageRel && raw.startsWith("."))
@@ -57980,7 +58006,7 @@ function parseWhen(s) {
57980
58006
  }
57981
58007
  var emHistory = defineTool({
57982
58008
  name: "em_history",
57983
- description: "Understand the project's git history instead of reading git log: commits classified by kind (feat/fix/refactor/\u2026), branches with ahead/behind and merge state, and a fix-origin trace that blames each fix's removed lines back to the commit that wrote them, so changes that later needed fixing are flagged. Use `overview` to orient, `file` before editing a file (its fixes, what broke after past changes, who knows it), `risk` for the files in the working tree (or given paths), `commit` for one change, `branch` to compare a branch with the base, `symbol` for a function's line history, `timeline`/`search` to find when and why something happened. Commit messages are also indexed for em_search (source `git`).",
58009
+ description: "Understand the project's git history instead of running git log/blame/show (the hooks route those here): commits classified by kind (feat/fix/refactor/\u2026), branches with ahead/behind and merge state, and a fix-origin trace that blames each fix's removed lines back to the commit that wrote them, so changes that later needed fixing are flagged. Use `overview` to orient (it also builds the table the hooks use to flag bug-prone files), `file` before editing a file (its fixes, what broke after past changes, who knows it), `risk` for the files in the working tree (or given paths), `commit` for one change, `branch` to compare a branch with the base, `symbol` for a function's line history, `timeline`/`search` to find when and why something happened. Commit messages are also indexed for em_search (source `git`).",
57984
58010
  schema: {
57985
58011
  action: external_exports.enum(["overview", "timeline", "search", "file", "commit", "branch", "risk", "symbol"]).optional().describe("default overview"),
57986
58012
  path: external_exports.string().optional().describe("file: the file; timeline: restrict to this path prefix"),
@@ -58336,7 +58362,7 @@ init_queue();
58336
58362
  init_store();
58337
58363
  var emRecall = defineTool({
58338
58364
  name: "em_recall",
58339
- description: 'Recall memories (.eminiar/memories, written by em_remember) and captured session history \u2014 decisions, constraints, errors and their fixes, rejected approaches \u2014 matching a query (hybrid keyword + semantic). Without a query, lists the most recent memories, onboarding first. Use at the start of a task to avoid re-deciding or re-breaking things; em_memory {action:"read"} shows a whole memory.',
58365
+ description: 'Call this first when a task starts, a design changes or an approach is chosen: it recalls memories (.eminiar/memories, written by em_remember) and captured session history \u2014 decisions, constraints, errors and their fixes, rejected approaches \u2014 matching a query (hybrid keyword + semantic), so nothing is re-decided or re-broken. Without a query, lists the most recent memories, onboarding first. The hooks also push matching memories on their own ("eminiar recall", "eminiar memory"); this tool searches further, with synonyms. em_memory {action:"read"} shows a whole memory.',
58340
58366
  schema: {
58341
58367
  query: external_exports.string().optional(),
58342
58368
  kind: external_exports.enum(MEMORY_KINDS).optional().describe("Restrict to one memory kind"),
@@ -58462,7 +58488,7 @@ function sourceFilter(source) {
58462
58488
  }
58463
58489
  var emSearch = defineTool({
58464
58490
  name: "em_search",
58465
- description: "Search everything eminiar has indexed: sandbox outputs, files, fetched pages, session memory, memories and code (chunks named by symbol, e.g. `src/auth.ts > Session.refresh`). Pass several queries at once \u2014 the round trip is paid once. Hybrid BM25 (porter + trigram) with typo correction; `sort: timeline` orders by recency (use after a resume); `mode: regex` scans indexed content with a JS regular expression and returns the matching lines with context (for ad-hoc greps over files not indexed, use em_execute with rg).",
58491
+ description: 'Search everything eminiar has indexed: sandbox outputs, files, fetched pages, session memory, memories, commit messages (source git) and code (chunks named by symbol, e.g. `src/auth.ts > Session.refresh`). Reach for it before Grep or Bash when looking for where something lives, what an earlier command printed, or what a past session found: `source: "code"` answers code questions by symbol, `sort: timeline` orders by recency (use after a resume), `mode: regex` scans indexed content with a JS regular expression and returns the matching lines with context (for ad-hoc greps over files not indexed, use em_execute with rg). Pass several queries at once \u2014 the round trip is paid once. Hybrid BM25 (porter + trigram) with typo correction.',
58466
58492
  schema: {
58467
58493
  queries: external_exports.array(external_exports.string().min(1)).min(1).describe("One or more search queries"),
58468
58494
  source: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().describe(
@@ -58522,9 +58548,60 @@ var emSearch = defineTool({
58522
58548
 
58523
58549
  // src/tools/em_stats.ts
58524
58550
  init_esm_shims();
58551
+
58552
+ // src/session/recall.ts
58553
+ init_esm_shims();
58554
+ init_ids();
58555
+ init_model();
58556
+ var RECALL_CATEGORY = "recall";
58557
+ function recallStats(events) {
58558
+ const byPath = {};
58559
+ const gated = /* @__PURE__ */ new Map();
58560
+ let decisions = 0;
58561
+ for (const e of events) {
58562
+ if (e.category !== RECALL_CATEGORY) continue;
58563
+ let p;
58564
+ try {
58565
+ p = JSON.parse(e.payload);
58566
+ } catch {
58567
+ continue;
58568
+ }
58569
+ decisions++;
58570
+ const path3 = p.path ?? "file";
58571
+ byPath[path3] ??= { fired: 0, quiet: 0, items: 0 };
58572
+ const row = byPath[path3];
58573
+ const n = p.keys?.length ?? 0;
58574
+ if (n) {
58575
+ row.fired++;
58576
+ row.items += n;
58577
+ } else row.quiet++;
58578
+ for (const g of p.gated ?? []) {
58579
+ const why = g.replace(/^\S+\s+/, "").replace(/\d+\/\d+ terms/, "too few terms");
58580
+ gated.set(why, (gated.get(why) ?? 0) + 1);
58581
+ }
58582
+ }
58583
+ return {
58584
+ decisions,
58585
+ byPath,
58586
+ gated: [...gated.entries()].map(([why, n]) => ({ why, n })).sort((a, b) => b.n - a.n).slice(0, 6)
58587
+ };
58588
+ }
58589
+ function formatRecallStats(title, s) {
58590
+ if (!s.decisions) return `${title}: no recall decisions recorded yet`;
58591
+ const paths2 = Object.entries(s.byPath).sort((a, b) => b[1].fired + b[1].quiet - (a[1].fired + a[1].quiet)).map(([path3, r]) => {
58592
+ const total = r.fired + r.quiet;
58593
+ return `${path3} ${r.fired}/${total} fired (${Math.round(100 * r.fired / total)}%, ${r.items} items)`;
58594
+ });
58595
+ const lines = [`${title}: ${s.decisions} decision(s) \u2014 ${paths2.join(" \xB7 ")}`];
58596
+ if (s.gated.length)
58597
+ lines.push(` held back: ${s.gated.map((g) => `${g.why} \xD7${g.n}`).join(", ")}`);
58598
+ return lines.join("\n");
58599
+ }
58600
+
58601
+ // src/tools/em_stats.ts
58525
58602
  var emStats = defineTool({
58526
58603
  name: "em_stats",
58527
- 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.",
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.",
58528
58605
  schema: {
58529
58606
  scope: external_exports.enum(["session", "project", "lifetime", "all"]).optional().describe("default all")
58530
58607
  },
@@ -58535,9 +58612,21 @@ var emStats = defineTool({
58535
58612
  const parts = [];
58536
58613
  if (scope === "session" || scope === "all") {
58537
58614
  parts.push(formatStats(`session ${sessionId.slice(0, 8)}`, db.statsBySession(sessionId)));
58615
+ parts.push(
58616
+ formatRecallStats(
58617
+ "recall (session)",
58618
+ recallStats(db.eventsBySession(sessionId, { categories: [RECALL_CATEGORY] }))
58619
+ )
58620
+ );
58538
58621
  }
58539
58622
  if (scope === "project" || scope === "all") {
58540
58623
  parts.push(formatStats(`project ${ctx.root}`, mergeByTool(db.statsAll())));
58624
+ parts.push(
58625
+ formatRecallStats(
58626
+ "recall (project, last 2000 decisions)",
58627
+ recallStats(db.eventsRecent(2e3, [RECALL_CATEGORY]))
58628
+ )
58629
+ );
58541
58630
  }
58542
58631
  if (scope === "lifetime" || scope === "all") {
58543
58632
  const g = globalDb();
@@ -245,6 +245,12 @@
245
245
  "type": "integer",
246
246
  "minimum": 0,
247
247
  "maximum": 10
248
+ },
249
+ "repeatAfter": {
250
+ "default": 10,
251
+ "type": "integer",
252
+ "minimum": 0,
253
+ "maximum": 1000
248
254
  }
249
255
  }
250
256
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eminiar",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
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",