nlm-memory 0.5.0 → 0.5.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.
Files changed (247) hide show
  1. package/README.md +72 -34
  2. package/dist/cli/nlm.js +2 -1
  3. package/dist/cli/nlm.js.map +1 -1
  4. package/dist/http/app.js +2 -1
  5. package/dist/http/app.js.map +1 -1
  6. package/dist/mcp/server.js +20 -1
  7. package/dist/mcp/server.js.map +1 -1
  8. package/dist/ui/assets/{index-C8cpwbYJ.css → index-Beo8psd-.css} +1 -1
  9. package/dist/ui/assets/{index-CB50QnL-.js → index-CSPTTeeM.js} +8 -8
  10. package/dist/ui/index.html +2 -2
  11. package/package.json +26 -1
  12. package/.agents/plugins/marketplace.json +0 -20
  13. package/.github/workflows/ci.yml +0 -30
  14. package/docs/methodology/re-derivation-rate.md +0 -112
  15. package/docs/methodology/useful-hit-rate.md +0 -79
  16. package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
  17. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
  18. package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
  19. package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
  20. package/docs/plans/desktop-product.md +0 -69
  21. package/docs/plans/factstore-design.md +0 -236
  22. package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1575
  23. package/logs/CHANGELOG/CHANGELOG.md +0 -209
  24. package/migrations/000_initial_schema.sql +0 -174
  25. package/migrations/001_entity_type_rename.sql +0 -17
  26. package/migrations/002_adapter_state_extend.sql +0 -12
  27. package/migrations/003_session_embeddings.sql +0 -11
  28. package/migrations/004_facts.sql +0 -46
  29. package/migrations/005_sources.sql +0 -31
  30. package/migrations/006_providers.sql +0 -33
  31. package/migrations/007_source_tokens.sql +0 -17
  32. package/migrations/008_fts_rebuild.sql +0 -9
  33. package/migrations/009_session_embedding_chunks.sql +0 -46
  34. package/migrations/010_sources_opencode.sql +0 -30
  35. package/migrations/011_sources_hermes_agent.sql +0 -30
  36. package/migrations/012_sources_aider.sql +0 -30
  37. package/migrations/013_adapter_state_failure_count.sql +0 -12
  38. package/migrations/014_sources_cursor.sql +0 -30
  39. package/migrations/015_sources_windsurf.sql +0 -30
  40. package/plugin-hermes-agent/README.md +0 -49
  41. package/plugin-hermes-agent/__init__.py +0 -75
  42. package/plugin-hermes-agent/plugin.yaml +0 -15
  43. package/scripts/backfill-citations.mjs +0 -0
  44. package/scripts/build-codex-plugin.mjs +0 -61
  45. package/scripts/deepseek-probe.mjs +0 -67
  46. package/scripts/extract-triples.mjs +0 -207
  47. package/scripts/longmemeval/embedding-cache.ts +0 -77
  48. package/scripts/longmemeval/fetch-dataset.sh +0 -25
  49. package/scripts/longmemeval/run-harness.ts +0 -315
  50. package/scripts/longmemeval/scorer.ts +0 -99
  51. package/scripts/longmemeval/tsconfig.json +0 -9
  52. package/scripts/longmemeval/types.ts +0 -35
  53. package/scripts/nlm-daily-digest.py +0 -239
  54. package/scripts/nlm-daily-digest.sh +0 -28
  55. package/src/cli/classify-parity.ts +0 -257
  56. package/src/cli/launchctl-helpers.ts +0 -49
  57. package/src/cli/nlm.ts +0 -1078
  58. package/src/core/actions/actions-log.ts +0 -118
  59. package/src/core/actions/overlay.ts +0 -117
  60. package/src/core/adapters/aider.ts +0 -205
  61. package/src/core/adapters/claude-code.ts +0 -293
  62. package/src/core/adapters/common.ts +0 -54
  63. package/src/core/adapters/cursor.ts +0 -486
  64. package/src/core/adapters/from-source.ts +0 -67
  65. package/src/core/adapters/hermes-agent.ts +0 -240
  66. package/src/core/adapters/hermes.ts +0 -277
  67. package/src/core/adapters/jsonl-generic.ts +0 -208
  68. package/src/core/adapters/opencode.ts +0 -281
  69. package/src/core/adapters/pi.ts +0 -264
  70. package/src/core/adapters/windsurf.ts +0 -386
  71. package/src/core/classifier/prompt.ts +0 -200
  72. package/src/core/dataset/build-dataset.ts +0 -463
  73. package/src/core/embedding/chunk-body.ts +0 -76
  74. package/src/core/embedding/embed-backfill.ts +0 -210
  75. package/src/core/embedding/embed-normalize.ts +0 -135
  76. package/src/core/facts/backfill-facts.ts +0 -254
  77. package/src/core/facts/extract-facts.ts +0 -50
  78. package/src/core/hook/citation-detect.ts +0 -124
  79. package/src/core/hook/cite-memo.ts +0 -68
  80. package/src/core/hook/claude-settings.ts +0 -187
  81. package/src/core/hook/gate.ts +0 -25
  82. package/src/core/hook/hook-log.ts +0 -41
  83. package/src/core/hook/memo-sweep.ts +0 -164
  84. package/src/core/hook/memo.ts +0 -67
  85. package/src/core/hook/pointer-block.ts +0 -26
  86. package/src/core/hook/select.ts +0 -32
  87. package/src/core/hook/transcript.ts +0 -121
  88. package/src/core/ingest/ingest-session.ts +0 -111
  89. package/src/core/providers/provider-models.ts +0 -100
  90. package/src/core/providers/provider-registry.ts +0 -196
  91. package/src/core/recall/citation-log.ts +0 -108
  92. package/src/core/recall/filter.ts +0 -27
  93. package/src/core/recall/index.ts +0 -6
  94. package/src/core/recall/match-fields.ts +0 -40
  95. package/src/core/recall/query-log.ts +0 -149
  96. package/src/core/recall/query-shape.ts +0 -66
  97. package/src/core/recall/recall-service.ts +0 -320
  98. package/src/core/recall/recent-log.ts +0 -59
  99. package/src/core/recall/tokenize.ts +0 -18
  100. package/src/core/recall/useful-scan.ts +0 -336
  101. package/src/core/recall-facts/fact-query-log.ts +0 -150
  102. package/src/core/recall-facts/fact-recall-service.ts +0 -327
  103. package/src/core/scheduler/scan-once.ts +0 -142
  104. package/src/core/scheduler/scheduler.ts +0 -225
  105. package/src/core/sources/source-registry.ts +0 -278
  106. package/src/core/storage/db-restore.ts +0 -133
  107. package/src/core/storage/live-status.ts +0 -45
  108. package/src/core/storage/migrate.ts +0 -72
  109. package/src/core/storage/sqlite-fact-store.ts +0 -304
  110. package/src/core/storage/sqlite-session-store.ts +0 -810
  111. package/src/hook/hook-auth.ts +0 -18
  112. package/src/hook/prompt-recall-hook.ts +0 -180
  113. package/src/hook/session-end-hook.ts +0 -81
  114. package/src/hook/session-start-hook.ts +0 -168
  115. package/src/hook/stop-hook.ts +0 -239
  116. package/src/http/app.ts +0 -1215
  117. package/src/install/claude-code.ts +0 -128
  118. package/src/install/codex.ts +0 -367
  119. package/src/install/cursor.ts +0 -68
  120. package/src/install/hermes-agent.ts +0 -76
  121. package/src/install/hermes.ts +0 -78
  122. package/src/install/nlm-dir-perms.ts +0 -55
  123. package/src/install/ollama.ts +0 -284
  124. package/src/install/setup.ts +0 -489
  125. package/src/install/windsurf.ts +0 -68
  126. package/src/llm/classifier-box.ts +0 -64
  127. package/src/llm/deepseek-client.ts +0 -150
  128. package/src/llm/env-autoload.ts +0 -55
  129. package/src/llm/ollama-client.ts +0 -189
  130. package/src/mcp/server.ts +0 -534
  131. package/src/ports/fact-store.ts +0 -102
  132. package/src/ports/llm-client.ts +0 -52
  133. package/src/ports/logger.ts +0 -16
  134. package/src/ports/session-store.ts +0 -45
  135. package/src/ports/transcript-adapter.ts +0 -55
  136. package/src/shared/types.ts +0 -149
  137. package/src/ui/App.tsx +0 -58
  138. package/src/ui/components/PromoteOpenButton.tsx +0 -65
  139. package/src/ui/components/SessionDrawer.tsx +0 -199
  140. package/src/ui/components/SideNav.tsx +0 -162
  141. package/src/ui/components/Skeleton.tsx +0 -107
  142. package/src/ui/index.html +0 -13
  143. package/src/ui/lib/actions.ts +0 -30
  144. package/src/ui/lib/api.ts +0 -92
  145. package/src/ui/lib/dataset.ts +0 -141
  146. package/src/ui/lib/registries.ts +0 -155
  147. package/src/ui/lib/view-settings.ts +0 -41
  148. package/src/ui/main.tsx +0 -15
  149. package/src/ui/pages/Live.tsx +0 -229
  150. package/src/ui/pages/Pulse.tsx +0 -415
  151. package/src/ui/pages/Recall.tsx +0 -190
  152. package/src/ui/pages/River.tsx +0 -354
  153. package/src/ui/pages/Search.tsx +0 -386
  154. package/src/ui/pages/Stub.tsx +0 -9
  155. package/src/ui/pages/Thread.tsx +0 -473
  156. package/src/ui/pages/settings/Classifier.tsx +0 -227
  157. package/src/ui/pages/settings/Data.tsx +0 -190
  158. package/src/ui/pages/settings/Index.tsx +0 -65
  159. package/src/ui/pages/settings/Labels.tsx +0 -224
  160. package/src/ui/pages/settings/Providers.tsx +0 -305
  161. package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
  162. package/src/ui/pages/settings/Sources.tsx +0 -326
  163. package/src/ui/pages/settings/Views.tsx +0 -96
  164. package/src/ui/styles.css +0 -1890
  165. package/src/ui/tsconfig.json +0 -21
  166. package/src/ui/vite.config.ts +0 -19
  167. package/tests/fixtures/claude_code/short_session.jsonl +0 -2
  168. package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
  169. package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
  170. package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
  171. package/tests/fixtures/facts.ts +0 -17
  172. package/tests/fixtures/golden-corpus.ts +0 -85
  173. package/tests/fixtures/hermes/paired_request_dump.json +0 -24
  174. package/tests/fixtures/hermes/paired_session.json +0 -23
  175. package/tests/fixtures/hermes/request_dump.json +0 -28
  176. package/tests/fixtures/hermes/session_iso.json +0 -38
  177. package/tests/fixtures/hermes/session_unix.json +0 -38
  178. package/tests/fixtures/hermes/system_only.json +0 -18
  179. package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
  180. package/tests/fixtures/pi/short-successful.jsonl +0 -5
  181. package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
  182. package/tests/fixtures/sessions.ts +0 -22
  183. package/tests/integration/backfill-facts.test.ts +0 -362
  184. package/tests/integration/citation-explicit.test.ts +0 -111
  185. package/tests/integration/cite-event.test.ts +0 -169
  186. package/tests/integration/cite-memo.test.ts +0 -87
  187. package/tests/integration/db-restore.test.ts +0 -153
  188. package/tests/integration/embed-backfill.test.ts +0 -176
  189. package/tests/integration/fact-supersedence.test.ts +0 -313
  190. package/tests/integration/fts-index.test.ts +0 -60
  191. package/tests/integration/getbyids-sqlite.test.ts +0 -100
  192. package/tests/integration/hermes-agent-hooks.test.ts +0 -248
  193. package/tests/integration/hook-claude-settings.test.ts +0 -218
  194. package/tests/integration/hook-log.test.ts +0 -54
  195. package/tests/integration/hook-memo.test.ts +0 -68
  196. package/tests/integration/hook-pre-compact.test.ts +0 -105
  197. package/tests/integration/hook-subagent-start.test.ts +0 -102
  198. package/tests/integration/http.test.ts +0 -401
  199. package/tests/integration/keyword-search-fts.test.ts +0 -66
  200. package/tests/integration/mcp-recall-logging.test.ts +0 -88
  201. package/tests/integration/mcp.test.ts +0 -260
  202. package/tests/integration/memo-sweep.test.ts +0 -91
  203. package/tests/integration/prompt-recall-hook.test.ts +0 -88
  204. package/tests/integration/provider-registry.test.ts +0 -107
  205. package/tests/integration/recall-golden.test.ts +0 -59
  206. package/tests/integration/recall-sqlite.test.ts +0 -169
  207. package/tests/integration/scheduler.test.ts +0 -391
  208. package/tests/integration/session-end-hook.test.ts +0 -48
  209. package/tests/integration/session-start-hook.test.ts +0 -126
  210. package/tests/integration/source-registry.test.ts +0 -122
  211. package/tests/integration/sqlite-fact-store.test.ts +0 -346
  212. package/tests/integration/stop-hook.test.ts +0 -560
  213. package/tests/integration/wal-checkpoint.test.ts +0 -49
  214. package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
  215. package/tests/unit/core/adapters/aider.test.ts +0 -230
  216. package/tests/unit/core/adapters/claude-code.test.ts +0 -118
  217. package/tests/unit/core/adapters/cursor.test.ts +0 -485
  218. package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
  219. package/tests/unit/core/adapters/hermes.test.ts +0 -81
  220. package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
  221. package/tests/unit/core/adapters/opencode.test.ts +0 -354
  222. package/tests/unit/core/adapters/pi.test.ts +0 -110
  223. package/tests/unit/core/adapters/windsurf.test.ts +0 -416
  224. package/tests/unit/core/classifier/prompt.test.ts +0 -126
  225. package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
  226. package/tests/unit/core/facts/extract-facts.test.ts +0 -117
  227. package/tests/unit/core/filter.test.ts +0 -40
  228. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
  229. package/tests/unit/core/hook/citation-detect.test.ts +0 -124
  230. package/tests/unit/core/hook/gate.test.ts +0 -29
  231. package/tests/unit/core/hook/pointer-block.test.ts +0 -22
  232. package/tests/unit/core/hook/select.test.ts +0 -66
  233. package/tests/unit/core/match-fields.test.ts +0 -39
  234. package/tests/unit/core/mcp-cite-session.test.ts +0 -51
  235. package/tests/unit/core/providers/provider-models.test.ts +0 -101
  236. package/tests/unit/core/query-shape.test.ts +0 -92
  237. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
  238. package/tests/unit/core/recall-service.test.ts +0 -200
  239. package/tests/unit/core/storage/live-status.test.ts +0 -54
  240. package/tests/unit/core/tokenize.test.ts +0 -32
  241. package/tests/unit/core/useful-scan.test.ts +0 -537
  242. package/tests/unit/llm/embed.test.ts +0 -93
  243. package/tests/unit/llm/ollama-client.test.ts +0 -124
  244. package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
  245. package/tsconfig.json +0 -31
  246. package/tsconfig.test.json +0 -11
  247. package/vitest.config.ts +0 -22
