engrm 0.4.27 → 0.4.28
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/README.md +4 -2
- package/dist/cli.js +84 -0
- package/dist/hooks/elicitation-result.js +73 -0
- package/dist/hooks/post-tool-use.js +81 -2
- package/dist/hooks/pre-compact.js +81 -2
- package/dist/hooks/sentinel.js +70 -0
- package/dist/hooks/session-start.js +77 -1
- package/dist/hooks/stop.js +88 -3
- package/dist/hooks/user-prompt-submit.js +146 -61
- package/dist/server.js +121 -10
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -14230,6 +14230,17 @@ var MIGRATIONS = [
|
|
|
14230
14230
|
ON chat_messages(session_id, transcript_index)
|
|
14231
14231
|
WHERE transcript_index IS NOT NULL;
|
|
14232
14232
|
`
|
|
14233
|
+
},
|
|
14234
|
+
{
|
|
14235
|
+
version: 18,
|
|
14236
|
+
description: "Add sqlite-vec semantic search for chat recall",
|
|
14237
|
+
condition: (db) => isVecExtensionLoaded(db),
|
|
14238
|
+
sql: `
|
|
14239
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
14240
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
14241
|
+
embedding FLOAT[384]
|
|
14242
|
+
);
|
|
14243
|
+
`
|
|
14233
14244
|
}
|
|
14234
14245
|
];
|
|
14235
14246
|
function isVecExtensionLoaded(db) {
|
|
@@ -14303,6 +14314,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
14303
14314
|
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
14304
14315
|
version2 = Math.max(version2, 17);
|
|
14305
14316
|
}
|
|
14317
|
+
if (tableExists(db, "vec_chat_messages")) {
|
|
14318
|
+
version2 = Math.max(version2, 18);
|
|
14319
|
+
}
|
|
14306
14320
|
return version2;
|
|
14307
14321
|
}
|
|
14308
14322
|
function runMigrations(db) {
|
|
@@ -14419,6 +14433,20 @@ function ensureChatMessageColumns(db) {
|
|
|
14419
14433
|
db.exec("PRAGMA user_version = 17");
|
|
14420
14434
|
}
|
|
14421
14435
|
}
|
|
14436
|
+
function ensureChatVectorTable(db) {
|
|
14437
|
+
if (!isVecExtensionLoaded(db))
|
|
14438
|
+
return;
|
|
14439
|
+
db.exec(`
|
|
14440
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
14441
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
14442
|
+
embedding FLOAT[384]
|
|
14443
|
+
);
|
|
14444
|
+
`);
|
|
14445
|
+
const current = getSchemaVersion(db);
|
|
14446
|
+
if (current < 18) {
|
|
14447
|
+
db.exec("PRAGMA user_version = 18");
|
|
14448
|
+
}
|
|
14449
|
+
}
|
|
14422
14450
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
14423
14451
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
14424
14452
|
const current = getSchemaVersion(db);
|
|
@@ -14632,6 +14660,7 @@ class MemDatabase {
|
|
|
14632
14660
|
ensureObservationTypes(this.db);
|
|
14633
14661
|
ensureSessionSummaryColumns(this.db);
|
|
14634
14662
|
ensureChatMessageColumns(this.db);
|
|
14663
|
+
ensureChatVectorTable(this.db);
|
|
14635
14664
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
14636
14665
|
}
|
|
14637
14666
|
loadVecExtension() {
|
|
@@ -14998,6 +15027,14 @@ class MemDatabase {
|
|
|
14998
15027
|
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
14999
15028
|
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
15000
15029
|
}
|
|
15030
|
+
getChatMessagesByIds(ids) {
|
|
15031
|
+
if (ids.length === 0)
|
|
15032
|
+
return [];
|
|
15033
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
15034
|
+
const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
|
|
15035
|
+
const order = new Map(ids.map((id, index) => [id, index]));
|
|
15036
|
+
return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
15037
|
+
}
|
|
15001
15038
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
15002
15039
|
return this.db.query(`SELECT * FROM chat_messages
|
|
15003
15040
|
WHERE session_id = ?
|
|
@@ -15074,6 +15111,39 @@ class MemDatabase {
|
|
|
15074
15111
|
ORDER BY created_at_epoch DESC, id DESC
|
|
15075
15112
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
15076
15113
|
}
|
|
15114
|
+
vecChatInsert(chatMessageId, embedding) {
|
|
15115
|
+
if (!this.vecAvailable)
|
|
15116
|
+
return;
|
|
15117
|
+
this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
|
|
15118
|
+
}
|
|
15119
|
+
searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
|
|
15120
|
+
if (!this.vecAvailable)
|
|
15121
|
+
return [];
|
|
15122
|
+
const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
|
|
15123
|
+
const visibilityClause = userId ? " AND c.user_id = ?" : "";
|
|
15124
|
+
const transcriptPreference = `
|
|
15125
|
+
AND (
|
|
15126
|
+
c.source_kind = 'transcript'
|
|
15127
|
+
OR NOT EXISTS (
|
|
15128
|
+
SELECT 1 FROM chat_messages t2
|
|
15129
|
+
WHERE t2.session_id = c.session_id
|
|
15130
|
+
AND t2.source_kind = 'transcript'
|
|
15131
|
+
)
|
|
15132
|
+
)`;
|
|
15133
|
+
if (projectId !== null) {
|
|
15134
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
15135
|
+
FROM vec_chat_messages v
|
|
15136
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
15137
|
+
WHERE v.embedding MATCH ?
|
|
15138
|
+
AND k = ?
|
|
15139
|
+
AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
|
|
15140
|
+
}
|
|
15141
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
15142
|
+
FROM vec_chat_messages v
|
|
15143
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
15144
|
+
WHERE v.embedding MATCH ?
|
|
15145
|
+
AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
|
|
15146
|
+
}
|
|
15077
15147
|
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
15078
15148
|
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
15079
15149
|
}
|
|
@@ -15815,6 +15885,9 @@ function composeEmbeddingText(obs) {
|
|
|
15815
15885
|
|
|
15816
15886
|
`);
|
|
15817
15887
|
}
|
|
15888
|
+
function composeChatEmbeddingText(text) {
|
|
15889
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 2000);
|
|
15890
|
+
}
|
|
15818
15891
|
async function getPipeline() {
|
|
15819
15892
|
if (_pipeline)
|
|
15820
15893
|
return _pipeline;
|
|
@@ -16322,7 +16395,7 @@ function sanitizeFtsQuery(query) {
|
|
|
16322
16395
|
}
|
|
16323
16396
|
|
|
16324
16397
|
// src/tools/search-chat.ts
|
|
16325
|
-
function searchChat(db, input) {
|
|
16398
|
+
async function searchChat(db, input) {
|
|
16326
16399
|
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16327
16400
|
const projectScoped = input.project_scoped !== false;
|
|
16328
16401
|
let projectId = null;
|
|
@@ -16336,15 +16409,39 @@ function searchChat(db, input) {
|
|
|
16336
16409
|
projectName = project.name;
|
|
16337
16410
|
}
|
|
16338
16411
|
}
|
|
16339
|
-
const
|
|
16412
|
+
const lexical = db.searchChatMessages(input.query, projectId, limit * 2, input.user_id);
|
|
16413
|
+
let semantic = [];
|
|
16414
|
+
const queryEmbedding = db.vecAvailable ? await embedText(composeChatEmbeddingText(queryForEmbedding(input.query))) : null;
|
|
16415
|
+
if (queryEmbedding && db.vecAvailable) {
|
|
16416
|
+
semantic = db.searchChatVec(queryEmbedding, projectId, limit * 2, input.user_id);
|
|
16417
|
+
}
|
|
16418
|
+
const messageIds = mergeChatResults(lexical, semantic, limit);
|
|
16419
|
+
const messages = messageIds.length > 0 ? db.getChatMessagesByIds(messageIds) : [];
|
|
16340
16420
|
return {
|
|
16341
16421
|
messages,
|
|
16342
16422
|
project: projectName,
|
|
16343
16423
|
session_count: countDistinctSessions(messages),
|
|
16344
16424
|
source_summary: summarizeChatSources(messages),
|
|
16345
|
-
transcript_backed: messages.some((message) => message.source_kind === "transcript")
|
|
16425
|
+
transcript_backed: messages.some((message) => message.source_kind === "transcript"),
|
|
16426
|
+
semantic_backed: semantic.length > 0
|
|
16346
16427
|
};
|
|
16347
16428
|
}
|
|
16429
|
+
var RRF_K2 = 40;
|
|
16430
|
+
function mergeChatResults(lexical, semantic, limit) {
|
|
16431
|
+
const scores = new Map;
|
|
16432
|
+
for (let rank = 0;rank < lexical.length; rank++) {
|
|
16433
|
+
const message = lexical[rank];
|
|
16434
|
+
scores.set(message.id, (scores.get(message.id) ?? 0) + 1 / (RRF_K2 + rank + 1));
|
|
16435
|
+
}
|
|
16436
|
+
for (let rank = 0;rank < semantic.length; rank++) {
|
|
16437
|
+
const match = semantic[rank];
|
|
16438
|
+
scores.set(match.chat_message_id, (scores.get(match.chat_message_id) ?? 0) + 1 / (RRF_K2 + rank + 1));
|
|
16439
|
+
}
|
|
16440
|
+
return Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([id]) => id);
|
|
16441
|
+
}
|
|
16442
|
+
function queryForEmbedding(query) {
|
|
16443
|
+
return query.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
16444
|
+
}
|
|
16348
16445
|
function summarizeChatSources(messages) {
|
|
16349
16446
|
return messages.reduce((summary, message) => {
|
|
16350
16447
|
summary[message.source_kind] += 1;
|
|
@@ -16368,13 +16465,13 @@ async function searchRecall(db, input) {
|
|
|
16368
16465
|
const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
|
|
16369
16466
|
const [memory, chat] = await Promise.all([
|
|
16370
16467
|
searchObservations(db, input),
|
|
16371
|
-
|
|
16468
|
+
searchChat(db, {
|
|
16372
16469
|
query,
|
|
16373
16470
|
limit: limit * 2,
|
|
16374
16471
|
project_scoped: input.project_scoped,
|
|
16375
16472
|
cwd: input.cwd,
|
|
16376
16473
|
user_id: input.user_id
|
|
16377
|
-
})
|
|
16474
|
+
})
|
|
16378
16475
|
]);
|
|
16379
16476
|
const merged = mergeRecallResults(memory.observations, chat.messages, limit);
|
|
16380
16477
|
return {
|
|
@@ -18220,6 +18317,9 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
|
|
|
18220
18317
|
if (requestCount > 0 || toolCount > 0) {
|
|
18221
18318
|
suggested.push("activity_feed");
|
|
18222
18319
|
}
|
|
18320
|
+
if (requestCount > 0 || recentChatCount > 0 || observationCount > 0) {
|
|
18321
|
+
suggested.push("search_recall");
|
|
18322
|
+
}
|
|
18223
18323
|
if (observationCount > 0) {
|
|
18224
18324
|
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
18225
18325
|
}
|
|
@@ -18317,6 +18417,8 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
|
|
|
18317
18417
|
suggested.push("recent_sessions");
|
|
18318
18418
|
if (requestCount > 0 || toolCount > 0)
|
|
18319
18419
|
suggested.push("activity_feed");
|
|
18420
|
+
if (requestCount > 0 || chatCount > 0 || observationCount > 0)
|
|
18421
|
+
suggested.push("search_recall");
|
|
18320
18422
|
if (observationCount > 0)
|
|
18321
18423
|
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
18322
18424
|
if (sessionCount > 0)
|
|
@@ -19192,6 +19294,9 @@ function buildSuggestedTools2(context, transcriptBackedChat) {
|
|
|
19192
19294
|
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
|
|
19193
19295
|
tools.push("activity_feed");
|
|
19194
19296
|
}
|
|
19297
|
+
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
|
|
19298
|
+
tools.push("search_recall");
|
|
19299
|
+
}
|
|
19195
19300
|
if (context.observations.length > 0) {
|
|
19196
19301
|
tools.push("tool_memory_index", "capture_git_worktree");
|
|
19197
19302
|
}
|
|
@@ -21177,7 +21282,7 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
21177
21282
|
}
|
|
21178
21283
|
return messages;
|
|
21179
21284
|
}
|
|
21180
|
-
function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
21285
|
+
async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
21181
21286
|
const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
21182
21287
|
...message,
|
|
21183
21288
|
text: message.text.trim()
|
|
@@ -21207,6 +21312,12 @@ function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
|
21207
21312
|
transcript_index: transcriptIndex
|
|
21208
21313
|
});
|
|
21209
21314
|
db.addToOutbox("chat_message", row.id);
|
|
21315
|
+
if (db.vecAvailable) {
|
|
21316
|
+
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
21317
|
+
if (embedding) {
|
|
21318
|
+
db.vecChatInsert(row.id, embedding);
|
|
21319
|
+
}
|
|
21320
|
+
}
|
|
21210
21321
|
imported++;
|
|
21211
21322
|
}
|
|
21212
21323
|
return { imported, total: messages.length };
|
|
@@ -21282,7 +21393,7 @@ process.on("SIGTERM", () => {
|
|
|
21282
21393
|
});
|
|
21283
21394
|
var server = new McpServer({
|
|
21284
21395
|
name: "engrm",
|
|
21285
|
-
version: "0.4.
|
|
21396
|
+
version: "0.4.28"
|
|
21286
21397
|
});
|
|
21287
21398
|
server.tool("save_observation", "Save an observation to memory", {
|
|
21288
21399
|
type: exports_external.enum([
|
|
@@ -22506,7 +22617,7 @@ server.tool("refresh_chat_recall", "Hydrate the separate chat lane from the curr
|
|
|
22506
22617
|
content: [{ type: "text", text: "No session available to hydrate chat recall from." }]
|
|
22507
22618
|
};
|
|
22508
22619
|
}
|
|
22509
|
-
const result = syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
|
|
22620
|
+
const result = await syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
|
|
22510
22621
|
return {
|
|
22511
22622
|
content: [
|
|
22512
22623
|
{
|
|
@@ -22622,10 +22733,10 @@ server.tool("search_chat", "Search the separate chat lane without mixing it into
|
|
|
22622
22733
|
cwd: exports_external.string().optional(),
|
|
22623
22734
|
user_id: exports_external.string().optional()
|
|
22624
22735
|
}, async (params) => {
|
|
22625
|
-
const result = searchChat(db, params);
|
|
22736
|
+
const result = await searchChat(db, params);
|
|
22626
22737
|
const projectLine = result.project ? `Project: ${result.project}
|
|
22627
22738
|
` : "";
|
|
22628
|
-
const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · hook ${result.source_summary.hook}
|
|
22739
|
+
const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · hook ${result.source_summary.hook}` + `${result.semantic_backed ? " · semantic yes" : ""}
|
|
22629
22740
|
` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall if this looks under-captured.
|
|
22630
22741
|
`}`;
|
|
22631
22742
|
const rows = result.messages.length > 0 ? result.messages.map((msg) => {
|