@lotargo/memory_plugin 1.6.3 → 1.6.5

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.
@@ -3,7 +3,8 @@ import { join } from "path";
3
3
  import { MEMORY_DIR, GLOBAL_KEY, buildMemoryContent, extractFacts, writeMemoryFile } from "../memory.js";
4
4
  import { toVectorBytes } from "../retrieval/retriever.js";
5
5
 
6
- let isSyncing = false;
6
+ let isSyncing = false;
7
+ let syncRequested = false;
7
8
 
8
9
  // Reverse sync (cloud -> local) throttling: only pull at most once per window
9
10
  // even if readMemory triggers it frequently (recall hits every keystroke).
@@ -30,13 +31,24 @@ async function processSyncTask(db, task) {
30
31
  sql: "SELECT id FROM documents WHERE id = ? OR path = ?;",
31
32
  args: [docId, docId],
32
33
  });
33
- if (docRow.rows.length > 0) {
34
- const realDocId = docRow.rows[0].id;
35
- await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);", args: [realDocId] });
34
+ if (docRow.rows.length > 0) {
35
+ const realDocId = docRow.rows[0].id;
36
+ await db.cloudClient.execute({
37
+ sql: `DELETE FROM graph_edges
38
+ WHERE source_id = ? OR target_id = ?
39
+ OR target_id GLOB ?
40
+ OR source_id IN (SELECT id FROM sections WHERE doc_id = ?)
41
+ OR target_id IN (SELECT id FROM sections WHERE doc_id = ?)
42
+ OR source_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
43
+ OR target_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
44
+ OR source_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?)
45
+ OR target_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);`,
46
+ args: [realDocId, realDocId, `${realDocId}:L*`, realDocId, realDocId, realDocId, realDocId, realDocId, realDocId],
47
+ });
48
+ await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);", args: [realDocId] });
36
49
  await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks WHERE doc_id = ?;", args: [realDocId] });
37
50
  await db.cloudClient.execute({ sql: "DELETE FROM medium_chunks WHERE doc_id = ?;", args: [realDocId] });
38
51
  await db.cloudClient.execute({ sql: "DELETE FROM sections WHERE doc_id = ?;", args: [realDocId] });
39
- await db.cloudClient.execute({ sql: "DELETE FROM graph_edges WHERE source_id = ? OR target_id = ?;", args: [realDocId, realDocId] });
40
52
  await db.cloudClient.execute({ sql: "DELETE FROM knowledge_links WHERE doc_id = ?;", args: [realDocId] });
41
53
  await db.cloudClient.execute({ sql: "DELETE FROM documents WHERE id = ?;", args: [realDocId] });
42
54
  }
@@ -52,14 +64,25 @@ async function processSyncTask(db, task) {
52
64
  sql: "SELECT id FROM documents WHERE id = ? OR path = ?;",
53
65
  args: [doc.id, doc.path],
54
66
  });
55
- if (existingDocRow.rows.length > 0) {
56
- const realDocId = existingDocRow.rows[0].id;
57
- await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);", args: [realDocId] });
67
+ if (existingDocRow.rows.length > 0) {
68
+ const realDocId = existingDocRow.rows[0].id;
69
+ await db.cloudClient.execute({
70
+ sql: `DELETE FROM graph_edges
71
+ WHERE source_id = ? OR target_id = ?
72
+ OR target_id GLOB ?
73
+ OR source_id IN (SELECT id FROM sections WHERE doc_id = ?)
74
+ OR target_id IN (SELECT id FROM sections WHERE doc_id = ?)
75
+ OR source_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
76
+ OR target_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
77
+ OR source_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?)
78
+ OR target_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);`,
79
+ args: [realDocId, realDocId, `${realDocId}:L*`, realDocId, realDocId, realDocId, realDocId, realDocId, realDocId],
80
+ });
81
+ await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);", args: [realDocId] });
58
82
  await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks WHERE doc_id = ?;", args: [realDocId] });
59
83
  await db.cloudClient.execute({ sql: "DELETE FROM medium_chunks WHERE doc_id = ?;", args: [realDocId] });
