@loreai/core 0.0.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -5
- package/dist/bun/agents-file.d.ts +59 -0
- package/dist/bun/agents-file.d.ts.map +1 -0
- package/dist/bun/config.d.ts +58 -0
- package/dist/bun/config.d.ts.map +1 -0
- package/dist/bun/curator.d.ts +35 -0
- package/dist/bun/curator.d.ts.map +1 -0
- package/dist/bun/db/driver.bun.d.ts +5 -0
- package/dist/bun/db/driver.bun.d.ts.map +1 -0
- package/dist/bun/db/driver.node.d.ts +15 -0
- package/dist/bun/db/driver.node.d.ts.map +1 -0
- package/dist/bun/db.d.ts +22 -0
- package/dist/bun/db.d.ts.map +1 -0
- package/dist/bun/distillation.d.ts +32 -0
- package/dist/bun/distillation.d.ts.map +1 -0
- package/dist/bun/embedding.d.ts +90 -0
- package/dist/bun/embedding.d.ts.map +1 -0
- package/dist/bun/gradient.d.ts +73 -0
- package/dist/bun/gradient.d.ts.map +1 -0
- package/dist/bun/index.d.ts +19 -0
- package/dist/bun/index.d.ts.map +1 -0
- package/dist/bun/index.js +28236 -0
- package/dist/bun/index.js.map +7 -0
- package/dist/bun/lat-reader.d.ts +69 -0
- package/dist/bun/lat-reader.d.ts.map +1 -0
- package/dist/bun/log.d.ts +17 -0
- package/dist/bun/log.d.ts.map +1 -0
- package/dist/bun/ltm.d.ts +138 -0
- package/dist/bun/ltm.d.ts.map +1 -0
- package/dist/bun/markdown.d.ts +37 -0
- package/dist/bun/markdown.d.ts.map +1 -0
- package/dist/bun/prompt.d.ts +47 -0
- package/dist/bun/prompt.d.ts.map +1 -0
- package/dist/bun/recall.d.ts +41 -0
- package/dist/bun/recall.d.ts.map +1 -0
- package/dist/bun/search.d.ts +113 -0
- package/dist/bun/search.d.ts.map +1 -0
- package/dist/bun/temporal.d.ts +66 -0
- package/dist/bun/temporal.d.ts.map +1 -0
- package/dist/bun/types.d.ts +180 -0
- package/dist/bun/types.d.ts.map +1 -0
- package/dist/bun/worker.d.ts +6 -0
- package/dist/bun/worker.d.ts.map +1 -0
- package/dist/node/agents-file.d.ts +59 -0
- package/dist/node/agents-file.d.ts.map +1 -0
- package/dist/node/config.d.ts +58 -0
- package/dist/node/config.d.ts.map +1 -0
- package/dist/node/curator.d.ts +35 -0
- package/dist/node/curator.d.ts.map +1 -0
- package/dist/node/db/driver.bun.d.ts +5 -0
- package/dist/node/db/driver.bun.d.ts.map +1 -0
- package/dist/node/db/driver.node.d.ts +15 -0
- package/dist/node/db/driver.node.d.ts.map +1 -0
- package/dist/node/db.d.ts +22 -0
- package/dist/node/db.d.ts.map +1 -0
- package/dist/node/distillation.d.ts +32 -0
- package/dist/node/distillation.d.ts.map +1 -0
- package/dist/node/embedding.d.ts +90 -0
- package/dist/node/embedding.d.ts.map +1 -0
- package/dist/node/gradient.d.ts +73 -0
- package/dist/node/gradient.d.ts.map +1 -0
- package/dist/node/index.d.ts +19 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +28253 -0
- package/dist/node/index.js.map +7 -0
- package/dist/node/lat-reader.d.ts +69 -0
- package/dist/node/lat-reader.d.ts.map +1 -0
- package/dist/node/log.d.ts +17 -0
- package/dist/node/log.d.ts.map +1 -0
- package/dist/node/ltm.d.ts +138 -0
- package/dist/node/ltm.d.ts.map +1 -0
- package/dist/node/markdown.d.ts +37 -0
- package/dist/node/markdown.d.ts.map +1 -0
- package/dist/node/prompt.d.ts +47 -0
- package/dist/node/prompt.d.ts.map +1 -0
- package/dist/node/recall.d.ts +41 -0
- package/dist/node/recall.d.ts.map +1 -0
- package/dist/node/search.d.ts +113 -0
- package/dist/node/search.d.ts.map +1 -0
- package/dist/node/temporal.d.ts +66 -0
- package/dist/node/temporal.d.ts.map +1 -0
- package/dist/node/types.d.ts +180 -0
- package/dist/node/types.d.ts.map +1 -0
- package/dist/node/worker.d.ts +6 -0
- package/dist/node/worker.d.ts.map +1 -0
- package/dist/types/agents-file.d.ts +59 -0
- package/dist/types/agents-file.d.ts.map +1 -0
- package/dist/types/config.d.ts +58 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/curator.d.ts +35 -0
- package/dist/types/curator.d.ts.map +1 -0
- package/dist/types/db/driver.bun.d.ts +5 -0
- package/dist/types/db/driver.bun.d.ts.map +1 -0
- package/dist/types/db/driver.node.d.ts +15 -0
- package/dist/types/db/driver.node.d.ts.map +1 -0
- package/dist/types/db.d.ts +22 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/distillation.d.ts +32 -0
- package/dist/types/distillation.d.ts.map +1 -0
- package/dist/types/embedding.d.ts +90 -0
- package/dist/types/embedding.d.ts.map +1 -0
- package/dist/types/gradient.d.ts +73 -0
- package/dist/types/gradient.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lat-reader.d.ts +69 -0
- package/dist/types/lat-reader.d.ts.map +1 -0
- package/dist/types/log.d.ts +17 -0
- package/dist/types/log.d.ts.map +1 -0
- package/dist/types/ltm.d.ts +138 -0
- package/dist/types/ltm.d.ts.map +1 -0
- package/dist/types/markdown.d.ts +37 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/prompt.d.ts +47 -0
- package/dist/types/prompt.d.ts.map +1 -0
- package/dist/types/recall.d.ts +41 -0
- package/dist/types/recall.d.ts.map +1 -0
- package/dist/types/search.d.ts +113 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/temporal.d.ts +66 -0
- package/dist/types/temporal.d.ts.map +1 -0
- package/dist/types/types.d.ts +180 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/worker.d.ts +6 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/package.json +48 -5
- package/src/agents-file.ts +406 -0
- package/src/config.ts +132 -0
- package/src/curator.ts +220 -0
- package/src/db/driver.bun.ts +18 -0
- package/src/db/driver.node.ts +54 -0
- package/src/db.ts +433 -0
- package/src/distillation.ts +433 -0
- package/src/embedding.ts +528 -0
- package/src/gradient.ts +1387 -0
- package/src/index.ts +109 -0
- package/src/lat-reader.ts +374 -0
- package/src/log.ts +27 -0
- package/src/ltm.ts +861 -0
- package/src/markdown.ts +129 -0
- package/src/prompt.ts +454 -0
- package/src/recall.ts +446 -0
- package/src/search.ts +330 -0
- package/src/temporal.ts +379 -0
- package/src/types.ts +199 -0
- package/src/worker.ts +26 -0
package/src/db.ts
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { Database } from "#db/driver";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
|
|
5
|
+
const SCHEMA_VERSION = 10;
|
|
6
|
+
|
|
7
|
+
const MIGRATIONS: string[] = [
|
|
8
|
+
`
|
|
9
|
+
-- Version 1: Initial schema
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
path TEXT NOT NULL UNIQUE,
|
|
14
|
+
name TEXT,
|
|
15
|
+
created_at INTEGER NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS temporal_messages (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
21
|
+
session_id TEXT NOT NULL,
|
|
22
|
+
role TEXT NOT NULL,
|
|
23
|
+
content TEXT NOT NULL,
|
|
24
|
+
tokens INTEGER DEFAULT 0,
|
|
25
|
+
distilled INTEGER DEFAULT 0,
|
|
26
|
+
created_at INTEGER NOT NULL,
|
|
27
|
+
metadata TEXT
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS temporal_fts USING fts5(
|
|
31
|
+
content,
|
|
32
|
+
content=temporal_messages,
|
|
33
|
+
content_rowid=rowid,
|
|
34
|
+
tokenize='porter unicode61'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
-- Triggers to keep FTS in sync
|
|
38
|
+
CREATE TRIGGER IF NOT EXISTS temporal_fts_insert AFTER INSERT ON temporal_messages BEGIN
|
|
39
|
+
INSERT INTO temporal_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
40
|
+
END;
|
|
41
|
+
|
|
42
|
+
CREATE TRIGGER IF NOT EXISTS temporal_fts_delete AFTER DELETE ON temporal_messages BEGIN
|
|
43
|
+
INSERT INTO temporal_fts(temporal_fts, rowid, content) VALUES('delete', old.rowid, old.content);
|
|
44
|
+
END;
|
|
45
|
+
|
|
46
|
+
CREATE TRIGGER IF NOT EXISTS temporal_fts_update AFTER UPDATE ON temporal_messages BEGIN
|
|
47
|
+
INSERT INTO temporal_fts(temporal_fts, rowid, content) VALUES('delete', old.rowid, old.content);
|
|
48
|
+
INSERT INTO temporal_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
49
|
+
END;
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_session ON temporal_messages(session_id);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_project ON temporal_messages(project_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_distilled ON temporal_messages(distilled);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_created ON temporal_messages(created_at);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS distillations (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
59
|
+
session_id TEXT NOT NULL,
|
|
60
|
+
narrative TEXT NOT NULL,
|
|
61
|
+
facts TEXT NOT NULL,
|
|
62
|
+
source_ids TEXT NOT NULL,
|
|
63
|
+
generation INTEGER DEFAULT 0,
|
|
64
|
+
token_count INTEGER DEFAULT 0,
|
|
65
|
+
created_at INTEGER NOT NULL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_session ON distillations(session_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_project ON distillations(project_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_generation ON distillations(generation);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_created ON distillations(created_at);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS knowledge (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
project_id TEXT,
|
|
76
|
+
category TEXT NOT NULL,
|
|
77
|
+
title TEXT NOT NULL,
|
|
78
|
+
content TEXT NOT NULL,
|
|
79
|
+
source_session TEXT,
|
|
80
|
+
cross_project INTEGER DEFAULT 0,
|
|
81
|
+
confidence REAL DEFAULT 1.0,
|
|
82
|
+
created_at INTEGER NOT NULL,
|
|
83
|
+
updated_at INTEGER NOT NULL,
|
|
84
|
+
metadata TEXT
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
|
|
88
|
+
title,
|
|
89
|
+
content,
|
|
90
|
+
category,
|
|
91
|
+
content=knowledge,
|
|
92
|
+
content_rowid=rowid,
|
|
93
|
+
tokenize='porter unicode61'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TRIGGER IF NOT EXISTS knowledge_fts_insert AFTER INSERT ON knowledge BEGIN
|
|
97
|
+
INSERT INTO knowledge_fts(rowid, title, content, category)
|
|
98
|
+
VALUES (new.rowid, new.title, new.content, new.category);
|
|
99
|
+
END;
|
|
100
|
+
|
|
101
|
+
CREATE TRIGGER IF NOT EXISTS knowledge_fts_delete AFTER DELETE ON knowledge BEGIN
|
|
102
|
+
INSERT INTO knowledge_fts(knowledge_fts, rowid, title, content, category)
|
|
103
|
+
VALUES('delete', old.rowid, old.title, old.content, old.category);
|
|
104
|
+
END;
|
|
105
|
+
|
|
106
|
+
CREATE TRIGGER IF NOT EXISTS knowledge_fts_update AFTER UPDATE ON knowledge BEGIN
|
|
107
|
+
INSERT INTO knowledge_fts(knowledge_fts, rowid, title, content, category)
|
|
108
|
+
VALUES('delete', old.rowid, old.title, old.content, old.category);
|
|
109
|
+
INSERT INTO knowledge_fts(rowid, title, content, category)
|
|
110
|
+
VALUES (new.rowid, new.title, new.content, new.category);
|
|
111
|
+
END;
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_project ON knowledge(project_id);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge(category);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_cross ON knowledge(cross_project);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
118
|
+
version INTEGER NOT NULL
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
INSERT INTO schema_version (version) VALUES (1);
|
|
122
|
+
`,
|
|
123
|
+
`
|
|
124
|
+
-- Version 2: Replace narrative+facts with observations text
|
|
125
|
+
ALTER TABLE distillations ADD COLUMN observations TEXT NOT NULL DEFAULT '';
|
|
126
|
+
`,
|
|
127
|
+
`
|
|
128
|
+
-- Version 3: One-time vacuum to reclaim accumulated free pages, and enable
|
|
129
|
+
-- incremental auto-vacuum so future deletes return pages to the OS.
|
|
130
|
+
-- VACUUM must run outside a transaction and cannot be in a multi-statement
|
|
131
|
+
-- exec, so it is handled specially in the migrate() function.
|
|
132
|
+
`,
|
|
133
|
+
`
|
|
134
|
+
-- Version 4: Persistent session state for error recovery.
|
|
135
|
+
-- Stores forceMinLayer so it survives OpenCode restarts. Without this,
|
|
136
|
+
-- a "prompt too long" error recovery (escalate to layer 2) is lost if
|
|
137
|
+
-- the process restarts before the next turn.
|
|
138
|
+
CREATE TABLE IF NOT EXISTS session_state (
|
|
139
|
+
session_id TEXT PRIMARY KEY,
|
|
140
|
+
force_min_layer INTEGER NOT NULL DEFAULT 0,
|
|
141
|
+
updated_at INTEGER NOT NULL
|
|
142
|
+
);
|
|
143
|
+
`,
|
|
144
|
+
`
|
|
145
|
+
-- Version 5: Multi-resolution composable distillations.
|
|
146
|
+
-- Instead of deleting gen-0 distillations during meta-distillation,
|
|
147
|
+
-- mark them as archived. Archived entries are excluded from the in-context
|
|
148
|
+
-- prefix but remain searchable via the recall tool, providing a detailed
|
|
149
|
+
-- "zoom-in" layer beneath the compressed gen-1 summary.
|
|
150
|
+
-- Inspired by Cartridges (Eyuboglu et al., 2025) composability: independently
|
|
151
|
+
-- compressed representations can be concatenated and queried without retraining.
|
|
152
|
+
-- Reference: https://arxiv.org/abs/2501.17390
|
|
153
|
+
ALTER TABLE distillations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_archived ON distillations(archived);
|
|
155
|
+
`,
|
|
156
|
+
`
|
|
157
|
+
-- Version 6: Compound indexes for common multi-column query patterns.
|
|
158
|
+
-- Almost every query filters on (project_id, session_id) but only single-column
|
|
159
|
+
-- indexes existed, forcing SQLite to pick one and scan for the rest.
|
|
160
|
+
|
|
161
|
+
-- temporal_messages: covers bySession, search-LIKE fallback, count, undistilledCount
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_project_session ON temporal_messages(project_id, session_id);
|
|
163
|
+
-- temporal_messages: covers undistilled() and undistilledCount() with distilled filter
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_project_session_distilled ON temporal_messages(project_id, session_id, distilled);
|
|
165
|
+
-- temporal_messages: covers pruning TTL pass and size-cap pass (distilled=1 ordered by created_at)
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_temporal_project_distilled_created ON temporal_messages(project_id, distilled, created_at);
|
|
167
|
+
|
|
168
|
+
-- distillations: covers loadForSession, latestObservations, searchDistillations, resetOrphans
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_project_session ON distillations(project_id, session_id);
|
|
170
|
+
-- distillations: covers gen0Count, loadGen0, gradient prefix loading (archived filter)
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_project_session_gen_archived ON distillations(project_id, session_id, generation, archived);
|
|
172
|
+
|
|
173
|
+
-- Drop redundant single-column indexes that are now left-prefixes of compound indexes.
|
|
174
|
+
-- idx_temporal_project is a prefix of idx_temporal_project_session.
|
|
175
|
+
-- idx_distillation_project is a prefix of idx_distillation_project_session.
|
|
176
|
+
-- idx_temporal_distilled is a prefix of no compound index but is low-selectivity (0/1)
|
|
177
|
+
-- and all queries that use it also filter on project_id — covered by the new compounds.
|
|
178
|
+
DROP INDEX IF EXISTS idx_temporal_project;
|
|
179
|
+
DROP INDEX IF EXISTS idx_temporal_distilled;
|
|
180
|
+
DROP INDEX IF EXISTS idx_distillation_project;
|
|
181
|
+
`,
|
|
182
|
+
`
|
|
183
|
+
-- Version 7: FTS5 for distillations — enables ranked search instead of LIKE.
|
|
184
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS distillation_fts USING fts5(
|
|
185
|
+
observations,
|
|
186
|
+
content=distillations,
|
|
187
|
+
content_rowid=rowid,
|
|
188
|
+
tokenize='porter unicode61'
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
-- Backfill existing data (skip empty observations from schema v1→v2 migration)
|
|
192
|
+
INSERT INTO distillation_fts(rowid, observations)
|
|
193
|
+
SELECT rowid, observations FROM distillations WHERE observations != '';
|
|
194
|
+
|
|
195
|
+
-- Sync triggers
|
|
196
|
+
CREATE TRIGGER IF NOT EXISTS distillation_fts_insert AFTER INSERT ON distillations BEGIN
|
|
197
|
+
INSERT INTO distillation_fts(rowid, observations) VALUES (new.rowid, new.observations);
|
|
198
|
+
END;
|
|
199
|
+
|
|
200
|
+
CREATE TRIGGER IF NOT EXISTS distillation_fts_delete AFTER DELETE ON distillations BEGIN
|
|
201
|
+
INSERT INTO distillation_fts(distillation_fts, rowid, observations)
|
|
202
|
+
VALUES('delete', old.rowid, old.observations);
|
|
203
|
+
END;
|
|
204
|
+
|
|
205
|
+
CREATE TRIGGER IF NOT EXISTS distillation_fts_update AFTER UPDATE ON distillations BEGIN
|
|
206
|
+
INSERT INTO distillation_fts(distillation_fts, rowid, observations)
|
|
207
|
+
VALUES('delete', old.rowid, old.observations);
|
|
208
|
+
INSERT INTO distillation_fts(rowid, observations) VALUES (new.rowid, new.observations);
|
|
209
|
+
END;
|
|
210
|
+
`,
|
|
211
|
+
`
|
|
212
|
+
-- Version 8: Embedding BLOB column for vector search (Voyage AI).
|
|
213
|
+
-- No backfill — entries get embedded lazily on next create/update
|
|
214
|
+
-- or via explicit backfill when embeddings are first enabled.
|
|
215
|
+
ALTER TABLE knowledge ADD COLUMN embedding BLOB;
|
|
216
|
+
|
|
217
|
+
-- Key-value metadata table for plugin state (e.g. embedding config fingerprint).
|
|
218
|
+
CREATE TABLE IF NOT EXISTS kv_meta (
|
|
219
|
+
key TEXT PRIMARY KEY,
|
|
220
|
+
value TEXT NOT NULL
|
|
221
|
+
);
|
|
222
|
+
`,
|
|
223
|
+
`
|
|
224
|
+
-- Version 9: Embedding BLOB column for distillation vector search.
|
|
225
|
+
-- Same pattern as knowledge embeddings (version 8). Enables semantic
|
|
226
|
+
-- search over distilled session summaries via cosine similarity.
|
|
227
|
+
-- No backfill — entries get embedded lazily on next distillation
|
|
228
|
+
-- or via explicit backfill when embeddings are first enabled.
|
|
229
|
+
ALTER TABLE distillations ADD COLUMN embedding BLOB;
|
|
230
|
+
`,
|
|
231
|
+
`
|
|
232
|
+
-- Version 10: lat.md section cache + knowledge cross-references.
|
|
233
|
+
|
|
234
|
+
-- lat.md section cache for recall integration.
|
|
235
|
+
-- Parsed from lat.md/ directory markdown files, FTS5-indexed for search.
|
|
236
|
+
CREATE TABLE IF NOT EXISTS lat_sections (
|
|
237
|
+
id TEXT PRIMARY KEY,
|
|
238
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
239
|
+
file TEXT NOT NULL,
|
|
240
|
+
heading TEXT NOT NULL,
|
|
241
|
+
depth INTEGER NOT NULL,
|
|
242
|
+
content TEXT NOT NULL,
|
|
243
|
+
content_hash TEXT NOT NULL,
|
|
244
|
+
first_paragraph TEXT,
|
|
245
|
+
updated_at INTEGER NOT NULL
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lat_sections_fts USING fts5(
|
|
249
|
+
heading,
|
|
250
|
+
content,
|
|
251
|
+
content=lat_sections,
|
|
252
|
+
content_rowid=rowid,
|
|
253
|
+
tokenize='porter unicode61'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
CREATE TRIGGER IF NOT EXISTS lat_fts_insert AFTER INSERT ON lat_sections BEGIN
|
|
257
|
+
INSERT INTO lat_sections_fts(rowid, heading, content)
|
|
258
|
+
VALUES (new.rowid, new.heading, new.content);
|
|
259
|
+
END;
|
|
260
|
+
|
|
261
|
+
CREATE TRIGGER IF NOT EXISTS lat_fts_delete AFTER DELETE ON lat_sections BEGIN
|
|
262
|
+
INSERT INTO lat_sections_fts(lat_sections_fts, rowid, heading, content)
|
|
263
|
+
VALUES('delete', old.rowid, old.heading, old.content);
|
|
264
|
+
END;
|
|
265
|
+
|
|
266
|
+
CREATE TRIGGER IF NOT EXISTS lat_fts_update AFTER UPDATE ON lat_sections BEGIN
|
|
267
|
+
INSERT INTO lat_sections_fts(lat_sections_fts, rowid, heading, content)
|
|
268
|
+
VALUES('delete', old.rowid, old.heading, old.content);
|
|
269
|
+
INSERT INTO lat_sections_fts(rowid, heading, content)
|
|
270
|
+
VALUES (new.rowid, new.heading, new.content);
|
|
271
|
+
END;
|
|
272
|
+
|
|
273
|
+
CREATE INDEX IF NOT EXISTS idx_lat_sections_project ON lat_sections(project_id);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_lat_sections_file ON lat_sections(project_id, file);
|
|
275
|
+
|
|
276
|
+
-- Knowledge cross-references via [[entry-id]] wiki links.
|
|
277
|
+
-- ON DELETE CASCADE: when either entry is deleted, the ref row is auto-removed.
|
|
278
|
+
CREATE TABLE IF NOT EXISTS knowledge_refs (
|
|
279
|
+
from_id TEXT NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
280
|
+
to_id TEXT NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
281
|
+
PRIMARY KEY (from_id, to_id)
|
|
282
|
+
);
|
|
283
|
+
`,
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
function dataDir() {
|
|
287
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
288
|
+
const base = xdg || join(process.env.HOME || "~", ".local", "share");
|
|
289
|
+
return join(base, "opencode-lore");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let instance: Database | undefined;
|
|
293
|
+
|
|
294
|
+
export function db(): Database {
|
|
295
|
+
if (instance) return instance;
|
|
296
|
+
const envPath = process.env.LORE_DB_PATH;
|
|
297
|
+
let path: string;
|
|
298
|
+
if (envPath) {
|
|
299
|
+
mkdirSync(dirname(envPath), { recursive: true });
|
|
300
|
+
path = envPath;
|
|
301
|
+
} else {
|
|
302
|
+
const dir = dataDir();
|
|
303
|
+
mkdirSync(dir, { recursive: true });
|
|
304
|
+
path = join(dir, "lore.db");
|
|
305
|
+
}
|
|
306
|
+
// Both `bun:sqlite` and `node:sqlite` create the file by default if it doesn't
|
|
307
|
+
// exist, so no special option is needed. (bun:sqlite's `{ create: true }`
|
|
308
|
+
// exists only to opt INTO creation when you want readonly=false — which is
|
|
309
|
+
// already the default for our case.)
|
|
310
|
+
instance = new Database(path);
|
|
311
|
+
instance.exec("PRAGMA journal_mode = WAL");
|
|
312
|
+
instance.exec("PRAGMA foreign_keys = ON");
|
|
313
|
+
// Return freed pages to the OS incrementally on each transaction commit
|
|
314
|
+
// instead of accumulating a free-page list that bloats the file.
|
|
315
|
+
instance.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
|
316
|
+
migrate(instance);
|
|
317
|
+
return instance;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Index of the migration that performs a one-time VACUUM.
|
|
321
|
+
// VACUUM cannot run inside a transaction, so migrate() handles it specially.
|
|
322
|
+
const VACUUM_MIGRATION_INDEX = 2; // 0-based index of version-3 migration
|
|
323
|
+
|
|
324
|
+
function migrate(database: Database) {
|
|
325
|
+
const row = database
|
|
326
|
+
.query(
|
|
327
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'",
|
|
328
|
+
)
|
|
329
|
+
.get() as { name: string } | null;
|
|
330
|
+
const current = row
|
|
331
|
+
? ((
|
|
332
|
+
database.query("SELECT version FROM schema_version").get() as {
|
|
333
|
+
version: number;
|
|
334
|
+
}
|
|
335
|
+
)?.version ?? 0)
|
|
336
|
+
: 0;
|
|
337
|
+
if (current >= MIGRATIONS.length) return;
|
|
338
|
+
for (let i = current; i < MIGRATIONS.length; i++) {
|
|
339
|
+
if (i === VACUUM_MIGRATION_INDEX) {
|
|
340
|
+
// VACUUM cannot run inside a transaction. Run it directly.
|
|
341
|
+
// auto_vacuum mode must be set *before* VACUUM — SQLite bakes it into
|
|
342
|
+
// the file header during the rebuild. After this, every subsequent
|
|
343
|
+
// startup's "PRAGMA auto_vacuum = INCREMENTAL" is a no-op (already set).
|
|
344
|
+
database.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
|
345
|
+
database.exec("VACUUM");
|
|
346
|
+
} else {
|
|
347
|
+
database.exec(MIGRATIONS[i]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Update version to latest. Migration 0 inserts version=1 via its own INSERT,
|
|
351
|
+
// but subsequent migrations don't update it, so always normalize to MIGRATIONS.length.
|
|
352
|
+
database.exec(`UPDATE schema_version SET version = ${MIGRATIONS.length}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function close() {
|
|
356
|
+
if (instance) {
|
|
357
|
+
instance.close();
|
|
358
|
+
instance = undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Project management
|
|
363
|
+
export function ensureProject(path: string, name?: string): string {
|
|
364
|
+
const existing = db()
|
|
365
|
+
.query("SELECT id FROM projects WHERE path = ?")
|
|
366
|
+
.get(path) as { id: string } | null;
|
|
367
|
+
if (existing) return existing.id;
|
|
368
|
+
const id = crypto.randomUUID();
|
|
369
|
+
db()
|
|
370
|
+
.query(
|
|
371
|
+
"INSERT INTO projects (id, path, name, created_at) VALUES (?, ?, ?, ?)",
|
|
372
|
+
)
|
|
373
|
+
.run(id, path, name ?? path.split("/").pop() ?? "unknown", Date.now());
|
|
374
|
+
return id;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function projectId(path: string): string | undefined {
|
|
378
|
+
const row = db()
|
|
379
|
+
.query("SELECT id FROM projects WHERE path = ?")
|
|
380
|
+
.get(path) as { id: string } | null;
|
|
381
|
+
return row?.id;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Look up a project's display name by its internal ID. */
|
|
385
|
+
export function projectName(id: string): string | null {
|
|
386
|
+
const row = db()
|
|
387
|
+
.query("SELECT name FROM projects WHERE id = ?")
|
|
388
|
+
.get(id) as { name: string } | null;
|
|
389
|
+
return row?.name ?? null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Returns true if Lore has never been used before (no projects in the DB).
|
|
394
|
+
* Must be called before ensureProject() to get an accurate result.
|
|
395
|
+
*/
|
|
396
|
+
export function isFirstRun(): boolean {
|
|
397
|
+
const row = db()
|
|
398
|
+
.query("SELECT COUNT(*) as count FROM projects")
|
|
399
|
+
.get() as { count: number };
|
|
400
|
+
return row.count === 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Persistent session state (error recovery)
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Load persisted forceMinLayer for a session. Returns 0 if none stored.
|
|
409
|
+
*/
|
|
410
|
+
export function loadForceMinLayer(sessionID: string): number {
|
|
411
|
+
const row = db()
|
|
412
|
+
.query("SELECT force_min_layer FROM session_state WHERE session_id = ?")
|
|
413
|
+
.get(sessionID) as { force_min_layer: number } | null;
|
|
414
|
+
return row?.force_min_layer ?? 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Persist forceMinLayer for a session. Deletes the row when layer is 0
|
|
419
|
+
* (consumed) to avoid unbounded growth.
|
|
420
|
+
*/
|
|
421
|
+
export function saveForceMinLayer(sessionID: string, layer: number): void {
|
|
422
|
+
if (layer === 0) {
|
|
423
|
+
db()
|
|
424
|
+
.query("DELETE FROM session_state WHERE session_id = ?")
|
|
425
|
+
.run(sessionID);
|
|
426
|
+
} else {
|
|
427
|
+
db()
|
|
428
|
+
.query(
|
|
429
|
+
"INSERT OR REPLACE INTO session_state (session_id, force_min_layer, updated_at) VALUES (?, ?, ?)",
|
|
430
|
+
)
|
|
431
|
+
.run(sessionID, layer, Date.now());
|
|
432
|
+
}
|
|
433
|
+
}
|