eminiar 0.8.1 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli.js +2935 -2173
- package/dist/hook.js +562 -79
- package/dist/server.js +194 -42
- package/eminiar.config.schema.json +6 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
14890
|
-
importedPkgs.add(
|
|
14891
|
-
if (!imports.has(pkgId(
|
|
14892
|
-
nodes.push({ id: pkgId(
|
|
14893
|
-
addImport(pkgId(
|
|
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,
|
|
15001
|
-
for (let cur = n.parent; cur && cur.id !==
|
|
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
|
|
17432
|
-
const succ = adj.get(
|
|
17433
|
-
if (
|
|
17434
|
-
const m = succ[
|
|
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(
|
|
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(
|
|
17449
|
-
if (low.get(
|
|
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 !==
|
|
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
|
|
17612
|
-
counts.set(
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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) {
|
|
@@ -18432,11 +18452,11 @@ function gitignoreHint(ctx) {
|
|
|
18432
18452
|
const file3 = join12(ctx.root, ".gitignore");
|
|
18433
18453
|
if (!existsSync8(file3)) return void 0;
|
|
18434
18454
|
const dir = memoriesRel(ctx);
|
|
18435
|
-
const
|
|
18436
|
-
const ignored = readFileSync8(file3, "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, "").replace(/\/$/, "")).some((l) => l ===
|
|
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}/**`);
|
|
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. */
|
|
@@ -51193,11 +51219,11 @@ function folderRows(files, languages2) {
|
|
|
51193
51219
|
score: Number((a.score / a.files).toFixed(3)),
|
|
51194
51220
|
languages: a.langs
|
|
51195
51221
|
}));
|
|
51196
|
-
const
|
|
51222
|
+
const top2 = new Map(rows.filter((r) => r.path.split("/").length <= 2).map((r) => [r.path, r]));
|
|
51197
51223
|
return rows.filter((r) => {
|
|
51198
51224
|
const segs = r.path.split("/");
|
|
51199
51225
|
if (segs.length <= 2) return true;
|
|
51200
|
-
return (
|
|
51226
|
+
return (top2.get(`${segs[0]}/`)?.files ?? 0) >= 12;
|
|
51201
51227
|
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
51202
51228
|
}
|
|
51203
51229
|
function entryPoints(root, db) {
|
|
@@ -52843,10 +52869,10 @@ async function search(db, query, opts = {}) {
|
|
|
52843
52869
|
}
|
|
52844
52870
|
}
|
|
52845
52871
|
const fused = rrf(lists);
|
|
52846
|
-
const
|
|
52847
|
-
const chunks = db.getChunks(
|
|
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));
|
|
52848
52874
|
const byId = new Map(chunks.map((c) => [c.id, c]));
|
|
52849
|
-
const scored =
|
|
52875
|
+
const scored = top2.map(([id, score]) => ({ chunk: byId.get(id), score, matched: [] })).filter((s) => !!s.chunk);
|
|
52850
52876
|
const allTerms = [...terms, ...parsed.phrases];
|
|
52851
52877
|
const ranked = rerank(scored, allTerms);
|
|
52852
52878
|
const seenHash = /* @__PURE__ */ new Set();
|
|
@@ -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
|
|
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(
|
|
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("."))
|
|
@@ -57866,12 +57892,12 @@ function renderBranch(m, name, limit = 20) {
|
|
|
57866
57892
|
);
|
|
57867
57893
|
const files = /* @__PURE__ */ new Map();
|
|
57868
57894
|
for (const c of own2) for (const f of c.files) files.set(f.path, (files.get(f.path) ?? 0) + 1);
|
|
57869
|
-
const
|
|
57870
|
-
if (
|
|
57895
|
+
const top2 = [...files.entries()].sort((a, b2) => b2[1] - a[1]).slice(0, 10);
|
|
57896
|
+
if (top2.length)
|
|
57871
57897
|
lines.push(
|
|
57872
|
-
`files touched: ${
|
|
57898
|
+
`files touched: ${top2.map(([p, n]) => n > 1 ? `${p} \xD7${n}` : p).join(", ")}${files.size > 10 ? ` \u2026 +${files.size - 10}` : ""}`
|
|
57873
57899
|
);
|
|
57874
|
-
const risky =
|
|
57900
|
+
const risky = top2.map(([p]) => m.files.get(p)).filter((f) => !!f && f.risk >= 0.3);
|
|
57875
57901
|
if (risky.length)
|
|
57876
57902
|
lines.push(
|
|
57877
57903
|
`touches bug-prone files: ${risky.map((f) => `${f.path} (${f.risk.toFixed(2)})`).join(", ")}`
|
|
@@ -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
|
|
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: '
|
|
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:
|
|
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,111 @@ 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/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
|
+
|
|
58652
|
+
// src/tools/em_stats.ts
|
|
58525
58653
|
var emStats = defineTool({
|
|
58526
58654
|
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.",
|
|
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.",
|
|
58528
58656
|
schema: {
|
|
58529
58657
|
scope: external_exports.enum(["session", "project", "lifetime", "all"]).optional().describe("default all")
|
|
58530
58658
|
},
|
|
@@ -58535,9 +58663,33 @@ var emStats = defineTool({
|
|
|
58535
58663
|
const parts = [];
|
|
58536
58664
|
if (scope === "session" || scope === "all") {
|
|
58537
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
|
+
);
|
|
58672
|
+
parts.push(
|
|
58673
|
+
formatRecallStats(
|
|
58674
|
+
"recall (session)",
|
|
58675
|
+
recallStats(db.eventsBySession(sessionId, { categories: [RECALL_CATEGORY] }))
|
|
58676
|
+
)
|
|
58677
|
+
);
|
|
58538
58678
|
}
|
|
58539
58679
|
if (scope === "project" || scope === "all") {
|
|
58540
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
|
+
);
|
|
58687
|
+
parts.push(
|
|
58688
|
+
formatRecallStats(
|
|
58689
|
+
"recall (project, last 2000 decisions)",
|
|
58690
|
+
recallStats(db.eventsRecent(2e3, [RECALL_CATEGORY]))
|
|
58691
|
+
)
|
|
58692
|
+
);
|
|
58541
58693
|
}
|
|
58542
58694
|
if (scope === "lifetime" || scope === "all") {
|
|
58543
58695
|
const g = globalDb();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eminiar",
|
|
3
|
-
"version": "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",
|