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/hook.js
CHANGED
|
@@ -5917,7 +5917,11 @@ 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,
|
|
5923
|
+
/** A routing decision (allow / annotate / deny) for one native call; never in the guide. */
|
|
5924
|
+
routing: 4
|
|
5921
5925
|
};
|
|
5922
5926
|
var GUIDE_ORDER = [
|
|
5923
5927
|
{ title: "Last Request", categories: ["user_prompt"] },
|
|
@@ -26082,7 +26086,12 @@ var ConfigSchema = external_exports.object({
|
|
|
26082
26086
|
/** Before WebSearch / WebFetch: the indexed doc pages that already answer it. */
|
|
26083
26087
|
onWeb: external_exports.boolean().default(true),
|
|
26084
26088
|
maxMemories: external_exports.number().int().min(0).max(10).default(3),
|
|
26085
|
-
maxDocs: external_exports.number().int().min(0).max(10).default(2)
|
|
26089
|
+
maxDocs: external_exports.number().int().min(0).max(10).default(2),
|
|
26090
|
+
/**
|
|
26091
|
+
* Prompts after which an item already shown in this context window may be injected
|
|
26092
|
+
* again when it matches (0 = once per context window only).
|
|
26093
|
+
*/
|
|
26094
|
+
repeatAfter: external_exports.number().int().min(0).max(1e3).default(10)
|
|
26086
26095
|
}).prefault({}),
|
|
26087
26096
|
docs: external_exports.object({
|
|
26088
26097
|
/** Project-relative manifest of documentation sources (`em_docs`); commit it. */
|
|
@@ -26975,6 +26984,8 @@ var PROMPT_FILLER = /* @__PURE__ */ new Set([
|
|
|
26975
26984
|
]);
|
|
26976
26985
|
var MAX_TERMS = 16;
|
|
26977
26986
|
var RECALL_CATEGORY = "recall";
|
|
26987
|
+
var DEFAULT_REPEAT_AFTER = 10;
|
|
26988
|
+
var NAME_MATCH_RATIO = 0.3;
|
|
26978
26989
|
function promptTerms(prompt) {
|
|
26979
26990
|
const body = prompt.replace(/^\s*\/\S+/, " ");
|
|
26980
26991
|
return parseQuery(body).terms.filter((t) => t.length >= 3 && !PROMPT_FILLER.has(t) && !/^\d+$/.test(t)).slice(0, MAX_TERMS);
|
|
@@ -26982,26 +26993,50 @@ function promptTerms(prompt) {
|
|
|
26982
26993
|
function requiredMatches(termCount) {
|
|
26983
26994
|
return Math.min(3, Math.max(2, Math.ceil(termCount * 0.2)));
|
|
26984
26995
|
}
|
|
26985
|
-
function shownKeys(db, sessionId) {
|
|
26996
|
+
function shownKeys(db, sessionId, repeatAfter = DEFAULT_REPEAT_AFTER) {
|
|
26986
26997
|
const events = db.eventsBySession(sessionId, {
|
|
26987
|
-
categories: [RECALL_CATEGORY, "session_start"]
|
|
26998
|
+
categories: [RECALL_CATEGORY, "session_start", "user_prompt"]
|
|
26988
26999
|
});
|
|
26989
|
-
const
|
|
27000
|
+
const shownAt = /* @__PURE__ */ new Map();
|
|
27001
|
+
let prompts = 0;
|
|
26990
27002
|
for (const e of events) {
|
|
26991
27003
|
if (e.category === "session_start") {
|
|
26992
|
-
|
|
27004
|
+
shownAt.clear();
|
|
27005
|
+
prompts = 0;
|
|
27006
|
+
continue;
|
|
27007
|
+
}
|
|
27008
|
+
if (e.category === "user_prompt") {
|
|
27009
|
+
prompts++;
|
|
26993
27010
|
continue;
|
|
26994
27011
|
}
|
|
26995
27012
|
try {
|
|
26996
|
-
for (const k of JSON.parse(e.payload).keys ?? [])
|
|
27013
|
+
for (const k of JSON.parse(e.payload).keys ?? [])
|
|
27014
|
+
shownAt.set(k, prompts);
|
|
26997
27015
|
} catch {
|
|
26998
27016
|
}
|
|
26999
27017
|
}
|
|
27018
|
+
const out = /* @__PURE__ */ new Set();
|
|
27019
|
+
for (const [k, at] of shownAt) if (repeatAfter <= 0 || prompts - at < repeatAfter) out.add(k);
|
|
27000
27020
|
return out;
|
|
27001
27021
|
}
|
|
27002
|
-
function recordShown(db, sessionId, keys) {
|
|
27003
|
-
if (!keys.length) return;
|
|
27004
|
-
|
|
27022
|
+
function recordShown(db, sessionId, keys, info = { path: "prompt" }) {
|
|
27023
|
+
if (!keys.length && !info.trace) return;
|
|
27024
|
+
const t = info.trace;
|
|
27025
|
+
db.eventInsert({
|
|
27026
|
+
sessionId,
|
|
27027
|
+
category: RECALL_CATEGORY,
|
|
27028
|
+
priority: 4,
|
|
27029
|
+
payload: {
|
|
27030
|
+
keys,
|
|
27031
|
+
path: info.path,
|
|
27032
|
+
...t ? {
|
|
27033
|
+
terms: t.terms,
|
|
27034
|
+
need: t.need,
|
|
27035
|
+
candidates: t.candidates.length,
|
|
27036
|
+
gated: t.candidates.filter((c) => c.outcome !== "shown").slice(0, 6).map((c) => `${c.key} ${c.outcome}${c.why ? ` (${c.why})` : ""}`)
|
|
27037
|
+
} : {}
|
|
27038
|
+
}
|
|
27039
|
+
});
|
|
27005
27040
|
}
|
|
27006
27041
|
function clip2(s, max) {
|
|
27007
27042
|
const t = s.replace(/\s+/g, " ").trim();
|
|
@@ -27024,16 +27059,53 @@ function kindRank(kind) {
|
|
|
27024
27059
|
const i = KIND_ORDER.indexOf(kind);
|
|
27025
27060
|
return i === -1 ? KIND_ORDER.length : i;
|
|
27026
27061
|
}
|
|
27062
|
+
function sharedTerms(terms, ...texts) {
|
|
27063
|
+
const hay = texts.join("\n").toLowerCase();
|
|
27064
|
+
return terms.filter((t) => {
|
|
27065
|
+
const base = t.replace(/\*$/, "").toLowerCase();
|
|
27066
|
+
return !!base && new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRe(base)}`, "u").test(hay);
|
|
27067
|
+
});
|
|
27068
|
+
}
|
|
27069
|
+
function nameTerms(m) {
|
|
27070
|
+
const slug = (m.name ?? "").split("/").pop() ?? "";
|
|
27071
|
+
let tags = [];
|
|
27072
|
+
try {
|
|
27073
|
+
tags = m.tags ? JSON.parse(m.tags) : [];
|
|
27074
|
+
} catch {
|
|
27075
|
+
}
|
|
27076
|
+
return [...slug.toLowerCase().split(/[-._]+/), ...tags.map((t) => t.toLowerCase())].filter(
|
|
27077
|
+
(t) => t.length >= 4
|
|
27078
|
+
);
|
|
27079
|
+
}
|
|
27080
|
+
function namesMemory(term, names) {
|
|
27081
|
+
const t = term.replace(/\*$/, "");
|
|
27082
|
+
if (t.length < 4) return false;
|
|
27083
|
+
return names.some((n) => t === n || t.includes(n) || n.includes(t));
|
|
27084
|
+
}
|
|
27085
|
+
function qualifies(matched, need, termCount, m, text) {
|
|
27086
|
+
if (matched.length >= need) return { ok: true };
|
|
27087
|
+
if (m?.name && text?.toLowerCase().includes(m.name)) return { ok: true, why: "named" };
|
|
27088
|
+
if (m && matched.length >= 1 && matched.length / Math.max(1, termCount) >= NAME_MATCH_RATIO) {
|
|
27089
|
+
const names = nameTerms(m);
|
|
27090
|
+
const hit = matched.find((t) => namesMemory(t, names));
|
|
27091
|
+
if (hit) return { ok: true, why: `slug/tag "${hit}"` };
|
|
27092
|
+
}
|
|
27093
|
+
return { ok: false, why: `${matched.length}/${need} terms` };
|
|
27094
|
+
}
|
|
27027
27095
|
async function recallForText(db, text, opts = {}) {
|
|
27028
27096
|
const maxMemories = opts.maxMemories ?? 3;
|
|
27029
27097
|
const maxDocs = opts.maxDocs ?? 2;
|
|
27030
27098
|
const terms = promptTerms(text);
|
|
27031
|
-
|
|
27099
|
+
const trace = opts.trace ? { path: opts.path ?? "prompt", terms, need: 0, candidates: [] } : void 0;
|
|
27100
|
+
const quiet = () => ({ text: "", keys: [], trace });
|
|
27101
|
+
if (terms.length < 2 || maxMemories <= 0 && maxDocs <= 0) return quiet();
|
|
27032
27102
|
const need = opts.minMatches ?? requiredMatches(terms.length);
|
|
27103
|
+
if (trace) trace.need = need;
|
|
27033
27104
|
const query = terms.join(" ");
|
|
27034
|
-
const seen = opts.sessionId ? shownKeys(db, opts.sessionId) : /* @__PURE__ */ new Set();
|
|
27105
|
+
const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
|
|
27035
27106
|
const lines = [];
|
|
27036
27107
|
const keys = [];
|
|
27108
|
+
const note = (c) => trace?.candidates.push(c);
|
|
27037
27109
|
if (maxMemories > 0) {
|
|
27038
27110
|
const r = await search(db, query, {
|
|
27039
27111
|
mode: "lexical",
|
|
@@ -27042,14 +27114,38 @@ async function recallForText(db, text, opts = {}) {
|
|
|
27042
27114
|
filters: { source: "memory" }
|
|
27043
27115
|
});
|
|
27044
27116
|
let n = 0;
|
|
27117
|
+
const considered = /* @__PURE__ */ new Set();
|
|
27045
27118
|
for (const h of r.hits) {
|
|
27046
|
-
if (n >= maxMemories) break;
|
|
27047
|
-
if (h.matched.length < need) continue;
|
|
27048
27119
|
const uri = h.chunk.document.uri;
|
|
27049
27120
|
const name = uri?.startsWith("memory:") ? uri.slice(7) : void 0;
|
|
27050
27121
|
const m = name ? db.memoryGetByName(name) : void 0;
|
|
27051
27122
|
const key = `memory:${name}`;
|
|
27052
|
-
if (!m?.name ||
|
|
27123
|
+
if (!m?.name || considered.has(key)) continue;
|
|
27124
|
+
considered.add(key);
|
|
27125
|
+
const label = `${m.name} [${m.kind}]`;
|
|
27126
|
+
const matched = sharedTerms(terms, m.name, m.content, m.tags ?? "");
|
|
27127
|
+
if (!live(m)) {
|
|
27128
|
+
note({ key, label, matched, outcome: "skipped", why: m.status ?? "retired" });
|
|
27129
|
+
continue;
|
|
27130
|
+
}
|
|
27131
|
+
if (!specific(m)) {
|
|
27132
|
+
note({ key, label, matched, outcome: "skipped", why: "onboarding" });
|
|
27133
|
+
continue;
|
|
27134
|
+
}
|
|
27135
|
+
const q = qualifies(matched, need, terms.length, m, text);
|
|
27136
|
+
if (!q.ok) {
|
|
27137
|
+
note({ key, label, matched, outcome: "gated", why: q.why });
|
|
27138
|
+
continue;
|
|
27139
|
+
}
|
|
27140
|
+
if (seen.has(key)) {
|
|
27141
|
+
note({ key, label, matched, outcome: "seen", why: q.why });
|
|
27142
|
+
continue;
|
|
27143
|
+
}
|
|
27144
|
+
if (n >= maxMemories) {
|
|
27145
|
+
note({ key, label, matched, outcome: "over-limit", why: q.why });
|
|
27146
|
+
continue;
|
|
27147
|
+
}
|
|
27148
|
+
note({ key, label, matched, outcome: "shown", why: q.why });
|
|
27053
27149
|
lines.push(`- memory ${m.name} [${m.kind}]: ${gist(m)}`);
|
|
27054
27150
|
keys.push(key);
|
|
27055
27151
|
n++;
|
|
@@ -27064,28 +27160,62 @@ async function recallForText(db, text, opts = {}) {
|
|
|
27064
27160
|
let n = 0;
|
|
27065
27161
|
const docsSeen = /* @__PURE__ */ new Set();
|
|
27066
27162
|
for (const h of r.hits) {
|
|
27067
|
-
if (n >= maxDocs) break;
|
|
27068
|
-
if (h.matched.length < need) continue;
|
|
27069
27163
|
const d = h.chunk.document;
|
|
27070
27164
|
const key = `doc:${d.id}`;
|
|
27071
|
-
if (docsSeen.has(d.id)
|
|
27165
|
+
if (docsSeen.has(d.id)) continue;
|
|
27072
27166
|
docsSeen.add(d.id);
|
|
27167
|
+
const label = `${d.source} \u203A ${clip2(d.title, 60)}`;
|
|
27168
|
+
const matched = sharedTerms(terms, h.chunk.title, h.chunk.body);
|
|
27169
|
+
if (matched.length < need) {
|
|
27170
|
+
note({ key, label, matched, outcome: "gated", why: `${matched.length}/${need} terms` });
|
|
27171
|
+
continue;
|
|
27172
|
+
}
|
|
27173
|
+
if (seen.has(key)) {
|
|
27174
|
+
note({ key, label, matched, outcome: "seen" });
|
|
27175
|
+
continue;
|
|
27176
|
+
}
|
|
27177
|
+
if (n >= maxDocs) {
|
|
27178
|
+
note({ key, label, matched, outcome: "over-limit" });
|
|
27179
|
+
continue;
|
|
27180
|
+
}
|
|
27181
|
+
note({ key, label, matched, outcome: "shown" });
|
|
27073
27182
|
lines.push(`- ${d.source} \u203A ${clip2(d.title, 80)}: ${clip2(h.snippet, 160)}`);
|
|
27074
27183
|
keys.push(key);
|
|
27075
27184
|
n++;
|
|
27076
27185
|
}
|
|
27077
27186
|
}
|
|
27078
|
-
if (!lines.length) return
|
|
27187
|
+
if (!lines.length) return quiet();
|
|
27079
27188
|
return {
|
|
27080
27189
|
text: [
|
|
27081
27190
|
"eminiar recall \u2014 project memories and indexed docs that match this request (check them before deciding; superseded ones are omitted):",
|
|
27082
27191
|
...lines,
|
|
27083
27192
|
'Full text: em_memory {action:"read", name} for a memory, em_docs {action:"lookup", query} for docs; em_recall {query} searches further.'
|
|
27084
27193
|
].join("\n"),
|
|
27085
|
-
keys
|
|
27194
|
+
keys,
|
|
27195
|
+
trace
|
|
27086
27196
|
};
|
|
27087
27197
|
}
|
|
27088
|
-
|
|
27198
|
+
var SYMBOL_KINDS = [
|
|
27199
|
+
"function",
|
|
27200
|
+
"class",
|
|
27201
|
+
"method",
|
|
27202
|
+
"interface",
|
|
27203
|
+
"trait",
|
|
27204
|
+
"type",
|
|
27205
|
+
"enum",
|
|
27206
|
+
"variable"
|
|
27207
|
+
];
|
|
27208
|
+
function fileSymbols(db, rel) {
|
|
27209
|
+
try {
|
|
27210
|
+
return db.graphFindNodes({ path: rel, kinds: SYMBOL_KINDS, limit: 400 }).filter((n) => n.path === rel).map((n) => n.name);
|
|
27211
|
+
} catch {
|
|
27212
|
+
return [];
|
|
27213
|
+
}
|
|
27214
|
+
}
|
|
27215
|
+
function escapeRe(s) {
|
|
27216
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27217
|
+
}
|
|
27218
|
+
function linksFile(m, rel) {
|
|
27089
27219
|
if (!m.node_ids) return false;
|
|
27090
27220
|
try {
|
|
27091
27221
|
return JSON.parse(m.node_ids).some((id) => fileOf(id) === rel);
|
|
@@ -27093,23 +27223,50 @@ function mentionsFile(m, rel) {
|
|
|
27093
27223
|
return false;
|
|
27094
27224
|
}
|
|
27095
27225
|
}
|
|
27226
|
+
function bodyNamesPath(body, rel) {
|
|
27227
|
+
return new RegExp(`(?:^|[^\\w/.-]|\\./)${escapeRe(rel)}(?![\\w-])`, "m").test(body);
|
|
27228
|
+
}
|
|
27229
|
+
function symbolMentionPattern(symbols) {
|
|
27230
|
+
const names = [...new Set(symbols)].filter((s) => /^[A-Za-z_$][\w$]{4,}$/.test(s));
|
|
27231
|
+
if (!names.length) return void 0;
|
|
27232
|
+
const alt = names.map(escapeRe).join("|");
|
|
27233
|
+
return new RegExp(`(?:[\`#.](?:${alt})(?!\\w)|(?<!\\w)(?:${alt})\\()`);
|
|
27234
|
+
}
|
|
27096
27235
|
function memoriesForFile(db, rel, opts = {}) {
|
|
27097
|
-
const
|
|
27098
|
-
|
|
27099
|
-
|
|
27236
|
+
const symRe = opts.symbols?.length ? symbolMentionPattern(opts.symbols) : void 0;
|
|
27237
|
+
const how = (m) => {
|
|
27238
|
+
if (linksFile(m, rel)) return "related";
|
|
27239
|
+
if (bodyNamesPath(m.content, rel)) return "names the path";
|
|
27240
|
+
if (symRe?.test(m.content)) return "names a symbol";
|
|
27241
|
+
return void 0;
|
|
27242
|
+
};
|
|
27243
|
+
const trace = opts.trace ? { path: "file", terms: [rel], need: 0, candidates: [] } : void 0;
|
|
27244
|
+
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));
|
|
27245
|
+
if (!linked.length) return { text: "", keys: [], trace };
|
|
27246
|
+
const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
|
|
27100
27247
|
const lines = [];
|
|
27101
27248
|
const keys = [];
|
|
27102
|
-
for (const m of linked) {
|
|
27103
|
-
if (lines.length >= (opts.max ?? 3)) break;
|
|
27249
|
+
for (const { m, why } of linked) {
|
|
27104
27250
|
const key = `memory:${m.name}`;
|
|
27105
|
-
|
|
27251
|
+
const label = `${m.name} [${m.kind}]`;
|
|
27252
|
+
if (!m.name) continue;
|
|
27253
|
+
if (seen.has(key)) {
|
|
27254
|
+
trace?.candidates.push({ key, label, matched: [rel], outcome: "seen", why });
|
|
27255
|
+
continue;
|
|
27256
|
+
}
|
|
27257
|
+
if (lines.length >= (opts.max ?? 3)) {
|
|
27258
|
+
trace?.candidates.push({ key, label, matched: [rel], outcome: "over-limit", why });
|
|
27259
|
+
continue;
|
|
27260
|
+
}
|
|
27261
|
+
trace?.candidates.push({ key, label, matched: [rel], outcome: "shown", why });
|
|
27106
27262
|
lines.push(`${m.name} [${m.kind}]: ${gist(m, 140)}`);
|
|
27107
27263
|
keys.push(key);
|
|
27108
27264
|
}
|
|
27109
|
-
if (!lines.length) return { text: "", keys: [] };
|
|
27265
|
+
if (!lines.length) return { text: "", keys: [], trace };
|
|
27110
27266
|
return {
|
|
27111
27267
|
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
|
|
27268
|
+
keys,
|
|
27269
|
+
trace
|
|
27113
27270
|
};
|
|
27114
27271
|
}
|
|
27115
27272
|
var RISKY = 0.3;
|
|
@@ -27125,22 +27282,32 @@ function riskLine(path2, f) {
|
|
|
27125
27282
|
return `${path2} is bug-prone (${parts.join("; ")})`;
|
|
27126
27283
|
}
|
|
27127
27284
|
function fileContext(db, rel, opts = {}) {
|
|
27128
|
-
const
|
|
27129
|
-
if (opts.sessionId && shownKeys(db, opts.sessionId).has(key)) return { text: "", keys: [] };
|
|
27285
|
+
const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
|
|
27130
27286
|
const lines = [];
|
|
27131
|
-
const
|
|
27132
|
-
const
|
|
27133
|
-
if (
|
|
27134
|
-
|
|
27135
|
-
|
|
27136
|
-
)
|
|
27137
|
-
|
|
27138
|
-
|
|
27139
|
-
|
|
27140
|
-
|
|
27141
|
-
|
|
27287
|
+
const keys = [];
|
|
27288
|
+
const histKey = `hist:${rel}`;
|
|
27289
|
+
if (!seen.has(histKey)) {
|
|
27290
|
+
const f = readFileRisk(db)[rel];
|
|
27291
|
+
const risk = f ? riskLine(rel, f) : void 0;
|
|
27292
|
+
if (risk) {
|
|
27293
|
+
lines.push(
|
|
27294
|
+
`eminiar history: ${risk}. em_history {action:"file", path:"${rel}"} shows those fixes; keep the behaviour they restored covered by a test.`
|
|
27295
|
+
);
|
|
27296
|
+
keys.push(histKey);
|
|
27297
|
+
}
|
|
27298
|
+
}
|
|
27299
|
+
const fanKey = `fanin:${rel}`;
|
|
27300
|
+
if (opts.edit && !seen.has(fanKey)) {
|
|
27301
|
+
const fanIn = db.metricsGet([fileId(rel)])[0]?.fan_in ?? 0;
|
|
27302
|
+
if (fanIn >= FAN_IN) {
|
|
27303
|
+
lines.push(
|
|
27304
|
+
`eminiar graph: ${rel} is imported by ${fanIn} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
|
|
27305
|
+
);
|
|
27306
|
+
keys.push(fanKey);
|
|
27307
|
+
}
|
|
27308
|
+
}
|
|
27142
27309
|
if (!lines.length) return { text: "", keys: [] };
|
|
27143
|
-
return { text: lines.join("\n"), keys
|
|
27310
|
+
return { text: lines.join("\n"), keys };
|
|
27144
27311
|
}
|
|
27145
27312
|
function commitContext(db, files) {
|
|
27146
27313
|
if (!files.length) return "";
|
|
@@ -27197,7 +27364,7 @@ async function recallForError(db, error62, opts = {}) {
|
|
|
27197
27364
|
const terms = promptTerms(error62.slice(0, 600)).filter((t) => !ERROR_FILLER.has(t));
|
|
27198
27365
|
if (terms.length < 3) return { text: "", keys: [] };
|
|
27199
27366
|
const need = Math.max(3, Math.ceil(terms.length * 0.4));
|
|
27200
|
-
const seen = opts.sessionId ? shownKeys(db, opts.sessionId) : /* @__PURE__ */ new Set();
|
|
27367
|
+
const seen = opts.sessionId ? shownKeys(db, opts.sessionId, opts.repeatAfter) : /* @__PURE__ */ new Set();
|
|
27201
27368
|
const past = [];
|
|
27202
27369
|
const keys = [];
|
|
27203
27370
|
const r = await search(db, terms.join(" "), {
|
|
@@ -27223,8 +27390,10 @@ async function recallForError(db, error62, opts = {}) {
|
|
|
27223
27390
|
maxMemories: opts.maxMemories ?? 2,
|
|
27224
27391
|
maxDocs: 0,
|
|
27225
27392
|
sessionId: opts.sessionId,
|
|
27393
|
+
repeatAfter: opts.repeatAfter,
|
|
27226
27394
|
// error text is wordy (paths, module names): the same proportional bar as past errors
|
|
27227
|
-
minMatches: need
|
|
27395
|
+
minMatches: need,
|
|
27396
|
+
path: "error"
|
|
27228
27397
|
});
|
|
27229
27398
|
const memLines = mem.text.split("\n").filter((l) => l.startsWith("- memory "));
|
|
27230
27399
|
if (!past.length && !memLines.length) return { text: "", keys: [] };
|
|
@@ -28720,10 +28889,13 @@ var userPromptSubmit = async (input2) => {
|
|
|
28720
28889
|
const r = await recallForText(db, input2.prompt, {
|
|
28721
28890
|
maxMemories: recall.maxMemories,
|
|
28722
28891
|
maxDocs: recall.maxDocs,
|
|
28723
|
-
sessionId
|
|
28892
|
+
sessionId,
|
|
28893
|
+
repeatAfter: recall.repeatAfter,
|
|
28894
|
+
trace: true,
|
|
28895
|
+
path: "prompt"
|
|
28724
28896
|
});
|
|
28897
|
+
recordShown(db, sessionId, r.keys, { path: "prompt", trace: r.trace });
|
|
28725
28898
|
if (!r.text) return void 0;
|
|
28726
|
-
recordShown(db, sessionId, r.keys);
|
|
28727
28899
|
return {
|
|
28728
28900
|
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: r.text }
|
|
28729
28901
|
};
|
|
@@ -28773,11 +28945,12 @@ var postToolUseFailure = async (input2) => {
|
|
|
28773
28945
|
try {
|
|
28774
28946
|
const r = await recallForError(db, input2.error, {
|
|
28775
28947
|
sessionId,
|
|
28776
|
-
maxMemories: Math.min(2, recall.maxMemories)
|
|
28948
|
+
maxMemories: Math.min(2, recall.maxMemories),
|
|
28949
|
+
repeatAfter: recall.repeatAfter
|
|
28777
28950
|
});
|
|
28778
28951
|
if (r.text) {
|
|
28779
28952
|
recalled = r.text;
|
|
28780
|
-
recordShown(db, sessionId, r.keys);
|
|
28953
|
+
recordShown(db, sessionId, r.keys, { path: "error" });
|
|
28781
28954
|
}
|
|
28782
28955
|
} catch {
|
|
28783
28956
|
}
|
|
@@ -28948,6 +29121,7 @@ var preCompact = (input2) => {
|
|
|
28948
29121
|
// src/hooks/routing.ts
|
|
28949
29122
|
init_esm_shims();
|
|
28950
29123
|
import { realpathSync as realpathSync4, statSync as statSync4 } from "fs";
|
|
29124
|
+
import { homedir as homedir4 } from "os";
|
|
28951
29125
|
import { extname as extname2, join as join11, relative as relative4, resolve as resolve3 } from "path";
|
|
28952
29126
|
|
|
28953
29127
|
// src/docs/manifest.ts
|
|
@@ -29011,7 +29185,7 @@ function parseRule(raw) {
|
|
|
29011
29185
|
if (!m) return void 0;
|
|
29012
29186
|
return { raw, tool: m[1], pattern: m[2] };
|
|
29013
29187
|
}
|
|
29014
|
-
function
|
|
29188
|
+
function escapeRe2(s) {
|
|
29015
29189
|
return s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
29016
29190
|
}
|
|
29017
29191
|
var readRegexCache = /* @__PURE__ */ new Map();
|
|
@@ -29020,7 +29194,7 @@ function readRegex(pattern) {
|
|
|
29020
29194
|
if (!re) {
|
|
29021
29195
|
if (readRegexCache.size >= 1024) readRegexCache.clear();
|
|
29022
29196
|
re = new RegExp(
|
|
29023
|
-
`^${pattern.split("**").map((seg) => seg.split("*").map(
|
|
29197
|
+
`^${pattern.split("**").map((seg) => seg.split("*").map(escapeRe2).join("[^/]*")).join(".*")}$`
|
|
29024
29198
|
);
|
|
29025
29199
|
readRegexCache.set(pattern, re);
|
|
29026
29200
|
}
|
|
@@ -29245,6 +29419,26 @@ function fitBudget(lines, budget) {
|
|
|
29245
29419
|
return out.length > budget ? `${out.slice(0, budget - 1)}\u2026` : out;
|
|
29246
29420
|
}
|
|
29247
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
|
+
|
|
29248
29442
|
// src/session/server-alive.ts
|
|
29249
29443
|
init_esm_shims();
|
|
29250
29444
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -29326,7 +29520,7 @@ function hookLog(level, msg) {
|
|
|
29326
29520
|
}
|
|
29327
29521
|
|
|
29328
29522
|
// src/hooks/routing.ts
|
|
29329
|
-
var
|
|
29523
|
+
var SYMBOL_KINDS2 = [
|
|
29330
29524
|
"function",
|
|
29331
29525
|
"class",
|
|
29332
29526
|
"method",
|
|
@@ -29344,7 +29538,7 @@ function literalOf(pattern) {
|
|
|
29344
29538
|
function symbolHints(db, pattern) {
|
|
29345
29539
|
const lit = literalOf(pattern);
|
|
29346
29540
|
if (!lit) return void 0;
|
|
29347
|
-
const nodes = db.graphFindNodes({ query: lit, kinds:
|
|
29541
|
+
const nodes = db.graphFindNodes({ query: lit, kinds: SYMBOL_KINDS2, limit: 5 });
|
|
29348
29542
|
if (!nodes.length) return void 0;
|
|
29349
29543
|
const list = nodes.map((n) => `${n.kind} ${n.name} (${n.path}:${n.start_line ?? "?"})`).join("; ");
|
|
29350
29544
|
return `eminiar graph: symbols matching "${lit}": ${list}. em_symbols {name_path, include_body: true} reads one; {mode:"references"} lists its callers.`;
|
|
@@ -29354,6 +29548,47 @@ function coverageHint(db, rel) {
|
|
|
29354
29548
|
if (!row || row.status === "indexed") return void 0;
|
|
29355
29549
|
return `eminiar graph: ${rel} is ${row.status}${row.reason ? ` (${row.reason})` : ""}; graph answers about it may be incomplete.`;
|
|
29356
29550
|
}
|
|
29551
|
+
var GIT_HISTORY = /(?:^|[;&|]\s*)git\s+(?:(?:-C|--git-dir|--work-tree)[= ]\S+\s+|--no-pager\s+)*(log|blame|shortlog|show|whatchanged|reflog)\b([^;&|]*)/;
|
|
29552
|
+
var ONE_COMMIT = /(^|\s)(-1|-n\s*1|-n1|--max-count[= ]1)(?=\s|$)/;
|
|
29553
|
+
function historyCall(cmd) {
|
|
29554
|
+
const m = GIT_HISTORY.exec(cmd);
|
|
29555
|
+
if (!m) return void 0;
|
|
29556
|
+
const sub = m[1];
|
|
29557
|
+
const rest = (m[2] ?? "").trim();
|
|
29558
|
+
const args = rest.split(/\s+/).filter(Boolean);
|
|
29559
|
+
const q = (s) => JSON.stringify(s.replace(/^['"]|['"]$/g, ""));
|
|
29560
|
+
const positional = (a) => !a.startsWith("-");
|
|
29561
|
+
if (sub === "show") {
|
|
29562
|
+
if (/\S+:\S+/.test(rest)) return void 0;
|
|
29563
|
+
const sha = args.find(positional);
|
|
29564
|
+
return `{action:"commit", sha:${q(sha ?? "HEAD")}}`;
|
|
29565
|
+
}
|
|
29566
|
+
if (sub === "blame") {
|
|
29567
|
+
const path3 = args.filter(positional).pop();
|
|
29568
|
+
return path3 ? `{action:"file", path:${q(path3)}}` : void 0;
|
|
29569
|
+
}
|
|
29570
|
+
if (sub === "log" && ONE_COMMIT.test(rest) && !/(^|\s)(-p|--patch|--stat)\b/.test(rest))
|
|
29571
|
+
return void 0;
|
|
29572
|
+
const lineRange = /-L\s*:?([^:\s]+):(\S+)/.exec(rest);
|
|
29573
|
+
if (lineRange) {
|
|
29574
|
+
const [, fn, file3] = lineRange;
|
|
29575
|
+
return /^[A-Za-z_$]/.test(fn) ? `{action:"symbol", name_path:${q(`${file3}#${fn}`)}}` : `{action:"file", path:${q(file3)}}`;
|
|
29576
|
+
}
|
|
29577
|
+
const grep = /--grep[= ]("[^"]*"|'[^']*'|\S+)/.exec(rest);
|
|
29578
|
+
if (grep) return `{action:"search", query:${q(grep[1])}}`;
|
|
29579
|
+
const author = /--author[= ]("[^"]*"|'[^']*'|\S+)/.exec(rest);
|
|
29580
|
+
const range = args.find((a) => positional(a) && /\.\.\.?/.test(a));
|
|
29581
|
+
if (range) {
|
|
29582
|
+
const [a, b] = range.split(/\.\.\.?/);
|
|
29583
|
+
return `{action:"branch", name:${q(b || a || "HEAD")}}`;
|
|
29584
|
+
}
|
|
29585
|
+
const dash = args.indexOf("--");
|
|
29586
|
+
const path2 = dash !== -1 ? args[dash + 1] : args.find((a) => positional(a) && /[/.]/.test(a) && !/^[0-9a-f]{7,40}$/i.test(a));
|
|
29587
|
+
if (path2) return `{action:"file", path:${q(path2)}}`;
|
|
29588
|
+
if (author) return `{action:"timeline", author:${q(author[1])}}`;
|
|
29589
|
+
if (sub === "shortlog") return `{action:"timeline"}`;
|
|
29590
|
+
return `{action:"overview"}`;
|
|
29591
|
+
}
|
|
29357
29592
|
function docsSources(root) {
|
|
29358
29593
|
try {
|
|
29359
29594
|
return readManifestAt(join11(root, ".eminiar", "docs.json")).sources;
|
|
@@ -29397,20 +29632,201 @@ var BIG_TEXT_EXT = /* @__PURE__ */ new Set([
|
|
|
29397
29632
|
function withoutHeredocs(cmd) {
|
|
29398
29633
|
return cmd.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?=\n|$)/g, "<<$2");
|
|
29399
29634
|
}
|
|
29400
|
-
function
|
|
29401
|
-
|
|
29402
|
-
|
|
29403
|
-
|
|
29404
|
-
|
|
29405
|
-
|
|
29406
|
-
|
|
29407
|
-
|
|
29408
|
-
|
|
29409
|
-
|
|
29410
|
-
|
|
29411
|
-
|
|
29412
|
-
|
|
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 {
|
|
29413
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
|
+
}
|
|
29826
|
+
function suggest(toolName, input2, root, extra = {}) {
|
|
29827
|
+
switch (toolName) {
|
|
29828
|
+
case "Bash":
|
|
29829
|
+
return bashSuggestion(String(input2.command ?? ""), extra.cwd ?? root);
|
|
29414
29830
|
case "Read": {
|
|
29415
29831
|
const fp = String(input2.file_path ?? "");
|
|
29416
29832
|
if (!fp) return void 0;
|
|
@@ -29422,6 +29838,7 @@ function suggest(toolName, input2, root) {
|
|
|
29422
29838
|
if (!checkReadPath(loadPermissionRules(root), abs).allowed) return void 0;
|
|
29423
29839
|
return {
|
|
29424
29840
|
tool: "em_execute_file",
|
|
29841
|
+
rule: "big-read",
|
|
29425
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`
|
|
29426
29843
|
};
|
|
29427
29844
|
}
|
|
@@ -29430,10 +29847,20 @@ function suggest(toolName, input2, root) {
|
|
|
29430
29847
|
return void 0;
|
|
29431
29848
|
}
|
|
29432
29849
|
case "Grep": {
|
|
29433
|
-
|
|
29850
|
+
const content = input2.output_mode === "content" && !input2.head_limit;
|
|
29851
|
+
if (extra.symbol) {
|
|
29852
|
+
return {
|
|
29853
|
+
tool: "em_symbols",
|
|
29854
|
+
rule: "symbol",
|
|
29855
|
+
annotateOnly: !content,
|
|
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`
|
|
29857
|
+
};
|
|
29858
|
+
}
|
|
29859
|
+
if (content) {
|
|
29434
29860
|
return {
|
|
29435
29861
|
tool: "em_execute",
|
|
29436
|
-
|
|
29862
|
+
rule: "content-grep",
|
|
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)`
|
|
29437
29864
|
};
|
|
29438
29865
|
}
|
|
29439
29866
|
return void 0;
|
|
@@ -29461,7 +29888,7 @@ function suggest(toolName, input2, root) {
|
|
|
29461
29888
|
return {
|
|
29462
29889
|
tool: "em_*",
|
|
29463
29890
|
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'
|
|
29891
|
+
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
29892
|
};
|
|
29466
29893
|
default:
|
|
29467
29894
|
return void 0;
|
|
@@ -29502,15 +29929,20 @@ function fileKnowledge(ctx, input2) {
|
|
|
29502
29929
|
if (!rel) return void 0;
|
|
29503
29930
|
const db = getStorage(ctx, { vectors: false });
|
|
29504
29931
|
const sessionId = input2.session_id || void 0;
|
|
29505
|
-
const
|
|
29506
|
-
|
|
29932
|
+
const repeatAfter = ctx.config.recall.repeatAfter;
|
|
29933
|
+
const edit = EDIT_TOOLS2.has(input2.tool_name ?? "");
|
|
29934
|
+
const results = [
|
|
29935
|
+
memoriesForFile(db, rel, { sessionId, symbols: fileSymbols(db, rel), repeatAfter }),
|
|
29936
|
+
fileContext(db, rel, { sessionId, edit, repeatAfter })
|
|
29937
|
+
];
|
|
29507
29938
|
const shown = results.filter((r) => r.text);
|
|
29508
29939
|
if (!shown.length) return void 0;
|
|
29509
29940
|
if (sessionId)
|
|
29510
29941
|
recordShown(
|
|
29511
29942
|
db,
|
|
29512
29943
|
sessionId,
|
|
29513
|
-
shown.flatMap((r) => r.keys)
|
|
29944
|
+
shown.flatMap((r) => r.keys),
|
|
29945
|
+
{ path: "file" }
|
|
29514
29946
|
);
|
|
29515
29947
|
return shown.map((r) => r.text).join("\n");
|
|
29516
29948
|
} catch {
|
|
@@ -29550,7 +29982,7 @@ async function webKnowledge(ctx, input2) {
|
|
|
29550
29982
|
if (!text.trim()) return void 0;
|
|
29551
29983
|
try {
|
|
29552
29984
|
const db = getStorage(ctx, { vectors: false });
|
|
29553
|
-
const r = await recallForText(db, text, { maxMemories: 0, maxDocs: 3 });
|
|
29985
|
+
const r = await recallForText(db, text, { maxMemories: 0, maxDocs: 3, path: "web" });
|
|
29554
29986
|
if (!r.text) return void 0;
|
|
29555
29987
|
const pages = r.text.split("\n").filter((l) => l.startsWith("- docs:"));
|
|
29556
29988
|
return [
|
|
@@ -29593,10 +30025,19 @@ var preToolUse = async (input2) => {
|
|
|
29593
30025
|
const coexisting = contextModeInstalled() && ctx.config.routing.deferToContextMode;
|
|
29594
30026
|
if (coexisting && ["Bash", "Read", "Grep", "WebFetch"].includes(input2.tool_name))
|
|
29595
30027
|
return withContext(void 0, memo2);
|
|
29596
|
-
const
|
|
29597
|
-
const
|
|
29598
|
-
|
|
29599
|
-
|
|
30028
|
+
const graph = graphContext(ctx, input2);
|
|
30029
|
+
const hint = joinHints(graph, memo2);
|
|
30030
|
+
const symbol2 = input2.tool_name === "Grep" && graph ? literalOf(String(input2.tool_input.pattern ?? "")) : void 0;
|
|
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
|
+
}
|
|
29600
30041
|
if (mode === "enforce") {
|
|
29601
30042
|
if (s.prefixPrompt) {
|
|
29602
30043
|
const prompt = String(input2.tool_input.prompt ?? "");
|
|
@@ -29617,6 +30058,7 @@ var preToolUse = async (input2) => {
|
|
|
29617
30058
|
"info",
|
|
29618
30059
|
`enforce \u2192 annotate for ${input2.tool_name}: no eminiar MCP server running for ${ctx.root}`
|
|
29619
30060
|
);
|
|
30061
|
+
record2("annotate", "no-server");
|
|
29620
30062
|
return withContext(
|
|
29621
30063
|
annotate(
|
|
29622
30064
|
s,
|
|
@@ -29625,6 +30067,8 @@ var preToolUse = async (input2) => {
|
|
|
29625
30067
|
hint
|
|
29626
30068
|
);
|
|
29627
30069
|
}
|
|
30070
|
+
record2("deny");
|
|
30071
|
+
hookLog("info", `deny ${input2.tool_name} \u2192 ${s.tool} [${s.rule ?? "?"}]: ${callHead(input2)}`);
|
|
29628
30072
|
return {
|
|
29629
30073
|
hookSpecificOutput: {
|
|
29630
30074
|
hookEventName: "PreToolUse",
|
|
@@ -29633,9 +30077,35 @@ var preToolUse = async (input2) => {
|
|
|
29633
30077
|
}
|
|
29634
30078
|
};
|
|
29635
30079
|
}
|
|
30080
|
+
record2("annotate", "mode");
|
|
29636
30081
|
const extra = s.prefixPrompt ? joinHints((await briefFor(ctx, String(input2.tool_input.prompt ?? ""))).trim(), hint) : hint;
|
|
29637
30082
|
return withContext(annotate(s), extra);
|
|
29638
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
|
+
}
|
|
29639
30109
|
async function briefFor(ctx, task) {
|
|
29640
30110
|
const parts = [];
|
|
29641
30111
|
try {
|
|
@@ -29651,7 +30121,8 @@ Verify structure with em_symbols / em_graph_impact before reading files; check e
|
|
|
29651
30121
|
if (recall.onAgent && task) {
|
|
29652
30122
|
const r = await recallForText(db, task, {
|
|
29653
30123
|
maxMemories: recall.maxMemories,
|
|
29654
|
-
maxDocs: recall.maxDocs
|
|
30124
|
+
maxDocs: recall.maxDocs,
|
|
30125
|
+
path: "agent"
|
|
29655
30126
|
});
|
|
29656
30127
|
if (r.text) parts.push(r.text);
|
|
29657
30128
|
}
|
|
@@ -29708,7 +30179,8 @@ function routingGuidance(mode) {
|
|
|
29708
30179
|
'- em_search {queries, sort:"timeline"}: everything indexed plus session memory.',
|
|
29709
30180
|
"- 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
30181
|
'- 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).",
|
|
30182
|
+
"- 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).",
|
|
30183
|
+
'- 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
30184
|
"Plain Bash/Read/Grep remain right for short fixed output and files you are about to edit."
|
|
29713
30185
|
].join("\n");
|
|
29714
30186
|
}
|
|
@@ -29775,11 +30247,22 @@ ${guide}`);
|
|
|
29775
30247
|
);
|
|
29776
30248
|
} catch {
|
|
29777
30249
|
}
|
|
30250
|
+
try {
|
|
30251
|
+
if (existsSync9(join13(ctx.root, ".git")) && !db.metaGet("history_files"))
|
|
30252
|
+
parts.push(
|
|
30253
|
+
'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.'
|
|
30254
|
+
);
|
|
30255
|
+
} catch {
|
|
30256
|
+
}
|
|
29778
30257
|
const docs = db.countDocuments();
|
|
29779
30258
|
const latest = db.sessionLatest();
|
|
29780
30259
|
parts.push(
|
|
29781
30260
|
`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
30261
|
);
|
|
30262
|
+
hookLog(
|
|
30263
|
+
"info",
|
|
30264
|
+
`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"}`
|
|
30265
|
+
);
|
|
29783
30266
|
return {
|
|
29784
30267
|
hookSpecificOutput: {
|
|
29785
30268
|
hookEventName: "SessionStart",
|