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,278 +0,0 @@
1
- /**
2
- * SourceRegistry — CRUD over the `sources` table.
3
- *
4
- * A "source" is any transcript origin the daemon scans (Claude Code's
5
- * projects dir, Hermes's sessions dir, pi.dev, a user-defined JSONL
6
- * directory, or a webhook).
7
- *
8
- * The three legacy adapters (claude-code, hermes, pi) seed as preset rows
9
- * pointing at fixed `path_or_url` values. The generic JSONL adapter and
10
- * webhook ingest piggy-back on this same table — the scheduler chooses
11
- * which adapter to dispatch by reading `kind`.
12
- *
13
- * See docs/plans/desktop-product.md (Phase 0).
14
- */
15
-
16
- import { randomBytes } from "node:crypto";
17
- import { existsSync } from "node:fs";
18
- import { homedir } from "node:os";
19
- import { join } from "node:path";
20
- import type Database from "better-sqlite3";
21
- import { defaultHistoryFile as defaultAiderHistoryFile } from "../adapters/aider.js";
22
- import { defaultDbPath as defaultCursorDbPath } from "../adapters/cursor.js";
23
- import { defaultDbPath as defaultHermesAgentDbPath } from "../adapters/hermes-agent.js";
24
- import { defaultDbPath as defaultOpenCodeDbPath } from "../adapters/opencode.js";
25
- import { defaultUserDir as defaultWindsurfUserDir } from "../adapters/windsurf.js";
26
-
27
- export type SourceKind = "claude-code" | "hermes" | "hermes-agent" | "aider" | "cursor" | "windsurf" | "opencode" | "pi" | "jsonl-generic" | "webhook";
28
-
29
- export interface SourceRow {
30
- readonly id: number;
31
- readonly kind: SourceKind;
32
- readonly name: string;
33
- readonly pathOrUrl: string | null;
34
- readonly runtimeLabel: string;
35
- readonly parseConfig: Record<string, unknown>;
36
- readonly enabled: boolean;
37
- /** Only populated on the response from `insert()` for webhook sources.
38
- * Always `null` from `list()` / `get()`. Use `getToken()` inside the daemon. */
39
- readonly token: string | null;
40
- readonly hasToken: boolean;
41
- readonly createdAt: string;
42
- readonly updatedAt: string;
43
- }
44
-
45
- export interface SourceInsert {
46
- readonly kind: SourceKind;
47
- readonly name: string;
48
- readonly pathOrUrl?: string | null;
49
- readonly runtimeLabel: string;
50
- readonly parseConfig?: Record<string, unknown>;
51
- readonly enabled?: boolean;
52
- }
53
-
54
- export interface SourceUpdate {
55
- readonly name?: string;
56
- readonly pathOrUrl?: string | null;
57
- readonly runtimeLabel?: string;
58
- readonly parseConfig?: Record<string, unknown>;
59
- readonly enabled?: boolean;
60
- }
61
-
62
- interface SourceDbRow {
63
- id: number;
64
- kind: string;
65
- name: string;
66
- path_or_url: string | null;
67
- runtime_label: string;
68
- parse_config: string;
69
- enabled: number;
70
- token: string | null;
71
- created_at: string;
72
- updated_at: string;
73
- }
74
-
75
- function rowFromDb(r: SourceDbRow, revealedToken: string | null = null): SourceRow {
76
- let parsed: Record<string, unknown> = {};
77
- try {
78
- parsed = r.parse_config ? (JSON.parse(r.parse_config) as Record<string, unknown>) : {};
79
- } catch {
80
- parsed = {};
81
- }
82
- return {
83
- id: r.id,
84
- kind: r.kind as SourceKind,
85
- name: r.name,
86
- pathOrUrl: r.path_or_url,
87
- runtimeLabel: r.runtime_label,
88
- parseConfig: parsed,
89
- enabled: r.enabled === 1,
90
- token: revealedToken,
91
- hasToken: r.token !== null && r.token.length > 0,
92
- createdAt: r.created_at,
93
- updatedAt: r.updated_at,
94
- };
95
- }
96
-
97
- function mintToken(): string {
98
- return `nlm_${randomBytes(24).toString("hex")}`;
99
- }
100
-
101
- export class SourceRegistry {
102
- constructor(private readonly db: Database.Database) {}
103
-
104
- list(): SourceRow[] {
105
- const rows = this.db.prepare<[], SourceDbRow>(
106
- `SELECT * FROM sources ORDER BY id ASC`,
107
- ).all();
108
- return rows.map((r) => rowFromDb(r));
109
- }
110
-
111
- get(id: number): SourceRow | null {
112
- const row = this.db.prepare<[number], SourceDbRow>(
113
- `SELECT * FROM sources WHERE id = ?`,
114
- ).get(id);
115
- return row ? rowFromDb(row) : null;
116
- }
117
-
118
- getByName(name: string): SourceRow | null {
119
- const row = this.db.prepare<[string], SourceDbRow>(
120
- `SELECT * FROM sources WHERE name = ?`,
121
- ).get(name);
122
- return row ? rowFromDb(row) : null;
123
- }
124
-
125
- insert(input: SourceInsert): SourceRow {
126
- const token = input.kind === "webhook" ? mintToken() : null;
127
- const stmt = this.db.prepare(`
128
- INSERT INTO sources (kind, name, path_or_url, runtime_label, parse_config, enabled, token)
129
- VALUES (@kind, @name, @path_or_url, @runtime_label, @parse_config, @enabled, @token)
130
- `);
131
- const result = stmt.run({
132
- kind: input.kind,
133
- name: input.name,
134
- path_or_url: input.pathOrUrl ?? null,
135
- runtime_label: input.runtimeLabel,
136
- parse_config: JSON.stringify(input.parseConfig ?? {}),
137
- enabled: input.enabled === false ? 0 : 1,
138
- token,
139
- });
140
- const id = Number(result.lastInsertRowid);
141
- const dbRow = this.db.prepare<[number], SourceDbRow>(
142
- `SELECT * FROM sources WHERE id = ?`,
143
- ).get(id);
144
- if (!dbRow) throw new Error(`SourceRegistry.insert: row ${id} not found after insert`);
145
- // Reveal the token on the insert response only — this is the user's
146
- // one chance to copy it. Subsequent list/get redact.
147
- return rowFromDb(dbRow, token);
148
- }
149
-
150
- /** Daemon-internal: resolve a bearer token to its owning source. */
151
- findByToken(token: string): SourceRow | null {
152
- if (!token) return null;
153
- const row = this.db.prepare<[string], SourceDbRow>(
154
- `SELECT * FROM sources WHERE token = ?`,
155
- ).get(token);
156
- return row ? rowFromDb(row) : null;
157
- }
158
-
159
- /** Daemon-internal: returns the raw token. Never echo to HTTP responses. */
160
- getToken(id: number): string | null {
161
- const row = this.db.prepare<[number], SourceDbRow>(
162
- `SELECT token FROM sources WHERE id = ?`,
163
- ).get(id);
164
- return row?.token ?? null;
165
- }
166
-
167
- /** Mint a fresh token, invalidating any previous one. */
168
- regenerateToken(id: number): string | null {
169
- const current = this.get(id);
170
- if (!current || current.kind !== "webhook") return null;
171
- const token = mintToken();
172
- this.db.prepare(`UPDATE sources SET token = ?, updated_at = datetime('now') WHERE id = ?`)
173
- .run(token, id);
174
- return token;
175
- }
176
-
177
- update(id: number, patch: SourceUpdate): SourceRow | null {
178
- const fields: string[] = [];
179
- const params: Record<string, unknown> = { id };
180
- if (patch.name !== undefined) { fields.push("name = @name"); params["name"] = patch.name; }
181
- if (patch.pathOrUrl !== undefined) { fields.push("path_or_url = @path"); params["path"] = patch.pathOrUrl; }
182
- if (patch.runtimeLabel !== undefined) { fields.push("runtime_label = @rt"); params["rt"] = patch.runtimeLabel; }
183
- if (patch.parseConfig !== undefined) { fields.push("parse_config = @cfg"); params["cfg"] = JSON.stringify(patch.parseConfig); }
184
- if (patch.enabled !== undefined) { fields.push("enabled = @en"); params["en"] = patch.enabled ? 1 : 0; }
185
- if (fields.length === 0) return this.get(id);
186
- fields.push("updated_at = datetime('now')");
187
- this.db.prepare(`UPDATE sources SET ${fields.join(", ")} WHERE id = @id`).run(params);
188
- return this.get(id);
189
- }
190
-
191
- delete(id: number): boolean {
192
- const result = this.db.prepare(`DELETE FROM sources WHERE id = ?`).run(id);
193
- return result.changes > 0;
194
- }
195
-
196
- /**
197
- * Seed the three legacy adapter presets on first boot of an empty
198
- * registry. Subsequent boots are no-ops. Respects per-runtime env
199
- * overrides so existing installs don't lose their custom paths.
200
- */
201
- seedDefaults(): void {
202
- const count = this.db.prepare<[], { c: number }>(`SELECT COUNT(*) AS c FROM sources`).get();
203
- if ((count?.c ?? 0) > 0) return;
204
-
205
- const claudePath = process.env["NLM_CLAUDE_PROJECTS_PATH"]
206
- ?? join(homedir(), ".claude", "projects");
207
- const hermesPath = process.env["NLM_HERMES_SESSIONS_PATH"]
208
- ?? join(homedir(), ".hermes", "sessions");
209
- const piPath = process.env["PI_SESSIONS_PATH"]
210
- ?? join(homedir(), ".pi", "agent", "sessions");
211
-
212
- const openCodeDbPath = defaultOpenCodeDbPath();
213
- const hermesAgentDbPath = defaultHermesAgentDbPath();
214
- const aiderHistoryFile = defaultAiderHistoryFile();
215
- const cursorDbPath = defaultCursorDbPath();
216
- const windsurfUserDir = defaultWindsurfUserDir();
217
-
218
- const presets: SourceInsert[] = [
219
- {
220
- kind: "claude-code",
221
- name: "Claude Code",
222
- pathOrUrl: claudePath,
223
- runtimeLabel: "claude-code/1.0",
224
- enabled: existsSync(claudePath),
225
- },
226
- {
227
- kind: "hermes",
228
- name: "Hermes",
229
- pathOrUrl: hermesPath,
230
- runtimeLabel: "hermes/1.0",
231
- enabled: existsSync(hermesPath),
232
- },
233
- {
234
- kind: "hermes-agent",
235
- name: "Hermes Agent",
236
- pathOrUrl: hermesAgentDbPath,
237
- runtimeLabel: "hermes-agent/1.0",
238
- enabled: existsSync(hermesAgentDbPath),
239
- },
240
- {
241
- kind: "aider",
242
- name: "Aider",
243
- pathOrUrl: aiderHistoryFile,
244
- runtimeLabel: "aider/1.0",
245
- enabled: existsSync(aiderHistoryFile),
246
- },
247
- {
248
- kind: "cursor",
249
- name: "Cursor",
250
- pathOrUrl: cursorDbPath,
251
- runtimeLabel: "cursor/1.0",
252
- enabled: existsSync(cursorDbPath),
253
- },
254
- {
255
- kind: "windsurf",
256
- name: "Windsurf",
257
- pathOrUrl: windsurfUserDir,
258
- runtimeLabel: "windsurf/1.0",
259
- enabled: existsSync(windsurfUserDir),
260
- },
261
- {
262
- kind: "opencode",
263
- name: "OpenCode",
264
- pathOrUrl: openCodeDbPath,
265
- runtimeLabel: "opencode/1.0",
266
- enabled: existsSync(openCodeDbPath),
267
- },
268
- {
269
- kind: "pi",
270
- name: "pi.dev",
271
- pathOrUrl: piPath,
272
- runtimeLabel: "pi/1.0",
273
- enabled: existsSync(piPath),
274
- },
275
- ];
276
- for (const p of presets) this.insert(p);
277
- }
278
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * Backup + restore for the canonical SQLite store.
3
- *
4
- * Backup is live-safe: `VACUUM INTO` takes a read lock and writes a clean,
5
- * defragmented, single-file snapshot — no WAL sidecars, consistent even
6
- * while the daemon is ingesting.
7
- *
8
- * Restore cannot swap a file the daemon holds open. Instead the uploaded
9
- * DB is validated and parked at `<dbPath>.restore-pending`; the next daemon
10
- * boot calls `applyPendingRestore()` before opening the store, moving the
11
- * current DB aside to `<dbPath>.pre-restore-<ts>` and promoting the pending
12
- * file. The desktop shell turns "restart required" into one click.
13
- */
14
-
15
- import Database from "better-sqlite3";
16
- import { existsSync, renameSync, rmSync, statSync } from "node:fs";
17
- import { dirname, join } from "node:path";
18
-
19
- export const PENDING_SUFFIX = ".restore-pending";
20
-
21
- export interface RestoreValidation {
22
- ok: boolean;
23
- error?: string;
24
- sessions?: number;
25
- schemaVersion?: number;
26
- }
27
-
28
- /**
29
- * Validate that `filePath` is a usable canonical store: passes integrity
30
- * check and carries the `sessions` + `schema_migrations` tables. Opened
31
- * read-only and without the sqlite-vec extension — we never touch the
32
- * vec virtual tables here, so the extension isn't needed.
33
- */
34
- export function validateRestoreCandidate(filePath: string): RestoreValidation {
35
- let db: Database.Database | null = null;
36
- try {
37
- db = new Database(filePath, { readonly: true, fileMustExist: true });
38
- const integrity = db.pragma("integrity_check", { simple: true });
39
- if (integrity !== "ok") {
40
- return { ok: false, error: `integrity check failed: ${String(integrity)}` };
41
- }
42
- const tables = db
43
- .prepare<[], { name: string }>(
44
- "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('sessions','schema_migrations')",
45
- )
46
- .all()
47
- .map((r) => r.name);
48
- if (!tables.includes("sessions") || !tables.includes("schema_migrations")) {
49
- return { ok: false, error: "not an nlm-memory database (missing sessions/schema_migrations)" };
50
- }
51
- const sessions = db
52
- .prepare<[], { n: number }>("SELECT COUNT(*) AS n FROM sessions")
53
- .get();
54
- const version = db
55
- .prepare<[], { v: number | null }>("SELECT MAX(version) AS v FROM schema_migrations")
56
- .get();
57
- return {
58
- ok: true,
59
- sessions: sessions?.n ?? 0,
60
- schemaVersion: version?.v ?? 0,
61
- };
62
- } catch (e) {
63
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
64
- } finally {
65
- db?.close();
66
- }
67
- }
68
-
69
- /**
70
- * Park an already-written candidate file as the pending restore for
71
- * `dbPath`. Validates first; on failure the candidate is removed and the
72
- * validation error returned. On success the candidate is renamed to
73
- * `<dbPath>.restore-pending` (same directory, so the rename is atomic).
74
- */
75
- export function stageRestore(dbPath: string, candidatePath: string): RestoreValidation {
76
- const validation = validateRestoreCandidate(candidatePath);
77
- if (!validation.ok) {
78
- rmSync(candidatePath, { force: true });
79
- return validation;
80
- }
81
- const pending = dbPath + PENDING_SUFFIX;
82
- rmSync(pending, { force: true });
83
- renameSync(candidatePath, pending);
84
- return validation;
85
- }
86
-
87
- export interface PendingRestoreResult {
88
- applied: boolean;
89
- archivedTo?: string;
90
- }
91
-
92
- /**
93
- * If a pending restore exists for `dbPath`, promote it: move the current
94
- * DB (and its WAL/SHM sidecars) aside, then rename the pending file into
95
- * place. Call once at boot, before the store is opened.
96
- */
97
- export function applyPendingRestore(dbPath: string): PendingRestoreResult {
98
- const pending = dbPath + PENDING_SUFFIX;
99
- if (!existsSync(pending)) return { applied: false };
100
-
101
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
102
- const archived = `${dbPath}.pre-restore-${stamp}`;
103
-
104
- if (existsSync(dbPath)) {
105
- renameSync(dbPath, archived);
106
- }
107
- // The archived DB's WAL/SHM belong to it — drop the live sidecars so the
108
- // promoted file isn't paired with a stale WAL.
109
- for (const sidecar of [`${dbPath}-wal`, `${dbPath}-shm`]) {
110
- rmSync(sidecar, { force: true });
111
- }
112
- renameSync(pending, dbPath);
113
-
114
- return existsSync(archived)
115
- ? { applied: true, archivedTo: archived }
116
- : { applied: true };
117
- }
118
-
119
- /**
120
- * Write a live-consistent snapshot of `db` to a fresh file via
121
- * `VACUUM INTO`. The destination must not already exist. Returns the
122
- * snapshot's size in bytes.
123
- */
124
- export function vacuumSnapshot(db: Database.Database, destPath: string): number {
125
- rmSync(destPath, { force: true });
126
- db.prepare("VACUUM INTO ?").run(destPath);
127
- return statSync(destPath).size;
128
- }
129
-
130
- /** Scratch path for a backup snapshot, alongside the DB so rename stays atomic. */
131
- export function snapshotScratchPath(dbPath: string): string {
132
- return join(dirname(dbPath), `.backup-${process.pid}-${Date.now()}.sqlite`);
133
- }
@@ -1,45 +0,0 @@
1
- /**
2
- * live-status — derive the three-tier session status (active / idle / closed)
3
- * from a transcript file's mtime. Mirrors the Python daemon's
4
- * live_session_status(): explicit supersedence wins; missing file → closed;
5
- * otherwise bucketed by age.
6
- *
7
- * Thresholds match Python exactly:
8
- * < 15 min → active
9
- * 15 min – 24 h → idle
10
- * ≥ 24 h → closed
11
- *
12
- * Pure function over filesystem mtime. Tested with synthetic file ages.
13
- */
14
-
15
- import { statSync } from "node:fs";
16
- import { join } from "node:path";
17
- import { homedir } from "node:os";
18
- import type { SessionStatus } from "@shared/types.js";
19
-
20
- const ACTIVE_THRESHOLD_MS = 15 * 60 * 1000;
21
- const IDLE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
22
-
23
- function expandHome(path: string): string {
24
- if (path.startsWith("~/")) return join(homedir(), path.slice(2));
25
- return path;
26
- }
27
-
28
- export function liveSessionStatus(
29
- transcriptPath: string | null,
30
- persistedStatus: SessionStatus | "active" | "closed" | "superseded",
31
- now: number = Date.now(),
32
- ): SessionStatus {
33
- if (persistedStatus === "superseded") return "superseded";
34
- if (!transcriptPath) return "closed";
35
- try {
36
- const expanded = expandHome(transcriptPath);
37
- const st = statSync(expanded);
38
- const ageMs = now - st.mtimeMs;
39
- if (ageMs < ACTIVE_THRESHOLD_MS) return "active";
40
- if (ageMs < IDLE_THRESHOLD_MS) return "idle";
41
- return "closed";
42
- } catch {
43
- return "closed";
44
- }
45
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Migration runner. Reads versioned *.sql files from a directory, applies any
3
- * whose integer prefix is not yet in schema_migrations, and returns the list
4
- * of newly applied versions. Idempotent: re-running on an up-to-date database
5
- * is a no-op. Each migration file is expected to end with its own
6
- * `INSERT OR IGNORE INTO schema_migrations (...) VALUES (...)`; the runner
7
- * also defensively upserts the row in case a file forgets.
8
- */
9
-
10
- import { readFileSync, readdirSync } from "node:fs";
11
- import { join } from "node:path";
12
- import type Database from "better-sqlite3";
13
-
14
- export interface AppliedMigration {
15
- readonly version: number;
16
- readonly name: string;
17
- }
18
-
19
- const FILE_PATTERN = /^(\d+)_([a-z0-9_-]+)\.sql$/i;
20
-
21
- export function runMigrations(
22
- db: Database.Database,
23
- migrationsDir: string,
24
- ): ReadonlyArray<AppliedMigration> {
25
- db.exec(`
26
- CREATE TABLE IF NOT EXISTS schema_migrations (
27
- version INTEGER PRIMARY KEY,
28
- name TEXT NOT NULL,
29
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
30
- );
31
- `);
32
-
33
- const applied = new Set<number>(
34
- db
35
- .prepare<[], { version: number }>("SELECT version FROM schema_migrations")
36
- .all()
37
- .map((r) => r.version),
38
- );
39
-
40
- const files = readdirSync(migrationsDir)
41
- .filter((f) => FILE_PATTERN.test(f))
42
- .sort();
43
-
44
- const newlyApplied: AppliedMigration[] = [];
45
- const upsert = db.prepare(
46
- "INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (?, ?)",
47
- );
48
-
49
- for (const file of files) {
50
- const match = FILE_PATTERN.exec(file);
51
- if (!match) continue;
52
- const version = Number(match[1]);
53
- const name = match[2] ?? file;
54
- if (applied.has(version)) continue;
55
-
56
- const sql = readFileSync(join(migrationsDir, file), "utf8");
57
- db.exec("BEGIN");
58
- try {
59
- db.exec(sql);
60
- upsert.run(version, name);
61
- db.exec("COMMIT");
62
- } catch (err) {
63
- db.exec("ROLLBACK");
64
- throw new Error(
65
- `Migration ${file} failed: ${err instanceof Error ? err.message : String(err)}`,
66
- );
67
- }
68
- newlyApplied.push({ version, name });
69
- }
70
-
71
- return newlyApplied;
72
- }