60
84
  await db.cloudClient.execute({ sql: "DELETE FROM sections WHERE doc_id = ?;", args: [realDocId] });
61
- await db.cloudClient.execute({ sql: "DELETE FROM graph_edges WHERE source_id = ? OR target_id = ?;", args: [realDocId, realDocId] });
62
- await db.cloudClient.execute({ sql: "DELETE FROM knowledge_links WHERE doc_id = ?;", args: [realDocId] });
85
+ await db.cloudClient.execute({ sql: "DELETE FROM knowledge_links WHERE doc_id = ?;", args: [realDocId] });
63
86
  await db.cloudClient.execute({ sql: "DELETE FROM documents WHERE id = ?;", args: [realDocId] });
64
87
  }
65
88
 
@@ -79,8 +102,18 @@ async function processSyncTask(db, task) {
79
102
  doc.metadata_json ? (typeof doc.metadata_json === "string" ? doc.metadata_json : JSON.stringify(doc.metadata_json)) : null,
80
103
  doc.created_at,
81
104
  doc.updated_at,
82
- ],
83
- });
105
+ ],
106
+ });
107
+
108
+ const scopes = Array.isArray(data.document_scopes) && data.document_scopes.length
109
+ ? data.document_scopes
110
+ : [{ scope_key: "global", created_at: doc.created_at }];
111
+ for (const scope of scopes) {
112
+ await db.cloudClient.execute({
113
+ sql: "INSERT OR IGNORE INTO document_scopes (doc_id, scope_key, created_at) VALUES (?, ?, ?);",
114
+ args: [doc.id, scope.scope_key || "global", scope.created_at || Date.now()],
115
+ });
116
+ }
84
117
 
85
118
  // 3. Insert sections
