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,208 +0,0 @@
1
- /**
2
- * Generic JSONL adapter — driven by per-source `parseConfig`.
3
- *
4
- * Reads any directory of newline-delimited JSON files where each line is one
5
- * conversation turn. The three baked-in adapters (claude-code, hermes, pi)
6
- * stay as their own classes because each has format-specific quirks that
7
- * would bloat a generic config schema. This adapter exists for everything
8
- * else: Cursor, Codex, custom logs, anything a user can drop on disk in a
9
- * consistent shape.
10
- *
11
- * parseConfig shape (all optional except where noted):
12
- * {
13
- * "textField": "content", // required — the message body
14
- * "roleField": "role", // optional — "user"/"assistant" filter
15
- * "userRole": "user", // string the user role takes
16
- * "assistantRole": "assistant", // string the assistant role takes
17
- * "timestampField": "timestamp", // optional — ISO or unix
18
- * "sessionIdField": "session_id", // optional — falls back to filename
19
- * "labelField": "title", // optional — falls back to filename
20
- * "filePattern": "*.jsonl", // glob within pathOrUrl
21
- * "idleMinutes": 15
22
- * }
23
- */
24
-
25
- import { promises as fs, existsSync, statSync } from "node:fs";
26
- import { basename, extname, join } from "node:path";
27
- import type {
28
- DetectionResult,
29
- DiscoverOptions,
30
- SessionChunk,
31
- TranscriptAdapter,
32
- } from "@ports/transcript-adapter.js";
33
- import { durationMinutes, normalizeTimestamp, safeSessionId } from "./common.js";
34
-
35
- export interface JsonlGenericConfig {
36
- readonly textField?: string;
37
- readonly roleField?: string;
38
- readonly userRole?: string;
39
- readonly assistantRole?: string;
40
- readonly timestampField?: string;
41
- readonly sessionIdField?: string;
42
- readonly labelField?: string;
43
- readonly filePattern?: string;
44
- readonly idleMinutes?: number;
45
- }
46
-
47
- export interface JsonlGenericAdapterOptions {
48
- readonly name: string;
49
- readonly path: string;
50
- readonly runtime: string;
51
- readonly config: JsonlGenericConfig;
52
- }
53
-
54
- export class JsonlGenericAdapter implements TranscriptAdapter {
55
- readonly name: string;
56
- readonly runtimeVersion: string;
57
- readonly transcriptKind = "jsonl-generic";
58
- readonly idleMinutes: number;
59
-
60
- private readonly path: string;
61
- private readonly cfg: Required<Omit<JsonlGenericConfig, "idleMinutes">>;
62
-
63
- constructor(opts: JsonlGenericAdapterOptions) {
64
- this.name = opts.name;
65
- this.runtimeVersion = opts.runtime;
66
- this.path = opts.path;
67
- this.idleMinutes = opts.config.idleMinutes ?? 15;
68
- this.cfg = {
69
- textField: opts.config.textField ?? "content",
70
- roleField: opts.config.roleField ?? "role",
71
- userRole: opts.config.userRole ?? "user",
72
- assistantRole: opts.config.assistantRole ?? "assistant",
73
- timestampField: opts.config.timestampField ?? "timestamp",
74
- sessionIdField: opts.config.sessionIdField ?? "session_id",
75
- labelField: opts.config.labelField ?? "title",
76
- filePattern: opts.config.filePattern ?? "*.jsonl",
77
- };
78
- }
79
-
80
- detect(): DetectionResult {
81
- if (existsSync(this.path) && statSync(this.path).isDirectory()) {
82
- return { adapterName: this.name, enabled: true, path: this.path, hint: null };
83
- }
84
- return {
85
- adapterName: this.name,
86
- enabled: false,
87
- path: null,
88
- hint: `${this.name}: directory not found at ${this.path}`,
89
- };
90
- }
91
-
92
- async discover(options: DiscoverOptions = {}): Promise<ReadonlyArray<string>> {
93
- if (!existsSync(this.path)) return [];
94
- const entries = await fs.readdir(this.path, { withFileTypes: true });
95
- const wantExt = this.extOfPattern(this.cfg.filePattern);
96
- const out: string[] = [];
97
- for (const e of entries) {
98
- if (!e.isFile()) continue;
99
- if (wantExt && extname(e.name) !== wantExt) continue;
100
- const full = join(this.path, e.name);
101
- if (options.since) {
102
- const st = statSync(full);
103
- if (st.mtime < options.since) continue;
104
- }
105
- out.push(full);
106
- }
107
- return out;
108
- }
109
-
110
- async parseSession(filePath: string): Promise<SessionChunk | null> {
111
- let raw: string;
112
- try {
113
- raw = await fs.readFile(filePath, "utf8");
114
- } catch {
115
- return null;
116
- }
117
- const lines = raw.split("\n").filter((l) => l.trim().length > 0);
118
- if (lines.length === 0) return null;
119
-
120
- const turns: { role: "user" | "assistant"; text: string; timestamp: string }[] = [];
121
- let sessionIdFromRows: string | null = null;
122
- let labelFromRows: string | null = null;
123
-
124
- for (const line of lines) {
125
- let row: Record<string, unknown>;
126
- try {
127
- row = JSON.parse(line) as Record<string, unknown>;
128
- } catch {
129
- continue;
130
- }
131
- if (!sessionIdFromRows && typeof row[this.cfg.sessionIdField] === "string") {
132
- sessionIdFromRows = row[this.cfg.sessionIdField] as string;
133
- }
134
- if (!labelFromRows && typeof row[this.cfg.labelField] === "string") {
135
- labelFromRows = row[this.cfg.labelField] as string;
136
- }
137
- const role = this.classifyRole(row[this.cfg.roleField]);
138
- if (!role) continue;
139
- const text = this.extractText(row[this.cfg.textField]);
140
- if (!text) continue;
141
- turns.push({
142
- role,
143
- text,
144
- timestamp: normalizeTimestamp(row[this.cfg.timestampField]),
145
- });
146
- }
147
-
148
- if (turns.length === 0) return null;
149
-
150
- const startedAt = turns[0]?.timestamp ?? "";
151
- const endedAt = turns[turns.length - 1]?.timestamp ?? startedAt;
152
- const fileName = basename(filePath, extname(filePath));
153
- const rawId = sessionIdFromRows ?? fileName;
154
- const label = labelFromRows ?? fileName;
155
- const text = turns
156
- .map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.text}`)
157
- .join("\n\n");
158
-
159
- return {
160
- id: safeSessionId(this.name, rawId),
161
- runtime: this.runtimeVersion,
162
- runtimeSessionId: rawId,
163
- sourcePath: filePath,
164
- startedAt,
165
- endedAt,
166
- durationMin: durationMinutes(startedAt, endedAt),
167
- turnCount: turns.length,
168
- byteRange: [0, Buffer.byteLength(raw)],
169
- projectDir: this.path,
170
- gitBranch: "",
171
- text,
172
- label,
173
- };
174
- }
175
-
176
- private classifyRole(raw: unknown): "user" | "assistant" | null {
177
- if (raw === undefined || raw === null) {
178
- // Some formats omit the field — assume alternating; bias to assistant.
179
- return "assistant";
180
- }
181
- if (typeof raw !== "string") return null;
182
- if (raw === this.cfg.userRole) return "user";
183
- if (raw === this.cfg.assistantRole) return "assistant";
184
- return null;
185
- }
186
-
187
- private extractText(raw: unknown): string {
188
- if (typeof raw === "string") return raw.trim();
189
- if (Array.isArray(raw)) {
190
- // OpenAI-style content: [{ type: "text", text: "..." }, ...]
191
- const parts: string[] = [];
192
- for (const item of raw) {
193
- if (typeof item === "string") parts.push(item);
194
- else if (item && typeof item === "object" && typeof (item as { text?: string }).text === "string") {
195
- parts.push((item as { text: string }).text);
196
- }
197
- }
198
- return parts.join(" ").trim();
199
- }
200
- return "";
201
- }
202
-
203
- private extOfPattern(pattern: string): string | null {
204
- const idx = pattern.lastIndexOf(".");
205
- if (idx < 0) return null;
206
- return pattern.slice(idx);
207
- }
208
- }
@@ -1,281 +0,0 @@
1
- /**
2
- * OpenCode adapter.
3
- *
4
- * Reads the OpenCode SQLite database at:
5
- * macOS: ~/Library/Application Support/opencode/opencode.db
6
- * Linux: $XDG_DATA_HOME/opencode/opencode.db (default ~/.local/share/opencode/opencode.db)
7
- *
8
- * Unlike the JSONL-based adapters, OpenCode stores all sessions and messages
9
- * in a single SQLite file. `discover()` queries the sessions table and returns
10
- * session IDs (not file paths). `parseSession()` treats its string argument as
11
- * a session ID and reconstructs a SessionChunk from the messages and parts tables.
12
- *
13
- * Part types extracted:
14
- * - text (non-ignored): the conversational prose
15
- * - tool : summarized as [tool: <name>]
16
- * All other part types (reasoning, step-start/finish, snapshot, patch,
17
- * compaction, agent, retry, subtask) are structural and skipped.
18
- *
19
- * Format reference: verified against sst/opencode migration
20
- * 20260127222353_familiar_lady_ursula and session.sql.ts, 2026-05-28.
21
- */
22
-
23
- import { existsSync, readFileSync } from "node:fs";
24
- import { homedir } from "node:os";
25
- import { join } from "node:path";
26
- import Database from "better-sqlite3";
27
- import type {
28
- DetectionResult,
29
- DiscoverOptions,
30
- SessionChunk,
31
- TranscriptAdapter,
32
- } from "@ports/transcript-adapter.js";
33
- import { durationMinutes, normalizeTimestamp } from "./common.js";
34
-
35
- const TOOL_OUTPUT_PREVIEW_CHARS = 240;
36
-
37
- export interface OpenCodeAdapterOptions {
38
- readonly dbPath?: string;
39
- }
40
-
41
- interface Turn {
42
- readonly role: "user" | "assistant";
43
- readonly text: string;
44
- readonly timestamp: string;
45
- }
46
-
47
- interface SessionRow {
48
- readonly id: string;
49
- readonly directory: string;
50
- readonly title: string;
51
- readonly time_created: number;
52
- readonly time_updated: number;
53
- }
54
-
55
- interface MessageRow {
56
- readonly id: string;
57
- readonly time_created: number;
58
- readonly data: string;
59
- }
60
-
61
- interface PartRow {
62
- readonly message_id: string;
63
- readonly time_created: number;
64
- readonly data: string;
65
- }
66
-
67
- interface MessageInfo {
68
- readonly role: "user" | "assistant";
69
- }
70
-
71
- interface TextPartData {
72
- readonly type: "text";
73
- readonly text: string;
74
- readonly ignored?: boolean;
75
- }
76
-
77
- interface ToolPartData {
78
- readonly type: "tool";
79
- readonly tool: string;
80
- readonly state?: Record<string, unknown>;
81
- }
82
-
83
- type PartData = TextPartData | ToolPartData | { readonly type: string };
84
-
85
- export function defaultDbPath(): string {
86
- if (process.env["OPENCODE_DB_PATH"]) return process.env["OPENCODE_DB_PATH"];
87
- if (process.platform === "darwin") {
88
- return join(homedir(), "Library", "Application Support", "opencode", "opencode.db");
89
- }
90
- const xdg = process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share");
91
- return join(xdg, "opencode", "opencode.db");
92
- }
93
-
94
- function readGitBranch(directory: string): string {
95
- try {
96
- const head = readFileSync(join(directory, ".git", "HEAD"), "utf8").trim();
97
- const match = /^ref: refs\/heads\/(.+)$/.exec(head);
98
- return match ? (match[1] ?? "") : "";
99
- } catch {
100
- return "";
101
- }
102
- }
103
-
104
- function extractTurns(messages: MessageRow[], partsByMessage: Map<string, PartData[]>): Turn[] {
105
- const turns: Turn[] = [];
106
- for (const msg of messages) {
107
- let info: MessageInfo;
108
- try {
109
- info = JSON.parse(msg.data) as MessageInfo;
110
- } catch {
111
- continue;
112
- }
113
- if (info.role !== "user" && info.role !== "assistant") continue;
114
-
115
- const parts = partsByMessage.get(msg.id) ?? [];
116
- const segments: string[] = [];
117
-
118
- for (const p of parts) {
119
- if (p.type === "text") {
120
- const tp = p as TextPartData;
121
- if (!tp.ignored && tp.text) segments.push(tp.text);
122
- } else if (p.type === "tool") {
123
- const tp = p as ToolPartData;
124
- const output = extractToolOutput(tp);
125
- segments.push(output ? `[tool: ${tp.tool}] ${output}` : `[tool: ${tp.tool}]`);
126
- }
127
- }
128
-
129
- const text = segments.join("\n").trim();
130
- if (!text) continue;
131
-
132
- turns.push({ role: info.role, text, timestamp: normalizeTimestamp(msg.time_created) });
133
- }
134
- return turns;
135
- }
136
-
137
- function extractToolOutput(part: ToolPartData): string {
138
- const state = part.state;
139
- if (!state) return "";
140
- const output =
141
- (state["output"] as string | undefined) ??
142
- (state["result"] as string | undefined) ??
143
- "";
144
- if (typeof output !== "string" || !output) return "";
145
- const preview = output.slice(0, TOOL_OUTPUT_PREVIEW_CHARS);
146
- return output.length > TOOL_OUTPUT_PREVIEW_CHARS ? `${preview}…` : preview;
147
- }
148
-
149
- function provisionalLabel(turns: ReadonlyArray<Turn>): string {
150
- for (const t of turns) {
151
- if (t.role !== "user") continue;
152
- const first = t.text.split("\n", 1)[0]?.trim();
153
- if (first) return first.slice(0, 80);
154
- }
155
- return "Untitled session";
156
- }
157
-
158
- export class OpenCodeAdapter implements TranscriptAdapter {
159
- readonly name = "opencode";
160
- readonly runtimeVersion = "opencode/1.0";
161
- readonly transcriptKind = "opencode-sqlite";
162
-
163
- private readonly dbPath: string;
164
-
165
- constructor(opts: OpenCodeAdapterOptions = {}) {
166
- this.dbPath = opts.dbPath ?? defaultDbPath();
167
- }
168
-
169
- detect(): DetectionResult {
170
- if (existsSync(this.dbPath)) {
171
- return { adapterName: this.name, enabled: true, path: this.dbPath, hint: null };
172
- }
173
- return { adapterName: this.name, enabled: false, path: null, hint: "opencode.db not found" };
174
- }
175
-
176
- async discover(options?: DiscoverOptions): Promise<ReadonlyArray<string>> {
177
- if (!existsSync(this.dbPath)) return [];
178
- let db: Database.Database | undefined;
179
- try {
180
- db = new Database(this.dbPath, { readonly: true });
181
- let rows: { id: string }[];
182
- if (options?.since) {
183
- const sinceMs = options.since.getTime();
184
- rows = db
185
- .prepare<[number], { id: string }>(
186
- `SELECT id FROM session WHERE time_archived IS NULL AND time_updated >= ?`,
187
- )
188
- .all(sinceMs);
189
- } else {
190
- rows = db
191
- .prepare<[], { id: string }>(
192
- `SELECT id FROM session WHERE time_archived IS NULL`,
193
- )
194
- .all();
195
- }
196
- return rows.map((r) => r.id);
197
- } catch {
198
- return [];
199
- } finally {
200
- db?.close();
201
- }
202
- }
203
-
204
- async parseSession(sessionId: string): Promise<SessionChunk | null> {
205
- if (!existsSync(this.dbPath)) return null;
206
- let db: Database.Database | undefined;
207
- try {
208
- db = new Database(this.dbPath, { readonly: true });
209
-
210
- const session = db
211
- .prepare<[string], SessionRow>(
212
- `SELECT id, directory, title, time_created, time_updated
213
- FROM session WHERE id = ?`,
214
- )
215
- .get(sessionId);
216
- if (!session) return null;
217
-
218
- const messages = db
219
- .prepare<[string], MessageRow>(
220
- `SELECT id, time_created, data FROM message
221
- WHERE session_id = ? ORDER BY time_created ASC`,
222
- )
223
- .all(sessionId);
224
-
225
- const partRows = db
226
- .prepare<[string], PartRow>(
227
- `SELECT message_id, time_created, data FROM part
228
- WHERE session_id = ? ORDER BY time_created ASC`,
229
- )
230
- .all(sessionId);
231
-
232
- const partsByMessage = new Map<string, PartData[]>();
233
- for (const row of partRows) {
234
- let data: PartData;
235
- try {
236
- data = JSON.parse(row.data) as PartData;
237
- } catch {
238
- continue;
239
- }
240
- const bucket = partsByMessage.get(row.message_id);
241
- if (bucket) {
242
- bucket.push(data);
243
- } else {
244
- partsByMessage.set(row.message_id, [data]);
245
- }
246
- }
247
-
248
- const turns = extractTurns(messages, partsByMessage);
249
- if (turns.length === 0) return null;
250
-
251
- const startedAt = normalizeTimestamp(session.time_created);
252
- const endedAt = normalizeTimestamp(session.time_updated);
253
- const transcript = turns.map((t) => `${t.role}: ${t.text}`).join("\n\n");
254
-
255
- const label =
256
- session.title && session.title !== "New session"
257
- ? session.title.slice(0, 80)
258
- : provisionalLabel(turns);
259
-
260
- return {
261
- id: `oc_${sessionId}`,
262
- runtime: this.runtimeVersion,
263
- runtimeSessionId: sessionId,
264
- sourcePath: `${this.dbPath}::${sessionId}`,
265
- startedAt,
266
- endedAt,
267
- durationMin: durationMinutes(startedAt, endedAt),
268
- turnCount: turns.length,
269
- byteRange: [0, Buffer.byteLength(transcript, "utf8")],
270
- projectDir: session.directory,
271
- gitBranch: readGitBranch(session.directory),
272
- text: transcript,
273
- label,
274
- };
275
- } catch {
276
- return null;
277
- } finally {
278
- db?.close();
279
- }
280
- }
281
- }