nlm-memory 0.12.0 → 0.13.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/README.md +1 -1
- package/dist/cli/classify-parity.js +1 -1
- package/dist/cli/classify-parity.js.map +1 -1
- package/dist/cli/nlm.js +314 -55
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/classifier/prompt.d.ts +70 -0
- package/dist/core/classifier/prompt.js +71 -2
- package/dist/core/classifier/prompt.js.map +1 -1
- package/dist/core/exemplars/extract-exemplar.d.ts +36 -0
- package/dist/core/exemplars/extract-exemplar.js +177 -0
- package/dist/core/exemplars/extract-exemplar.js.map +1 -0
- package/dist/core/exemplars/ingest-exemplar.d.ts +30 -0
- package/dist/core/exemplars/ingest-exemplar.js +71 -0
- package/dist/core/exemplars/ingest-exemplar.js.map +1 -0
- package/dist/core/exemplars/recall-code.d.ts +25 -0
- package/dist/core/exemplars/recall-code.js +49 -0
- package/dist/core/exemplars/recall-code.js.map +1 -0
- package/dist/core/facts/backfill-facts.d.ts +4 -2
- package/dist/core/facts/backfill-facts.js +18 -27
- package/dist/core/facts/backfill-facts.js.map +1 -1
- package/dist/core/hook/strip-injected-context.d.ts +24 -0
- package/dist/core/hook/strip-injected-context.js +73 -0
- package/dist/core/hook/strip-injected-context.js.map +1 -0
- package/dist/core/ingest/ingest-session.d.ts +4 -2
- package/dist/core/ingest/ingest-session.js +12 -4
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/providers/provider-registry.d.ts +31 -11
- package/dist/core/providers/provider-registry.js +27 -13
- package/dist/core/providers/provider-registry.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.js +75 -28
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +4 -2
- package/dist/core/scheduler/scheduler.js +26 -15
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/sources/source-registry.d.ts +36 -12
- package/dist/core/sources/source-registry.js +59 -44
- package/dist/core/sources/source-registry.js.map +1 -1
- package/dist/core/storage/backup-rotation.d.ts +38 -0
- package/dist/core/storage/backup-rotation.js +82 -0
- package/dist/core/storage/backup-rotation.js.map +1 -0
- package/dist/core/storage/null-code-exemplar-store.d.ts +21 -0
- package/dist/core/storage/null-code-exemplar-store.js +19 -0
- package/dist/core/storage/null-code-exemplar-store.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +2 -0
- package/dist/core/storage/pg-fact-store.js +34 -3
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-session-store.d.ts +14 -4
- package/dist/core/storage/pg-session-store.js +123 -2
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +11 -2
- package/dist/core/storage/pg-storage.js +14 -2
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-tx-context.d.ts +2 -0
- package/dist/core/storage/pg-tx-context.js +12 -0
- package/dist/core/storage/pg-tx-context.js.map +1 -1
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +40 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +247 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +2 -0
- package/dist/core/storage/sqlite-fact-store.js +30 -2
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +21 -0
- package/dist/core/storage/sqlite-session-store.js +23 -0
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +6 -0
- package/dist/core/storage/sqlite-storage.js +14 -2
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/supersedence-log.d.ts +8 -0
- package/dist/core/storage/supersedence-log.js +19 -0
- package/dist/core/storage/supersedence-log.js.map +1 -1
- package/dist/hook/prompt-recall-hook.d.ts +5 -4
- package/dist/hook/prompt-recall-hook.js +12 -7
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/http/app.d.ts +7 -1
- package/dist/http/app.js +226 -83
- package/dist/http/app.js.map +1 -1
- package/dist/install/codex.d.ts +29 -1
- package/dist/install/codex.js +117 -15
- package/dist/install/codex.js.map +1 -1
- package/dist/install/cursor.d.ts +4 -4
- package/dist/install/cursor.js +7 -7
- package/dist/install/cursor.js.map +1 -1
- package/dist/install/health.d.ts +56 -0
- package/dist/install/health.js +133 -0
- package/dist/install/health.js.map +1 -0
- package/dist/install/setup.js +1 -1
- package/dist/install/setup.js.map +1 -1
- package/dist/install/windsurf.d.ts +4 -4
- package/dist/install/windsurf.js +7 -7
- package/dist/install/windsurf.js.map +1 -1
- package/dist/llm/classifier-box.d.ts +4 -0
- package/dist/llm/classifier-box.js +9 -2
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/ollama-client.d.ts +19 -1
- package/dist/llm/ollama-client.js +14 -6
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +28 -0
- package/dist/llm/ollama-code-embedder.js +73 -0
- package/dist/llm/ollama-code-embedder.js.map +1 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +132 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +27 -0
- package/dist/ports/code-embedder.js +15 -0
- package/dist/ports/code-embedder.js.map +1 -0
- package/dist/ports/code-exemplar-store.d.ts +45 -0
- package/dist/ports/code-exemplar-store.js +11 -0
- package/dist/ports/code-exemplar-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/storage.d.ts +8 -0
- package/dist/shared/types.d.ts +35 -0
- package/dist/ui/assets/{index-Fr5AMEPj.js → index-CVo7KKw2.js} +1 -1
- package/dist/ui/assets/{index-C74N8APQ.css → index-DCuQrzJH.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/migrations/021_code_exemplars.sql +51 -0
- package/migrations/022_facts_retired_at.sql +17 -0
- package/migrations/pg/021_code_exemplars.sql +45 -0
- package/migrations/pg/022_facts_retired_at.sql +12 -0
- package/nlm/index.js +6 -1
- package/package.json +3 -2
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/hooks/hooks.json +13 -2
- package/plugin/scripts/prompt-recall-hook.mjs +7 -1
- package/plugin/scripts/session-start-hook.mjs +275 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hook/session-start-hook.ts
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
// src/core/hook/hook-log.ts
|
|
7
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
function logPath() {
|
|
11
|
+
return process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
|
|
12
|
+
}
|
|
13
|
+
function appendHookLog(entry) {
|
|
14
|
+
try {
|
|
15
|
+
const path = logPath();
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
+
appendFileSync(path, `${JSON.stringify(entry)}
|
|
18
|
+
`, "utf8");
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/core/hook/memo.ts
|
|
24
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { homedir as homedir2 } from "node:os";
|
|
26
|
+
import { join as join2 } from "node:path";
|
|
27
|
+
function stateDir() {
|
|
28
|
+
return process.env["NLM_HOOK_STATE_DIR"] ?? join2(homedir2(), ".nlm", "hook-state");
|
|
29
|
+
}
|
|
30
|
+
function memoPath(conversationId) {
|
|
31
|
+
const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
32
|
+
return join2(stateDir(), `${safe}.json`);
|
|
33
|
+
}
|
|
34
|
+
function loadSurfaced(conversationId) {
|
|
35
|
+
try {
|
|
36
|
+
const path = memoPath(conversationId);
|
|
37
|
+
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
38
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
40
|
+
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
41
|
+
} catch {
|
|
42
|
+
return /* @__PURE__ */ new Set();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function recordSurfaced(conversationId, ids) {
|
|
46
|
+
try {
|
|
47
|
+
const merged = loadSurfaced(conversationId);
|
|
48
|
+
for (const id of ids) merged.add(id);
|
|
49
|
+
mkdirSync2(stateDir(), { recursive: true });
|
|
50
|
+
writeFileSync(memoPath(conversationId), JSON.stringify([...merged]), "utf8");
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/core/hook/pointer-block.ts
|
|
56
|
+
function formatPointerBlock(hits, facts = []) {
|
|
57
|
+
if (hits.length === 0 && facts.length === 0) return "";
|
|
58
|
+
const out = [];
|
|
59
|
+
if (hits.length > 0) {
|
|
60
|
+
out.push("## Possibly-relevant prior sessions (nlm-memory)");
|
|
61
|
+
for (const h of hits) {
|
|
62
|
+
const datePart = h.startedAt.slice(0, 10);
|
|
63
|
+
if (h.summary) {
|
|
64
|
+
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${h.summary.slice(0, 120)}`);
|
|
65
|
+
} else {
|
|
66
|
+
out.push(`- ${h.id} \xB7 ${h.label} (${datePart})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (facts.length > 0) {
|
|
71
|
+
if (out.length > 0) out.push("");
|
|
72
|
+
out.push("## Known facts about top entities");
|
|
73
|
+
for (const f of facts) {
|
|
74
|
+
const tag = f.corroborationCount > 1 ? ` [${f.corroborationCount} sessions]` : "";
|
|
75
|
+
out.push(`- ${f.subject} ${f.predicate}: ${f.value}${tag}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
out.push(
|
|
79
|
+
"NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
|
|
80
|
+
);
|
|
81
|
+
return out.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/core/hook/select.ts
|
|
85
|
+
function selectHits(params) {
|
|
86
|
+
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
|
|
87
|
+
const eligible = hits.filter(
|
|
88
|
+
(h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
|
|
89
|
+
);
|
|
90
|
+
const budget = Math.max(0, perConversationCap - surfaced.size);
|
|
91
|
+
const limit = Math.min(perFireCap, budget);
|
|
92
|
+
return eligible.slice(0, limit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/llm/env-autoload.ts
|
|
96
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
97
|
+
import { homedir as homedir3 } from "node:os";
|
|
98
|
+
import { resolve } from "node:path";
|
|
99
|
+
var DEFAULT_SEARCH_PATHS = [
|
|
100
|
+
"~/.nlm/.env",
|
|
101
|
+
"./.env",
|
|
102
|
+
"../.env",
|
|
103
|
+
"../../.env"
|
|
104
|
+
];
|
|
105
|
+
function expandHome(p) {
|
|
106
|
+
if (p.startsWith("~/")) return resolve(homedir3(), p.slice(2));
|
|
107
|
+
return p;
|
|
108
|
+
}
|
|
109
|
+
function autoloadEnv(extraPaths = []) {
|
|
110
|
+
const loaded = [];
|
|
111
|
+
const paths = [...DEFAULT_SEARCH_PATHS, ...extraPaths];
|
|
112
|
+
for (const raw of paths) {
|
|
113
|
+
const path = expandHome(raw);
|
|
114
|
+
if (!existsSync2(path)) continue;
|
|
115
|
+
try {
|
|
116
|
+
const content = readFileSync2(path, "utf8");
|
|
117
|
+
for (const line of content.split("\n")) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
120
|
+
const eq = trimmed.indexOf("=");
|
|
121
|
+
const key = trimmed.slice(0, eq).trim();
|
|
122
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
123
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
124
|
+
value = value.slice(1, -1);
|
|
125
|
+
}
|
|
126
|
+
if (key && process.env[key] === void 0) {
|
|
127
|
+
process.env[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
loaded.push(path);
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return loaded;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/hook/hook-auth.ts
|
|
139
|
+
function hookAuthHeaders(extra = {}) {
|
|
140
|
+
const token = process.env["NLM_MCP_TOKEN"];
|
|
141
|
+
if (!token) return { ...extra };
|
|
142
|
+
return { ...extra, authorization: `Bearer ${token}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/hook/session-start-hook.ts
|
|
146
|
+
var SCORE_THRESHOLD = 0;
|
|
147
|
+
var PER_FIRE_CAP = 3;
|
|
148
|
+
var PER_CONVERSATION_CAP = 10;
|
|
149
|
+
var RECALL_LIMIT = 5;
|
|
150
|
+
var RECALL_TIMEOUT_MS = 2e3;
|
|
151
|
+
async function runHook(input, deps) {
|
|
152
|
+
let hits = [];
|
|
153
|
+
try {
|
|
154
|
+
hits = await deps.recall(input.query, input.conversationId);
|
|
155
|
+
} catch {
|
|
156
|
+
hits = [];
|
|
157
|
+
}
|
|
158
|
+
const surfaced = loadSurfaced(input.conversationId);
|
|
159
|
+
const selected = selectHits({
|
|
160
|
+
hits,
|
|
161
|
+
surfaced,
|
|
162
|
+
scoreThreshold: SCORE_THRESHOLD,
|
|
163
|
+
perFireCap: PER_FIRE_CAP,
|
|
164
|
+
perConversationCap: PER_CONVERSATION_CAP
|
|
165
|
+
});
|
|
166
|
+
const block = formatPointerBlock(selected);
|
|
167
|
+
const estTokens = Math.ceil(block.length / 4);
|
|
168
|
+
appendHookLog({
|
|
169
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
170
|
+
conversationId: input.conversationId,
|
|
171
|
+
promptPreview: input.query,
|
|
172
|
+
gate: "evaluate",
|
|
173
|
+
hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
|
|
174
|
+
wouldInject: selected.map((h) => h.id),
|
|
175
|
+
estTokens,
|
|
176
|
+
mode: deps.mode
|
|
177
|
+
});
|
|
178
|
+
if (deps.mode === "live" && selected.length > 0) {
|
|
179
|
+
recordSurfaced(input.conversationId, selected.map((h) => h.id));
|
|
180
|
+
return block;
|
|
181
|
+
}
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
function composeSessionStartOutput(failureModeBlock, recallBlock) {
|
|
185
|
+
return [failureModeBlock, recallBlock].filter((s) => s.length > 0).join("\n\n");
|
|
186
|
+
}
|
|
187
|
+
async function fetchFailureModeBlock(repo) {
|
|
188
|
+
if (!repo) return "";
|
|
189
|
+
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
190
|
+
const url = `http://localhost:${portValue}/api/signals/failure-modes?repo=${encodeURIComponent(repo)}`;
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(url, {
|
|
195
|
+
headers: hookAuthHeaders({ "x-recall-source": "session-start-hook" }),
|
|
196
|
+
signal: controller.signal
|
|
197
|
+
});
|
|
198
|
+
if (!res.ok) return "";
|
|
199
|
+
const body = await res.json();
|
|
200
|
+
return typeof body.block === "string" ? body.block : "";
|
|
201
|
+
} catch {
|
|
202
|
+
return "";
|
|
203
|
+
} finally {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function buildQuery(workingDirectory, projectName) {
|
|
208
|
+
const dirTail = workingDirectory.split("/").filter(Boolean).at(-1) ?? "";
|
|
209
|
+
const parts = [dirTail, projectName].filter(Boolean);
|
|
210
|
+
return parts.join(" ").trim() || "session start";
|
|
211
|
+
}
|
|
212
|
+
function readStdin() {
|
|
213
|
+
return new Promise((resolve2) => {
|
|
214
|
+
let data = "";
|
|
215
|
+
process.stdin.setEncoding("utf8");
|
|
216
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
217
|
+
process.stdin.on("end", () => resolve2(data));
|
|
218
|
+
process.stdin.on("error", () => resolve2(data));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async function recallOverHttp(query, conversationId) {
|
|
222
|
+
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
223
|
+
const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=hybrid&limit=${RECALL_LIMIT}` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "");
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch(url, {
|
|
228
|
+
headers: hookAuthHeaders({
|
|
229
|
+
"x-recall-source": "session-start-hook",
|
|
230
|
+
"x-recall-runtime": "claude-code"
|
|
231
|
+
}),
|
|
232
|
+
signal: controller.signal
|
|
233
|
+
});
|
|
234
|
+
if (!res.ok) return [];
|
|
235
|
+
let body;
|
|
236
|
+
try {
|
|
237
|
+
body = await res.json();
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
return (body.results ?? []).map((r) => ({
|
|
242
|
+
id: r.id,
|
|
243
|
+
label: r.label,
|
|
244
|
+
startedAt: r.startedAt,
|
|
245
|
+
matchScore: r.matchScore,
|
|
246
|
+
...r.summary !== void 0 ? { summary: r.summary } : {}
|
|
247
|
+
}));
|
|
248
|
+
} finally {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function main() {
|
|
253
|
+
try {
|
|
254
|
+
autoloadEnv();
|
|
255
|
+
const raw = await readStdin();
|
|
256
|
+
const payload = JSON.parse(raw);
|
|
257
|
+
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
258
|
+
const workingDirectory = typeof payload.cwd === "string" ? payload.cwd : typeof payload.working_directory === "string" ? payload.working_directory : "";
|
|
259
|
+
const projectName = typeof payload.project_name === "string" ? payload.project_name : "";
|
|
260
|
+
const query = buildQuery(workingDirectory, projectName);
|
|
261
|
+
const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
262
|
+
const out = await runHook({ conversationId, query }, { mode, recall: (q, cid) => recallOverHttp(q, cid === "unknown" ? void 0 : cid) });
|
|
263
|
+
const failureModes = mode === "live" ? await fetchFailureModeBlock(workingDirectory) : "";
|
|
264
|
+
const combined = composeSessionStartOutput(failureModes, out);
|
|
265
|
+
if (combined) process.stdout.write(combined);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
270
|
+
void main();
|
|
271
|
+
}
|
|
272
|
+
export {
|
|
273
|
+
composeSessionStartOutput,
|
|
274
|
+
runHook
|
|
275
|
+
};
|