86
119
  if (Array.isArray(data.sections)) {
@@ -112,8 +145,18 @@ async function processSyncTask(db, task) {
112
145
  ? Buffer.from(vecBytes.buffer, vecBytes.byteOffset, vecBytes.byteLength)
113
146
  : Buffer.alloc(0);
114
147
  await db.cloudClient.execute({
115
- sql: "INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id) VALUES (?, ?, ?, ?, ?, ?, ?);",
116
- args: [mc.id, mc.section_id, doc.id, mc.content, vecBuf, mc.token_count, mc.medium_id || null],
148
+ sql: "INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id, retrieval_policy, policy_source_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
149
+ args: [
150
+ mc.id,
151
+ mc.section_id,
152
+ doc.id,
153
+ mc.content,
154
+ vecBuf,
155
+ mc.token_count,
156
+ mc.medium_id || null,
157
+ mc.retrieval_policy || "micro_chunk",
158
+ mc.policy_source_id || null,
159
+ ],
117
160
  });
118
161
 
119
162
  // Insert into remote FTS
@@ -129,16 +172,38 @@ async function processSyncTask(db, task) {
129
172
  }
130
173
 
131
174
  // 6. Insert graph_edges
132
- if (Array.isArray(data.graph_edges)) {
175
+ if (Array.isArray(data.graph_edges)) {
133
176
  for (const e of data.graph_edges) {
134
177
  await db.cloudClient.execute({
135
178
  sql: "INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at) VALUES (?, ?, ?, ?, ?);",
136
179
  args: [e.source_id, e.target_id, e.relation_type, e.metadata_json ? (typeof e.metadata_json === "string" ? e.metadata_json : JSON.stringify(e.metadata_json)) : null, e.created_at || Date.now()],
137
180
  });
138
- }
139
- }
140
- }
141
- }
181
+ }
182
+ }
183
+
184
+ if (Array.isArray(data.knowledge_links)) {
185
+ for (const link of data.knowledge_links) {
186
+ await db.cloudClient.execute({
187
+ sql: `INSERT OR REPLACE INTO knowledge_links
188
+ (id, fact_key, fact_text, doc_id, section_id, start_line, end_line, relation_type, metadata_json, created_at)
189
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
190
+ args: [
191
+ link.id,
192
+ link.fact_key,
193
+ link.fact_text,
194
+ doc.id,
195
+ link.section_id || null,
196
+ link.start_line || null,
197
+ link.end_line || null,
198
+ link.relation_type || "LINKS_TO",
199
+ link.metadata_json || null,
200
+ link.created_at || Date.now(),
201
+ ],
202
+ });
203
+ }
204
+ }
205
+ }
206
+ }
142
207
 
143
208
  export async function enqueueSyncTask(action, keyOrId, payload = null) {
144
209
  const { getDatabase } = await import("./database.js");
@@ -317,9 +382,13 @@ export function resetReverseSyncThrottle() {
317
382
  lastReverseSync = 0;
318
383
  }
319
384
 
320
- export async function triggerBackgroundSync() {
321
- if (isSyncing) return;
322
- isSyncing = true;
385
+ export async function triggerBackgroundSync() {
386
+ if (isSyncing) {
387
+ syncRequested = true;
388
+ return;
389
+ }
390
+ isSyncing = true;
391
+ syncRequested = false;
323
392
 
324
393
  try {
325
394
  const { getDatabase } = await import("./database.js");
@@ -340,30 +409,35 @@ export async function triggerBackgroundSync() {
340
409
  );
341
410
  `);
342
411
 
343
- // Fetch tasks ordered by id
344
- const tasks = await db.prepare("SELECT * FROM sync_queue ORDER BY id ASC LIMIT 50;").all();
345
- if (tasks.length === 0) {
346
- isSyncing = false;
347
- return;
348
- }
349
-
350
- for (const task of tasks) {
351
- try {
352
- await processSyncTask(db, task);
353
- // On success, delete from queue
354
- await db.prepare("DELETE FROM sync_queue WHERE id = ?;").run(task.id);
355
- } catch (err) {
356
- console.error(`Failed to process sync task ${task.id} (${task.action}):`, err.message, err.stack);
357
- // Stop processing this batch to preserve order on error, retry next time
358
- break;
359
- }
360
- }
412
+ // Drain until empty. New tasks can be enqueued while a batch is running;
413
+ // stopping after one snapshot of the queue left those tasks stranded until
414
+ // an unrelated future write happened to wake the worker again.
415
+ let syncFailed = false;
416
+ while (!syncFailed) {
417
+ const tasks = await db.prepare("SELECT * FROM sync_queue ORDER BY id ASC LIMIT 50;").all();
418
+ if (tasks.length === 0) break;
419
+
420
+ for (const task of tasks) {
421
+ try {
422
+ await processSyncTask(db, task);
423
+ await db.prepare("DELETE FROM sync_queue WHERE id = ?;").run(task.id);
424
+ } catch (err) {
425
+ console.error(`Failed to process sync task ${task.id} (${task.action}):`, err.message, err.stack);
426
+ syncFailed = true;
427
+ break;
428
+ }
429
+ }
430
+ }
361
431
 
362
432
  // Push queue drained — now pull cloud state back down (reverse sync).
363
433
  await syncFromCloud();
364
434
  } catch (err) {
365
435
  console.error("Error during background sync execution:", err.message);
366
- } finally {
367
- isSyncing = false;
368
- }
369
- }
436
+ } finally {
437
+ isSyncing = false;
438
+ if (syncRequested) {
439
+ syncRequested = false;
440
+ await triggerBackgroundSync();
441
+ }
442
+ }
443
+ }
@@ -1,5 +1,18 @@
1
1
  import { getDatabase } from "../db/database.js";
2
- import { randomUUID } from "node:crypto";
2
+ import { randomUUID } from "node:crypto";
3
+ import { GLOBAL_KEY } from "../memory.js";
4
+
5
+ export async function queueDocumentSyncIfNeeded(db, docId) {
6
+ try {
7
+ const { getConfig } = await import("../config/config_manager.js");
8
+ if (getConfig().mode !== "hybrid-sync") return;
9
+ const { exportDocumentData } = await import("../ingest/exporter.js");
10
+ const { enqueueSyncTask } = await import("../db/sync_queue.js");
11
+ await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
12
+ } catch (err) {
13
+ console.warn(`Failed to queue linked RAG document sync for ${docId}: ${err.message}`);
14
+ }
15
+ }
3
16
 
