nlm-memory 0.5.0 → 0.5.2

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 (257) hide show
  1. package/README.md +89 -34
  2. package/dist/cli/digest.d.ts +20 -0
  3. package/dist/cli/digest.js +142 -0
  4. package/dist/cli/digest.js.map +1 -0
  5. package/dist/cli/nlm.d.ts +1 -0
  6. package/dist/cli/nlm.js +25 -1
  7. package/dist/cli/nlm.js.map +1 -1
  8. package/dist/core/digest/compose.d.ts +38 -0
  9. package/dist/core/digest/compose.js +93 -0
  10. package/dist/core/digest/compose.js.map +1 -0
  11. package/dist/core/digest/hook-liveness.d.ts +32 -0
  12. package/dist/core/digest/hook-liveness.js +54 -0
  13. package/dist/core/digest/hook-liveness.js.map +1 -0
  14. package/dist/http/app.js +2 -1
  15. package/dist/http/app.js.map +1 -1
  16. package/dist/mcp/server.js +20 -1
  17. package/dist/mcp/server.js.map +1 -1
  18. package/dist/ui/assets/{index-C8cpwbYJ.css → index-Beo8psd-.css} +1 -1
  19. package/dist/ui/assets/{index-CB50QnL-.js → index-CSPTTeeM.js} +8 -8
  20. package/dist/ui/index.html +2 -2
  21. package/package.json +26 -1
  22. package/.agents/plugins/marketplace.json +0 -20
  23. package/.github/workflows/ci.yml +0 -30
  24. package/docs/methodology/re-derivation-rate.md +0 -112
  25. package/docs/methodology/useful-hit-rate.md +0 -79
  26. package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
  27. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
  28. package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
  29. package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
  30. package/docs/plans/desktop-product.md +0 -69
  31. package/docs/plans/factstore-design.md +0 -236
  32. package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1575
  33. package/logs/CHANGELOG/CHANGELOG.md +0 -209
  34. package/migrations/000_initial_schema.sql +0 -174
  35. package/migrations/001_entity_type_rename.sql +0 -17
  36. package/migrations/002_adapter_state_extend.sql +0 -12
  37. package/migrations/003_session_embeddings.sql +0 -11
  38. package/migrations/004_facts.sql +0 -46
  39. package/migrations/005_sources.sql +0 -31
  40. package/migrations/006_providers.sql +0 -33
  41. package/migrations/007_source_tokens.sql +0 -17
  42. package/migrations/008_fts_rebuild.sql +0 -9
  43. package/migrations/009_session_embedding_chunks.sql +0 -46
  44. package/migrations/010_sources_opencode.sql +0 -30
  45. package/migrations/011_sources_hermes_agent.sql +0 -30
  46. package/migrations/012_sources_aider.sql +0 -30
  47. package/migrations/013_adapter_state_failure_count.sql +0 -12
  48. package/migrations/014_sources_cursor.sql +0 -30
  49. package/migrations/015_sources_windsurf.sql +0 -30
  50. package/plugin-hermes-agent/README.md +0 -49
  51. package/plugin-hermes-agent/__init__.py +0 -75
  52. package/plugin-hermes-agent/plugin.yaml +0 -15
  53. package/scripts/backfill-citations.mjs +0 -0
  54. package/scripts/build-codex-plugin.mjs +0 -61
  55. package/scripts/deepseek-probe.mjs +0 -67
  56. package/scripts/extract-triples.mjs +0 -207
  57. package/scripts/longmemeval/embedding-cache.ts +0 -77
  58. package/scripts/longmemeval/fetch-dataset.sh +0 -25
  59. package/scripts/longmemeval/run-harness.ts +0 -315
  60. package/scripts/longmemeval/scorer.ts +0 -99
  61. package/scripts/longmemeval/tsconfig.json +0 -9
  62. package/scripts/longmemeval/types.ts +0 -35
  63. package/scripts/nlm-daily-digest.py +0 -239
  64. package/scripts/nlm-daily-digest.sh +0 -28
  65. package/src/cli/classify-parity.ts +0 -257
  66. package/src/cli/launchctl-helpers.ts +0 -49
  67. package/src/cli/nlm.ts +0 -1078
  68. package/src/core/actions/actions-log.ts +0 -118
  69. package/src/core/actions/overlay.ts +0 -117
  70. package/src/core/adapters/aider.ts +0 -205
  71. package/src/core/adapters/claude-code.ts +0 -293
  72. package/src/core/adapters/common.ts +0 -54
  73. package/src/core/adapters/cursor.ts +0 -486
  74. package/src/core/adapters/from-source.ts +0 -67
  75. package/src/core/adapters/hermes-agent.ts +0 -240
  76. package/src/core/adapters/hermes.ts +0 -277
  77. package/src/core/adapters/jsonl-generic.ts +0 -208
  78. package/src/core/adapters/opencode.ts +0 -281
  79. package/src/core/adapters/pi.ts +0 -264
  80. package/src/core/adapters/windsurf.ts +0 -386
  81. package/src/core/classifier/prompt.ts +0 -200
  82. package/src/core/dataset/build-dataset.ts +0 -463
  83. package/src/core/embedding/chunk-body.ts +0 -76
  84. package/src/core/embedding/embed-backfill.ts +0 -210
  85. package/src/core/embedding/embed-normalize.ts +0 -135
  86. package/src/core/facts/backfill-facts.ts +0 -254
  87. package/src/core/facts/extract-facts.ts +0 -50
  88. package/src/core/hook/citation-detect.ts +0 -124
  89. package/src/core/hook/cite-memo.ts +0 -68
  90. package/src/core/hook/claude-settings.ts +0 -187
  91. package/src/core/hook/gate.ts +0 -25
  92. package/src/core/hook/hook-log.ts +0 -41
  93. package/src/core/hook/memo-sweep.ts +0 -164
  94. package/src/core/hook/memo.ts +0 -67
  95. package/src/core/hook/pointer-block.ts +0 -26
  96. package/src/core/hook/select.ts +0 -32
  97. package/src/core/hook/transcript.ts +0 -121
  98. package/src/core/ingest/ingest-session.ts +0 -111
  99. package/src/core/providers/provider-models.ts +0 -100
  100. package/src/core/providers/provider-registry.ts +0 -196
  101. package/src/core/recall/citation-log.ts +0 -108
  102. package/src/core/recall/filter.ts +0 -27
  103. package/src/core/recall/index.ts +0 -6
  104. package/src/core/recall/match-fields.ts +0 -40
  105. package/src/core/recall/query-log.ts +0 -149
  106. package/src/core/recall/query-shape.ts +0 -66
  107. package/src/core/recall/recall-service.ts +0 -320
  108. package/src/core/recall/recent-log.ts +0 -59
  109. package/src/core/recall/tokenize.ts +0 -18
  110. package/src/core/recall/useful-scan.ts +0 -336
  111. package/src/core/recall-facts/fact-query-log.ts +0 -150
  112. package/src/core/recall-facts/fact-recall-service.ts +0 -327
  113. package/src/core/scheduler/scan-once.ts +0 -142
  114. package/src/core/scheduler/scheduler.ts +0 -225
  115. package/src/core/sources/source-registry.ts +0 -278
  116. package/src/core/storage/db-restore.ts +0 -133
  117. package/src/core/storage/live-status.ts +0 -45
  118. package/src/core/storage/migrate.ts +0 -72
  119. package/src/core/storage/sqlite-fact-store.ts +0 -304
  120. package/src/core/storage/sqlite-session-store.ts +0 -810
  121. package/src/hook/hook-auth.ts +0 -18
  122. package/src/hook/prompt-recall-hook.ts +0 -180
  123. package/src/hook/session-end-hook.ts +0 -81
  124. package/src/hook/session-start-hook.ts +0 -168
  125. package/src/hook/stop-hook.ts +0 -239
  126. package/src/http/app.ts +0 -1215
  127. package/src/install/claude-code.ts +0 -128
  128. package/src/install/codex.ts +0 -367
  129. package/src/install/cursor.ts +0 -68
  130. package/src/install/hermes-agent.ts +0 -76
  131. package/src/install/hermes.ts +0 -78
  132. package/src/install/nlm-dir-perms.ts +0 -55
  133. package/src/install/ollama.ts +0 -284
  134. package/src/install/setup.ts +0 -489
  135. package/src/install/windsurf.ts +0 -68
  136. package/src/llm/classifier-box.ts +0 -64
  137. package/src/llm/deepseek-client.ts +0 -150
  138. package/src/llm/env-autoload.ts +0 -55
  139. package/src/llm/ollama-client.ts +0 -189
  140. package/src/mcp/server.ts +0 -534
  141. package/src/ports/fact-store.ts +0 -102
  142. package/src/ports/llm-client.ts +0 -52
  143. package/src/ports/logger.ts +0 -16
  144. package/src/ports/session-store.ts +0 -45
  145. package/src/ports/transcript-adapter.ts +0 -55
  146. package/src/shared/types.ts +0 -149
  147. package/src/ui/App.tsx +0 -58
  148. package/src/ui/components/PromoteOpenButton.tsx +0 -65
  149. package/src/ui/components/SessionDrawer.tsx +0 -199
  150. package/src/ui/components/SideNav.tsx +0 -162
  151. package/src/ui/components/Skeleton.tsx +0 -107
  152. package/src/ui/index.html +0 -13
  153. package/src/ui/lib/actions.ts +0 -30
  154. package/src/ui/lib/api.ts +0 -92
  155. package/src/ui/lib/dataset.ts +0 -141
  156. package/src/ui/lib/registries.ts +0 -155
  157. package/src/ui/lib/view-settings.ts +0 -41
  158. package/src/ui/main.tsx +0 -15
  159. package/src/ui/pages/Live.tsx +0 -229
  160. package/src/ui/pages/Pulse.tsx +0 -415
  161. package/src/ui/pages/Recall.tsx +0 -190
  162. package/src/ui/pages/River.tsx +0 -354
  163. package/src/ui/pages/Search.tsx +0 -386
  164. package/src/ui/pages/Stub.tsx +0 -9
  165. package/src/ui/pages/Thread.tsx +0 -473
  166. package/src/ui/pages/settings/Classifier.tsx +0 -227
  167. package/src/ui/pages/settings/Data.tsx +0 -190
  168. package/src/ui/pages/settings/Index.tsx +0 -65
  169. package/src/ui/pages/settings/Labels.tsx +0 -224
  170. package/src/ui/pages/settings/Providers.tsx +0 -305
  171. package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
  172. package/src/ui/pages/settings/Sources.tsx +0 -326
  173. package/src/ui/pages/settings/Views.tsx +0 -96
  174. package/src/ui/styles.css +0 -1890
  175. package/src/ui/tsconfig.json +0 -21
  176. package/src/ui/vite.config.ts +0 -19
  177. package/tests/fixtures/claude_code/short_session.jsonl +0 -2
  178. package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
  179. package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
  180. package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
  181. package/tests/fixtures/facts.ts +0 -17
  182. package/tests/fixtures/golden-corpus.ts +0 -85
  183. package/tests/fixtures/hermes/paired_request_dump.json +0 -24
  184. package/tests/fixtures/hermes/paired_session.json +0 -23
  185. package/tests/fixtures/hermes/request_dump.json +0 -28
  186. package/tests/fixtures/hermes/session_iso.json +0 -38
  187. package/tests/fixtures/hermes/session_unix.json +0 -38
  188. package/tests/fixtures/hermes/system_only.json +0 -18
  189. package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
  190. package/tests/fixtures/pi/short-successful.jsonl +0 -5
  191. package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
  192. package/tests/fixtures/sessions.ts +0 -22
  193. package/tests/integration/backfill-facts.test.ts +0 -362
  194. package/tests/integration/citation-explicit.test.ts +0 -111
  195. package/tests/integration/cite-event.test.ts +0 -169
  196. package/tests/integration/cite-memo.test.ts +0 -87
  197. package/tests/integration/db-restore.test.ts +0 -153
  198. package/tests/integration/embed-backfill.test.ts +0 -176
  199. package/tests/integration/fact-supersedence.test.ts +0 -313
  200. package/tests/integration/fts-index.test.ts +0 -60
  201. package/tests/integration/getbyids-sqlite.test.ts +0 -100
  202. package/tests/integration/hermes-agent-hooks.test.ts +0 -248
  203. package/tests/integration/hook-claude-settings.test.ts +0 -218
  204. package/tests/integration/hook-log.test.ts +0 -54
  205. package/tests/integration/hook-memo.test.ts +0 -68
  206. package/tests/integration/hook-pre-compact.test.ts +0 -105
  207. package/tests/integration/hook-subagent-start.test.ts +0 -102
  208. package/tests/integration/http.test.ts +0 -401
  209. package/tests/integration/keyword-search-fts.test.ts +0 -66
  210. package/tests/integration/mcp-recall-logging.test.ts +0 -88
  211. package/tests/integration/mcp.test.ts +0 -260
  212. package/tests/integration/memo-sweep.test.ts +0 -91
  213. package/tests/integration/prompt-recall-hook.test.ts +0 -88
  214. package/tests/integration/provider-registry.test.ts +0 -107
  215. package/tests/integration/recall-golden.test.ts +0 -59
  216. package/tests/integration/recall-sqlite.test.ts +0 -169
  217. package/tests/integration/scheduler.test.ts +0 -391
  218. package/tests/integration/session-end-hook.test.ts +0 -48
  219. package/tests/integration/session-start-hook.test.ts +0 -126
  220. package/tests/integration/source-registry.test.ts +0 -122
  221. package/tests/integration/sqlite-fact-store.test.ts +0 -346
  222. package/tests/integration/stop-hook.test.ts +0 -560
  223. package/tests/integration/wal-checkpoint.test.ts +0 -49
  224. package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
  225. package/tests/unit/core/adapters/aider.test.ts +0 -230
  226. package/tests/unit/core/adapters/claude-code.test.ts +0 -118
  227. package/tests/unit/core/adapters/cursor.test.ts +0 -485
  228. package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
  229. package/tests/unit/core/adapters/hermes.test.ts +0 -81
  230. package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
  231. package/tests/unit/core/adapters/opencode.test.ts +0 -354
  232. package/tests/unit/core/adapters/pi.test.ts +0 -110
  233. package/tests/unit/core/adapters/windsurf.test.ts +0 -416
  234. package/tests/unit/core/classifier/prompt.test.ts +0 -126
  235. package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
  236. package/tests/unit/core/facts/extract-facts.test.ts +0 -117
  237. package/tests/unit/core/filter.test.ts +0 -40
  238. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
  239. package/tests/unit/core/hook/citation-detect.test.ts +0 -124
  240. package/tests/unit/core/hook/gate.test.ts +0 -29
  241. package/tests/unit/core/hook/pointer-block.test.ts +0 -22
  242. package/tests/unit/core/hook/select.test.ts +0 -66
  243. package/tests/unit/core/match-fields.test.ts +0 -39
  244. package/tests/unit/core/mcp-cite-session.test.ts +0 -51
  245. package/tests/unit/core/providers/provider-models.test.ts +0 -101
  246. package/tests/unit/core/query-shape.test.ts +0 -92
  247. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
  248. package/tests/unit/core/recall-service.test.ts +0 -200
  249. package/tests/unit/core/storage/live-status.test.ts +0 -54
  250. package/tests/unit/core/tokenize.test.ts +0 -32
  251. package/tests/unit/core/useful-scan.test.ts +0 -537
  252. package/tests/unit/llm/embed.test.ts +0 -93
  253. package/tests/unit/llm/ollama-client.test.ts +0 -124
  254. package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
  255. package/tsconfig.json +0 -31
  256. package/tsconfig.test.json +0 -11
  257. 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
- }