@@ -1,67 +0,0 @@
1
- /**
2
- * Per-conversation dedup memo for the recall hook. One JSON file per
3
- * conversation holds the set of session ids already surfaced, so each is
4
- * surfaced at most once per conversation.
5
- *
6
- * State dir defaults to ~/.nlm/hook-state/, overridable via
7
- * NLM_HOOK_STATE_DIR (testability — mirrors query-log.ts).
8
- *
9
- * Every function is defensive: a missing or corrupt file yields an empty
10
- * memo, and a write failure is swallowed. The hook must never break on memo
11
- * I/O.
12
- */
13
-
14
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15
- import { homedir } from "node:os";
16
- import { join } from "node:path";
17
-
18
- function stateDir(): string {
19
- return process.env["NLM_HOOK_STATE_DIR"] ?? join(homedir(), ".nlm", "hook-state");
20
- }
21
-
22
- function memoPath(conversationId: string): string {
23
- const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
24
- return join(stateDir(), `${safe}.json`);
25
- }
26
-
27
- export function loadSurfaced(conversationId: string): Set<string> {
28
- try {
29
- const path = memoPath(conversationId);
30
- if (!existsSync(path)) return new Set();
31
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
32
- if (!Array.isArray(parsed)) return new Set();
33
- return new Set(parsed.filter((x): x is string => typeof x === "string"));
34
- } catch {
35
- return new Set();
36
- }
37
- }
38
-
39
- export function recordSurfaced(
40
- conversationId: string,
41
- ids: ReadonlyArray<string>,
42
- ): void {
43
- try {
44
- const merged = loadSurfaced(conversationId);
45
- for (const id of ids) merged.add(id);
46
- mkdirSync(stateDir(), { recursive: true });
47
- writeFileSync(memoPath(conversationId), JSON.stringify([...merged]), "utf8");
48
- } catch {
49
- // Memo write failure must never break the hook.
50
- }
51
- }
52
-
53
- /**
54
- * Delete the memo file for a closed conversation. Called by the SessionEnd
55
- * hook so memo files don't accumulate forever. Returns true if a file was
56
- * removed, false otherwise — callers may want to log the outcome.
57
- */
58
- export function clearSurfaced(conversationId: string): boolean {
59
- try {
60
- const path = memoPath(conversationId);
61
- if (!existsSync(path)) return false;
62
- rmSync(path);
63
- return true;
64
- } catch {
65
- return false;
66
- }
67
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Renders the pointer block injected by the recall hook in live mode. Pure.
3
- * Pointer-only by design: ids + labels, no session content. The footer
4
- * names all four NLM MCP tools because the pointer block is the only
5
- * cross-runtime distribution surface for teaching the tool inventory —
6
- * fresh-install users never edit a prompt or settings file, so anything
7
- * we want the agent to know about the tool surface ships here.
8
- */
9
-
10
- export interface PointerHit {
11
- readonly id: string;
12
- readonly label: string;
13
- readonly startedAt: string;
14
- }
15
-
16
- export function formatPointerBlock(hits: ReadonlyArray<PointerHit>): string {
17
- if (hits.length === 0) return "";
18
- const lines = hits.map(
19
- (h) => `- ${h.id} · ${h.label} (${h.startedAt.slice(0, 10)})`,
20
- );
21
- return [
22
- "## Possibly-relevant prior sessions (nlm-memory)",
23
- ...lines,
24
- "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved).",
25
- ].join("\n");
26
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Selects which recall hits the hook surfaces. Pure — no I/O.
3
- *
4
- * Order of filtering: score threshold, then dedup against ids already
5
- * surfaced in this conversation, then the per-fire cap bounded by the
6
- * remaining per-conversation budget. Hits are assumed pre-ranked best-first.
7
- */
8
-
9
- export interface RecallHitInput {
10
- readonly id: string;
11
- readonly label: string;
12
- readonly startedAt: string;
13
- readonly matchScore: number;
14
- }
15
-
16
- export interface SelectParams {
17
- readonly hits: ReadonlyArray<RecallHitInput>;
18
- readonly surfaced: ReadonlySet<string>;
19
- readonly scoreThreshold: number;
20
- readonly perFireCap: number;
21
- readonly perConversationCap: number;
22
- }
23
-
24
- export function selectHits(params: SelectParams): ReadonlyArray<RecallHitInput> {
25
- const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
26
- const eligible = hits.filter(
27
- (h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id),
28
- );
29
- const budget = Math.max(0, perConversationCap - surfaced.size);
30
- const limit = Math.min(perFireCap, budget);
31
- return eligible.slice(0, limit);
32
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * Read assistant messages from a Claude Code transcript JSONL.
3
- *
4
- * Claude Code passes `transcript_path` in the Stop hook payload. Each line is
5
- * a JSON object; assistant turns have `type:"assistant"` and a `message`
6
- * object whose `content` is an array of blocks (`{type:"text", text:...}` for
7
- * prose; `{type:"tool_use", name, input}` for tool invocations).
8
- *
9
- * Stop-hook citation detection needs the union of ALL assistant turns in the
10
- * transcript, not just the last one: the model typically calls a tool, reads
11
- * the result on the next user turn (tool_result), then writes a prose summary
12
- * as a separate assistant turn. Scanning only the last turn misses the
13
- * tool_use entirely. `readAllAssistantTurns` returns every assistant turn in
14
- * order so the detector can fire across the whole conversation; cross-firing
15
- * dedup happens upstream via the per-conversation cited memo.
16
- *
17
- * Fail-quiet: a malformed file yields nulls/empty rather than throwing —
18
- * the Stop hook must never break on transcript I/O.
19
- */
20
-
21
- import { existsSync, readFileSync } from "node:fs";
22
-
23
- interface ContentBlock {
24
- readonly type: string;
25
- readonly text?: string;
26
- readonly name?: string;
27
- readonly input?: unknown;
28
- }
29
- interface AssistantMessage {
30
- readonly content?: ReadonlyArray<ContentBlock> | string;
31
- }
32
- interface TranscriptLine {
33
- readonly type?: string;
34
- readonly message?: AssistantMessage;
35
- }
36
-
37
- export interface ToolUseBlock {
38
- readonly name: string;
39
- readonly input: unknown;
40
- }
41
-
42
- export interface AssistantTurn {
43
- readonly text: string;
44
- readonly toolUses: ReadonlyArray<ToolUseBlock>;
45
- }
46
-
47
- const EMPTY_TURN: AssistantTurn = { text: "", toolUses: [] };
48
-
49
- function parseTurn(parsed: TranscriptLine): AssistantTurn | null {
50
- if (parsed.type !== "assistant" || !parsed.message) return null;
51
- const content = parsed.message.content;
52
- if (typeof content === "string") {
53
- return content ? { text: content, toolUses: [] } : null;
54
- }
55
- if (!Array.isArray(content)) return null;
56
- const textParts: string[] = [];
57
- const toolUses: ToolUseBlock[] = [];
58
- for (const block of content) {
59
- if (block.type === "text" && typeof block.text === "string") {
60
- textParts.push(block.text);
61
- } else if (block.type === "tool_use" && typeof block.name === "string") {
62
- toolUses.push({ name: block.name, input: block.input });
63
- }
64
- }
65
- if (textParts.length === 0 && toolUses.length === 0) return null;
66
- return { text: textParts.join("\n"), toolUses };
67
- }
68
-
69
- function readLines(transcriptPath: string): string[] | null {
70
- if (!transcriptPath || !existsSync(transcriptPath)) return null;
71
- try {
72
- return readFileSync(transcriptPath, "utf8").split("\n");
73
- } catch {
74
- return null;
75
- }
76
- }
77
-
78
- export function readAllAssistantTurns(
79
- transcriptPath: string,
80
- ): ReadonlyArray<AssistantTurn> {
81
- const lines = readLines(transcriptPath);
82
- if (!lines) return [];
83
- const turns: AssistantTurn[] = [];
84
- for (const raw of lines) {
85
- const line = raw.trim();
86
- if (!line) continue;
87
- let parsed: TranscriptLine;
88
- try {
89
- parsed = JSON.parse(line) as TranscriptLine;
90
- } catch {
91
- continue;
92
- }
93
- const turn = parseTurn(parsed);
94
- if (turn) turns.push(turn);
95
- }
96
- return turns;
97
- }
98
-
99
- export function readLastAssistantTurn(transcriptPath: string): AssistantTurn {
100
- const lines = readLines(transcriptPath);
101
- if (!lines) return EMPTY_TURN;
102
- for (let i = lines.length - 1; i >= 0; i--) {
103
- const line = lines[i]?.trim();
104
- if (!line) continue;
105
- let parsed: TranscriptLine;
106
- try {
107
- parsed = JSON.parse(line) as TranscriptLine;
108
- } catch {
109
- continue;
110
- }
111
- const turn = parseTurn(parsed);
112
- if (turn) return turn;
113
- }
114
- return EMPTY_TURN;
115
- }
116
-
117
- /** Back-compat shim for callers that only need prose. */
118
- export function readLastAssistantText(transcriptPath: string): string | null {
119
- const turn = readLastAssistantTurn(transcriptPath);
120
- return turn.text || null;
121
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * ingestSession — push a single externally-supplied session through the
3
- * normal classifier → embedder → store pipeline.
4
- *
5
- * Shared by the webhook endpoint (POST /api/ingest) and anything else
6
- * that wants to push without going through a TranscriptAdapter. Mirrors
7
- * the inner loop of ScanScheduler.runOnce but accepts a pre-built chunk.
8
- */
9
-
10
- import { createHash } from "node:crypto";
11
- import { extractFacts } from "@core/facts/extract-facts.js";
12
- import type { SqliteFactStore } from "@core/storage/sqlite-fact-store.js";
13
- import type { IngestRecord, SqliteSessionStore } from "@core/storage/sqlite-session-store.js";
14
- import type { LLMClient } from "@ports/llm-client.js";
15
-
16
- const BODY_CAP = 200_000;
17
- const CONFIDENCE_FLOOR = 0.3;
18
-
19
- export interface IngestInput {
20
- /** Optional — if omitted, derived from a hash of (runtime + startedAt + text). */
21
- readonly id?: string;
22
- readonly runtime: string;
23
- readonly runtimeSessionId?: string | null;
24
- readonly text: string;
25
- readonly startedAt?: string;
26
- readonly endedAt?: string | null;
27
- readonly transcriptPath?: string | null;
28
- /** Webhook id when the source is webhook-pushed; null for generic. */
29
- readonly sourceId?: number | null;
30
- }
31
-
32
- export interface IngestDeps {
33
- readonly classifier: LLMClient;
34
- readonly embedder: LLMClient;
35
- readonly store: SqliteSessionStore;
36
- readonly factStore?: SqliteFactStore;
37
- /** Optional logger — defaults to console.error. */
38
- readonly log?: (msg: string) => void;
39
- }
40
-
41
- export interface IngestResult {
42
- readonly id: string;
43
- readonly status: "ingested" | "low_confidence" | "classifier_failed";
44
- readonly latencyMs: number;
45
- readonly confidence?: number;
46
- readonly error?: string;
47
- }
48
-
49
- export function deriveSessionId(runtime: string, startedAt: string, text: string): string {
50
- const hash = createHash("sha256")
51
- .update(runtime)
52
- .update("|")
53
- .update(startedAt)
54
- .update("|")
55
- .update(text.slice(0, 4_000))
56
- .digest("hex")
57
- .slice(0, 16);
58
- return `webhook_${hash}`;
59
- }
60
-
61
- export async function ingestSession(input: IngestInput, deps: IngestDeps): Promise<IngestResult> {
62
- const startedAt = input.startedAt ?? new Date().toISOString();
63
- const id = input.id ?? deriveSessionId(input.runtime, startedAt, input.text);
64
- const log = deps.log ?? ((m: string) => console.error(m));
65
- const t0 = Date.now();
66
-
67
- let classification;
68
- try {
69
- classification = await deps.classifier.classify(input.text);
70
- } catch (e) {
71
- const error = e instanceof Error ? e.message : String(e);
72
- log(`[ingest] classifier failed for ${id}: ${error}`);
73
- return { id, status: "classifier_failed", latencyMs: Date.now() - t0, error };
74
- }
75
-
76
- if (classification.confidence < CONFIDENCE_FLOOR) {
77
- return {
78
- id,
79
- status: "low_confidence",
80
- latencyMs: Date.now() - t0,
81
- confidence: classification.confidence,
82
- };
83
- }
84
-
85
- const record: IngestRecord = {
86
- id,
87
- runtime: input.runtime,
88
- runtimeSessionId: input.runtimeSessionId ?? null,
89
- startedAt,
90
- endedAt: input.endedAt ?? null,
91
- durationMin: null,
92
- label: classification.label,
93
- summary: classification.summary,
94
- body: input.text.slice(0, BODY_CAP),
95
- status: "closed",
96
- transcriptKind: "webhook",
97
- transcriptPath: input.transcriptPath ?? null,
98
- transcriptOffset: null,
99
- transcriptLength: null,
100
- entities: classification.entities,
101
- decisions: classification.decisions,
102
- openQuestions: classification.open,
103
- };
104
-
105
- const factSink = deps.factStore
106
- ? { factStore: deps.factStore, facts: extractFacts(classification, id, startedAt) }
107
- : null;
108
-
109
- await deps.store.insertSession(record, deps.embedder, null, factSink);
110
- return { id, status: "ingested", latencyMs: Date.now() - t0, confidence: classification.confidence };
111
- }
@@ -1,100 +0,0 @@
1
- /**
2
- * Provider model discovery — runtime lookup of available models.
3
- *
4
- * Per-kind strategy:
5
- * - ollama: GET {baseUrl}/api/tags
6
- * - openai: GET {baseUrl}/models with Bearer key
7
- * - openrouter: GET {baseUrl}/models with Bearer key
8
- * - openai-compatible: GET {baseUrl}/models, key optional
9
- * - deepseek: hardcoded (no public list endpoint)
10
- * - anthropic: hardcoded (their /v1/models exists but
11
- * requires beta header + returns subsets;
12
- * a hardcoded list is more reliable)
13
- *
14
- * Returns a flat `string[]`. Errors throw — callers (the HTTP endpoint
15
- * and connection-test) catch and surface to the user.
16
- */
17
-
18
- import type { ProviderKind, ProviderRow } from "./provider-registry.js";
19
-
20
- export type FetchImpl = typeof fetch;
21
-
22
- const HARDCODED_MODELS: Partial<Record<ProviderKind, string[]>> = {
23
- deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
24
- anthropic: [
25
- "claude-opus-4-7",
26
- "claude-sonnet-4-6",
27
- "claude-haiku-4-5-20251001",
28
- ],
29
- };
30
-
31
- interface OllamaTagsResponse {
32
- readonly models?: ReadonlyArray<{ readonly name?: string }>;
33
- }
34
-
35
- interface OpenAIModelsResponse {
36
- readonly data?: ReadonlyArray<{ readonly id?: string }>;
37
- }
38
-
39
- export interface ListModelsOptions {
40
- readonly apiKey?: string | null;
41
- readonly fetchImpl?: FetchImpl;
42
- readonly timeoutMs?: number;
43
- }
44
-
45
- export async function listModels(
46
- provider: ProviderRow,
47
- opts: ListModelsOptions = {},
48
- ): Promise<string[]> {
49
- const hardcoded = HARDCODED_MODELS[provider.kind];
50
- if (hardcoded) return [...hardcoded];
51
-
52
- const fetchImpl = opts.fetchImpl ?? fetch;
53
- const timeoutMs = opts.timeoutMs ?? 10_000;
54
- const baseUrl = (provider.baseUrl ?? "").replace(/\/+$/, "");
55
- if (!baseUrl) throw new Error(`${provider.name}: baseUrl not configured`);
56
-
57
- if (provider.kind === "ollama") {
58
- return fetchOllamaModels(baseUrl, fetchImpl, timeoutMs);
59
- }
60
- return fetchOpenAIModels(baseUrl, opts.apiKey ?? null, fetchImpl, timeoutMs);
61
- }
62
-
63
- async function fetchOllamaModels(
64
- baseUrl: string,
65
- fetchImpl: FetchImpl,
66
- timeoutMs: number,
67
- ): Promise<string[]> {
68
- const controller = new AbortController();
69
- const timer = setTimeout(() => controller.abort(), timeoutMs);
70
- try {
71
- const res = await fetchImpl(`${baseUrl}/api/tags`, { signal: controller.signal });
72
- if (!res.ok) throw new Error(`Ollama returned ${res.status}`);
73
- const data = (await res.json()) as OllamaTagsResponse;
74
- const names = (data.models ?? []).map((m) => m.name).filter((n): n is string => typeof n === "string");
75
- return names.sort();
76
- } finally {
77
- clearTimeout(timer);
78
- }
79
- }
80
-
81
- async function fetchOpenAIModels(
82
- baseUrl: string,
83
- apiKey: string | null,
84
- fetchImpl: FetchImpl,
85
- timeoutMs: number,
86
- ): Promise<string[]> {
87
- const controller = new AbortController();
88
- const timer = setTimeout(() => controller.abort(), timeoutMs);
89
- try {
90
- const headers: Record<string, string> = {};
91
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
92
- const res = await fetchImpl(`${baseUrl}/models`, { signal: controller.signal, headers });
93
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
94
- const data = (await res.json()) as OpenAIModelsResponse;
95
- const ids = (data.data ?? []).map((m) => m.id).filter((s): s is string => typeof s === "string");
96
- return ids.sort();
97
- } finally {
98
- clearTimeout(timer);
99
- }
100
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * ProviderRegistry — CRUD over the `providers` table.
3
- *
4
- * One row per LLM endpoint the user has configured. The classifier reads
5
- * this at boot to pick a provider/model; the UI lets users add their own.
6
- *
7
- * API keys live in the `api_key` column today. Phase 2 (Tauri shell)
8
- * migrates them to the OS keychain; the API shape stays identical so this
9
- * module's consumers don't change.
10
- *
11
- * `redact()` strips secrets on the way out — every HTTP response sends
12
- * redacted rows, with the key only retrievable via getSecret() inside the
13
- * daemon process.
14
- */
15
-
16
- import type Database from "better-sqlite3";
17
-
18
- export type ProviderKind =
19
- | "deepseek"
20
- | "ollama"
21
- | "openai"
22
- | "anthropic"
23
- | "openrouter"
24
- | "openai-compatible";
25
-
26
- export interface ProviderRow {
27
- readonly id: number;
28
- readonly kind: ProviderKind;
29
- readonly name: string;
30
- readonly baseUrl: string | null;
31
- /** Always `null` on rows returned by `list()` / `get()`. Use `getSecret()`. */
32
- readonly apiKey: string | null;
33
- readonly hasApiKey: boolean;
34
- readonly defaultModel: string | null;
35
- readonly enabled: boolean;
36
- readonly createdAt: string;
37
- readonly updatedAt: string;
38
- }
39
-
40
- export interface ProviderInsert {
41
- readonly kind: ProviderKind;
42
- readonly name: string;
43
- readonly baseUrl?: string | null;
44
- readonly apiKey?: string | null;
45
- readonly defaultModel?: string | null;
46
- readonly enabled?: boolean;
47
- }
48
-
49
- export interface ProviderUpdate {
50
- readonly name?: string;
51
- readonly baseUrl?: string | null;
52
- readonly apiKey?: string | null;
53
- readonly defaultModel?: string | null;
54
- readonly enabled?: boolean;
55
- }
56
-
57
- interface ProviderDbRow {
58
- id: number;
59
- kind: string;
60
- name: string;
61
- base_url: string | null;
62
- api_key: string | null;
63
- default_model: string | null;
64
- enabled: number;
65
- created_at: string;
66
- updated_at: string;
67
- }
68
-
69
- function rowFromDb(r: ProviderDbRow, includeSecret: boolean): ProviderRow {
70
- return {
71
- id: r.id,
72
- kind: r.kind as ProviderKind,
73
- name: r.name,
74
- baseUrl: r.base_url,
75
- apiKey: includeSecret ? r.api_key : null,
76
- hasApiKey: r.api_key !== null && r.api_key.length > 0,
77
- defaultModel: r.default_model,
78
- enabled: r.enabled === 1,
79
- createdAt: r.created_at,
80
- updatedAt: r.updated_at,
81
- };
82
- }
83
-
84
- const DEFAULT_BASE_URLS: Record<ProviderKind, string | null> = {
85
- deepseek: "https://api.deepseek.com",
86
- ollama: "http://localhost:11434",
87
- openai: "https://api.openai.com/v1",
88
- anthropic: "https://api.anthropic.com",
89
- openrouter: "https://openrouter.ai/api/v1",
90
- "openai-compatible": null,
91
- };
92
-
93
- const DEFAULT_MODELS: Record<ProviderKind, string | null> = {
94
- deepseek: "deepseek-v4-flash",
95
- ollama: "phi4-mini:latest",
96
- openai: "gpt-4o-mini",
97
- anthropic: "claude-haiku-4-5-20251001",
98
- openrouter: "anthropic/claude-haiku-4-5",
99
- "openai-compatible": null,
100
- };
101
-
102
- export class ProviderRegistry {
103
- constructor(private readonly db: Database.Database) {}
104
-
105
- list(): ProviderRow[] {
106
- const rows = this.db.prepare<[], ProviderDbRow>(
107
- `SELECT * FROM providers ORDER BY id ASC`,
108
- ).all();
109
- return rows.map((r) => rowFromDb(r, false));
110
- }
111
-
112
- get(id: number): ProviderRow | null {
113
- const row = this.db.prepare<[number], ProviderDbRow>(
114
- `SELECT * FROM providers WHERE id = ?`,
115
- ).get(id);
116
- return row ? rowFromDb(row, false) : null;
117
- }
118
-
119
- getByName(name: string): ProviderRow | null {
120
- const row = this.db.prepare<[string], ProviderDbRow>(
121
- `SELECT * FROM providers WHERE name = ?`,
122
- ).get(name);
123
- return row ? rowFromDb(row, false) : null;
124
- }
125
-
126
- /** Returns the secret. Use only inside the daemon — never echo to HTTP. */
127
- getSecret(id: number): string | null {
128
- const row = this.db.prepare<[number], ProviderDbRow>(
129
- `SELECT * FROM providers WHERE id = ?`,
130
- ).get(id);
131
- return row?.api_key ?? null;
132
- }
133
-
134
- insert(input: ProviderInsert): ProviderRow {
135
- const baseUrl = input.baseUrl ?? DEFAULT_BASE_URLS[input.kind];
136
- const defaultModel = input.defaultModel ?? DEFAULT_MODELS[input.kind];
137
- const result = this.db.prepare(`
138
- INSERT INTO providers (kind, name, base_url, api_key, default_model, enabled)
139
- VALUES (@kind, @name, @base_url, @api_key, @default_model, @enabled)
140
- `).run({
141
- kind: input.kind,
142
- name: input.name,
143
- base_url: baseUrl ?? null,
144
- api_key: input.apiKey ?? null,
145
- default_model: defaultModel ?? null,
146
- enabled: input.enabled === false ? 0 : 1,
147
- });
148
- const id = Number(result.lastInsertRowid);
149
- const row = this.get(id);
150
- if (!row) throw new Error(`ProviderRegistry.insert: row ${id} not found after insert`);
151
- return row;
152
- }
153
-
154
- update(id: number, patch: ProviderUpdate): ProviderRow | null {
155
- const fields: string[] = [];
156
- const params: Record<string, unknown> = { id };
157
- if (patch.name !== undefined) { fields.push("name = @name"); params["name"] = patch.name; }
158
- if (patch.baseUrl !== undefined) { fields.push("base_url = @url"); params["url"] = patch.baseUrl; }
159
- if (patch.apiKey !== undefined) { fields.push("api_key = @key"); params["key"] = patch.apiKey; }
160
- if (patch.defaultModel !== undefined) { fields.push("default_model = @m"); params["m"] = patch.defaultModel; }
161
- if (patch.enabled !== undefined) { fields.push("enabled = @en"); params["en"] = patch.enabled ? 1 : 0; }
162
- if (fields.length === 0) return this.get(id);
163
- fields.push("updated_at = datetime('now')");
164
- this.db.prepare(`UPDATE providers SET ${fields.join(", ")} WHERE id = @id`).run(params);
165
- return this.get(id);
166
- }
167
-
168
- delete(id: number): boolean {
169
- const result = this.db.prepare(`DELETE FROM providers WHERE id = ?`).run(id);
170
- return result.changes > 0;
171
- }
172
-
173
- /**
174
- * Seed defaults on an empty registry. Bridges from the legacy env-var
175
- * setup: if DEEPSEEK_API_KEY is present, the DeepSeek row carries it
176
- * forward; Ollama is always seeded since it needs no key.
177
- */
178
- seedDefaults(): void {
179
- const count = this.db.prepare<[], { c: number }>(`SELECT COUNT(*) AS c FROM providers`).get();
180
- if ((count?.c ?? 0) > 0) return;
181
-
182
- this.insert({
183
- kind: "ollama",
184
- name: "Ollama (local)",
185
- baseUrl: process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434",
186
- });
187
-
188
- const deepseekKey = process.env["DEEPSEEK_API_KEY"];
189
- this.insert({
190
- kind: "deepseek",
191
- name: "DeepSeek",
192
- apiKey: deepseekKey ?? null,
193
- enabled: Boolean(deepseekKey),
194
- });
195
- }
196
- }