4
17
  export async function linkFactToDocument({
5
18
  factKey,
@@ -15,9 +28,19 @@ export async function linkFactToDocument({
15
28
  const id = `link_${randomUUID().substring(0, 12)}`;
16
29
  const now = Date.now();
17
30
 
18
- const doc = await db
19
- .prepare("SELECT id, title, path FROM documents WHERE id = ? OR path = ? OR title = ?")
20
- .get(docId, docId, docId);
31
+ const allowedScopes = factKey === GLOBAL_KEY ? [GLOBAL_KEY] : [GLOBAL_KEY, factKey];
32
+ const placeholders = allowedScopes.map(() => "?").join(",");
33
+ const doc = await db
34
+ .prepare(`
35
+ SELECT d.id, d.title, d.path
36
+ FROM documents d
37
+ WHERE (d.id = ? OR d.path = ? OR d.title = ?)
38
+ AND EXISTS (
39
+ SELECT 1 FROM document_scopes ds
40
+ WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders})
41
+ )
42
+ `)
43
+ .get(docId, docId, docId, ...allowedScopes);
21
44
 
22
45
  if (!doc) {
23
46
  throw new Error(`Target document not found in knowledge base for link: ${docId}`);
@@ -46,7 +69,8 @@ export async function linkFactToDocument({
46
69
  VALUES (?, ?, ?, ?, ?)
47
70
  `);
48
71
  const targetSpec = startLine ? `${doc.id}:L${startLine}-${endLine || startLine}` : doc.id;
49
- await edgeStmt.run(`fact:${factKey}:${factText.substring(0, 30)}`, targetSpec, relationType, JSON.stringify({ linkId: id }), now);
72
+ await edgeStmt.run(`fact:${factKey}:${factText.substring(0, 30)}`, targetSpec, relationType, JSON.stringify({ linkId: id }), now);
73
+ await queueDocumentSyncIfNeeded(db, doc.id);
50
74
 
51
75
  return {
52
76
  linkId: id,
@@ -73,19 +97,33 @@ export async function getLinksForFact(factKey, factText) {
73
97
  return await stmt.all(factKey, queryPattern, factText);
74
98
  }
75
99
 
76
- export async function getLinksForDoc(docId) {
77
- const db = await getDatabase();
78
- const stmt = db.prepare(`
79
- SELECT k.*, d.title as doc_title, d.path as doc_path
80
- FROM knowledge_links k
81
- JOIN documents d ON k.doc_id = d.id
82
- WHERE k.doc_id = ? OR d.path = ? OR d.title = ?
83
- ORDER BY k.created_at DESC
84
- `);
85
- return await stmt.all(docId, docId, docId);
86
- }
100
+ export async function getLinksForDoc(docId, scopeKeys = null) {
101
+ const db = await getDatabase();
102
+ const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
103
+ const scopeClause = scoped
104
+ ? `AND k.fact_key IN (${scopeKeys.map(() => "?").join(",")})
105
+ AND EXISTS (
106
+ SELECT 1 FROM document_scopes ds
107
+ WHERE ds.doc_id = d.id AND ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
108
+ )`
109
+ : "";
110
+ const stmt = db.prepare(`
111
+ SELECT k.*, d.title as doc_title, d.path as doc_path
112
+ FROM knowledge_links k
113
+ JOIN documents d ON k.doc_id = d.id
114
+ WHERE (k.doc_id = ? OR d.path = ? OR d.title = ?)
115
+ ${scopeClause}
116
+ ORDER BY k.created_at DESC
117
+ `);
118
+ return await stmt.all(
119
+ docId,
120
+ docId,
121
+ docId,
122
+ ...(scoped ? [...scopeKeys, ...scopeKeys] : [])
123
+ );
124
+ }
87
125
 
88
- export async function listAllLinks(factKey = null) {
126
+ export async function listAllLinks(factKey = null) {
89
127
  const db = await getDatabase();
90
128
  let sql = `
91
129
  SELECT k.*, d.title as doc_title, d.path as doc_path
@@ -98,5 +136,74 @@ export async function listAllLinks(factKey = null) {
98
136
  params.push(factKey);
99
137
  }
100
138
  sql += " ORDER BY k.created_at DESC";
101
- return await db.prepare(sql).all(...params);
102
- }
139
+ return await db.prepare(sql).all(...params);
140
+ }
141
+
142
+ export async function moveKnowledgeScope(db, sourceKey, targetKey) {
143
+ if (!sourceKey || !targetKey || sourceKey === targetKey) {
144
+ return { movedLinks: 0, movedDocuments: 0 };
145
+ }
146
+
147
+ const links = await db.prepare("SELECT * FROM knowledge_links WHERE fact_key = ?").all(sourceKey);
148
+ const scopedDocs = await db.prepare("SELECT doc_id, created_at FROM document_scopes WHERE scope_key = ?").all(sourceKey);
149
+ const docIds = new Set([...scopedDocs.map((row) => row.doc_id), ...links.map((link) => link.doc_id)]);
150
+
151
+ for (const docId of docIds) {
152
+ await db
153
+ .prepare("INSERT OR IGNORE INTO document_scopes (doc_id, scope_key, created_at) VALUES (?, ?, ?)")
154
+ .run(docId, targetKey, Date.now());
155
+ }
156
+ await db.prepare("DELETE FROM document_scopes WHERE scope_key = ?").run(sourceKey);
157
+
158
+ for (const link of links) {
159
+ const oldSource = `fact:${sourceKey}:${link.fact_text.substring(0, 30)}`;
160
+ const targetSpec = link.start_line
161
+ ? `${link.doc_id}:L${link.start_line}-${link.end_line || link.start_line}`
162
+ : link.doc_id;
163
+ await db
164
+ .prepare("DELETE FROM graph_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?")
165
+ .run(oldSource, targetSpec, link.relation_type || "LINKS_TO");
166
+ const newSource = `fact:${targetKey}:${link.fact_text.substring(0, 30)}`;
167
+ await db.prepare(`
168
+ INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at)
169
+ VALUES (?, ?, ?, ?, ?)
170
+ `).run(
171
+ newSource,
172
+ targetSpec,
173
+ link.relation_type || "LINKS_TO",
174
+ JSON.stringify({ linkId: link.id }),
175
+ link.created_at || Date.now()
176
+ );
177
+ }
178
+ await db.prepare("UPDATE knowledge_links SET fact_key = ? WHERE fact_key = ?").run(targetKey, sourceKey);
179
+
180
+ for (const docId of docIds) {
181
+ await queueDocumentSyncIfNeeded(db, docId);
182
+ }
183
+
184
+ return { movedLinks: links.length, movedDocuments: docIds.size };
185
+ }
186
+
187
+ export async function deleteLinksForFacts(db, factKey, factTexts) {
188
+ const texts = [...new Set((factTexts || []).filter(Boolean))];
189
+ if (!factKey || texts.length === 0) return { deletedLinks: 0, affectedDocuments: 0 };
190
+ const placeholders = texts.map(() => "?").join(",");
191
+ const links = await db
192
+ .prepare(`SELECT * FROM knowledge_links WHERE fact_key = ? AND fact_text IN (${placeholders})`)
193
+ .all(factKey, ...texts);
194
+ for (const link of links) {
195
+ const source = `fact:${factKey}:${link.fact_text.substring(0, 30)}`;
196
+ const target = link.start_line
197
+ ? `${link.doc_id}:L${link.start_line}-${link.end_line || link.start_line}`
198
+ : link.doc_id;
199
+ await db
200
+ .prepare("DELETE FROM graph_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?")
201
+ .run(source, target, link.relation_type || "LINKS_TO");
202
+ }
203
+ await db
204
+ .prepare(`DELETE FROM knowledge_links WHERE fact_key = ? AND fact_text IN (${placeholders})`)
205
+ .run(factKey, ...texts);
206
+ const docIds = [...new Set(links.map((link) => link.doc_id))];
207
+ for (const docId of docIds) await queueDocumentSyncIfNeeded(db, docId);
208
+ return { deletedLinks: links.length, affectedDocuments: docIds.length };
209
+ }
@@ -1,7 +1,8 @@
1
1
  import { getDatabase } from "../db/database.js";
2
2
  import { writeFileSync, mkdirSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { MEMORY_DIR } from "../memory.js";
4
+ import { MEMORY_DIR } from "../memory.js";
5
+ import { toVectorBytes } from "../retrieval/retriever.js";
5
6
 
6
7
  export const EXPORTS_DIR = join(MEMORY_DIR, "exports");
7
8
 
@@ -35,8 +36,32 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
35
36
 
36
37
  const sections = await db.prepare("SELECT id, heading, breadcrumbs, content, token_count FROM sections WHERE doc_id = ?").all(doc.id);
37
38
  const mediumChunks = await db.prepare("SELECT id, section_id, content, block_type, token_count, created_at FROM medium_chunks WHERE doc_id = ?").all(doc.id);
38
- const microChunks = await db.prepare("SELECT id, medium_id, section_id, content, token_count FROM micro_chunks WHERE doc_id = ?").all(doc.id);
39
- const graphEdges = await db.prepare("SELECT source_id, target_id, relation_type, metadata_json FROM graph_edges WHERE source_id = ? OR target_id = ?").all(doc.id, doc.id);
39
+ const rawMicroChunks = await db.prepare("SELECT id, medium_id, section_id, content, vector, token_count, retrieval_policy, policy_source_id FROM micro_chunks WHERE doc_id = ?").all(doc.id);
40
+ const microChunks = rawMicroChunks.map((chunk) => {
41
+ const bytes = toVectorBytes(chunk.vector);
42
+ return {
43
+ ...chunk,
44
+ vector: bytes && bytes.byteLength
45
+ ? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64")
46
+ : "",
47
+ };
48
+ });
49
+ const ownedRows = await db.prepare(`
50
+ SELECT id FROM sections WHERE doc_id = ?
51
+ UNION SELECT id FROM medium_chunks WHERE doc_id = ?
52
+ UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
53
+ `).all(doc.id, doc.id, doc.id);
54
+ const ownedIds = [doc.id, ...ownedRows.map((row) => row.id)];
55
+ const placeholders = ownedIds.map(() => "?").join(",");
56
+ const graphEdges = await db.prepare(`
57
+ SELECT source_id, target_id, relation_type, metadata_json, created_at
58
+ FROM graph_edges
59
+ WHERE source_id IN (${placeholders})
60
+ OR target_id IN (${placeholders})
61
+ OR target_id GLOB ?;
62
+ `).all(...ownedIds, ...ownedIds, `${doc.id}:L*`);
63
+ const knowledgeLinks = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(doc.id);
64
+ const documentScopes = await db.prepare("SELECT scope_key, created_at FROM document_scopes WHERE doc_id = ?").all(doc.id);
40
65
 
41
66
  return {
42
67
  document: {
@@ -44,9 +69,11 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
44
69
  path: doc.path,
45
70
  title: doc.title,
46
71
  blob_hash: doc.blob_hash,
47
- checksum: doc.checksum,
48
- toc,
49
- metadata,
72
+ checksum: doc.checksum,
73
+ toc_json: doc.toc_json,
74
+ metadata_json: doc.metadata_json,
75
+ toc,
76
+ metadata,
50
77
  created_at: doc.created_at,
51
78
  updated_at: doc.updated_at,
52
79
  },
@@ -59,9 +86,11 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
59
86
  sections,
60
87
  medium_chunks: mediumChunks,
61
88
  micro_chunks: microChunks,
62
- graph_edges: graphEdges,
63
- };
64
- }
89
+ graph_edges: graphEdges,
90
+ knowledge_links: knowledgeLinks,
91
+ document_scopes: documentScopes,
92
+ };
93
+ }
65
94
 
66
95
  export async function exportDocumentToJsonString(docIdOrPath, customDb = null) {
67
96
  const data = await exportDocumentData(docIdOrPath, customDb);