@lotargo/memory_plugin 1.3.2 → 1.4.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.
- package/README.md +334 -324
- package/mcp-server/admin/auth.js +385 -0
- package/mcp-server/admin/snapshot.js +34 -32
- package/mcp-server/cli.js +108 -12
- package/mcp-server/config/auth_store.js +137 -0
- package/mcp-server/config/config_manager.js +5 -0
- package/mcp-server/db/database.js +198 -14
- package/mcp-server/db/migrations.js +62 -33
- package/mcp-server/db/sync_queue.js +211 -0
- package/mcp-server/graph/graph_extractor.js +40 -17
- package/mcp-server/graph/knowledge_linker.js +14 -14
- package/mcp-server/index.js +34 -23
- package/mcp-server/ingest/exporter.js +12 -12
- package/mcp-server/ingest/normalizer.js +102 -5
- package/mcp-server/ingest/pipeline.js +53 -29
- package/mcp-server/memory.js +73 -0
- package/mcp-server/retrieval/retriever.js +24 -15
- package/package.json +6 -2
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { saveSecrets, deleteSecrets } from "../config/auth_store.js";
|
|
5
|
+
import { updateConfig } from "../config/config_manager.js";
|
|
6
|
+
|
|
7
|
+
export const TURSO_API_BASE = () => process.env.TURSO_API_BASE || "https://api.turso.tech";
|
|
8
|
+
|
|
9
|
+
function openBrowser(url) {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
try {
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
|
|
14
|
+
} else if (platform === "darwin") {
|
|
15
|
+
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
16
|
+
} else {
|
|
17
|
+
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
18
|
+
}
|
|
19
|
+
} catch (err) {
|
|
20
|
+
// Browser auto-open is best-effort; the printed URL can be opened manually.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Starts a temporary loopback HTTP server to receive the OAuth callback.
|
|
25
|
+
// Turso redirects the browser back to the root path: /?jwt=<JWT>&username=<USERNAME>
|
|
26
|
+
export function startAuthLoopbackServer(port = 48900) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const server = http.createServer((req, res) => {
|
|
29
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
30
|
+
const token = url.searchParams.get("jwt") || url.searchParams.get("token");
|
|
31
|
+
const username = url.searchParams.get("username");
|
|
32
|
+
const error = url.searchParams.get("error");
|
|
33
|
+
|
|
34
|
+
if (token) {
|
|
35
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
36
|
+
res.end(`
|
|
37
|
+
<!DOCTYPE html>
|
|
38
|
+
<html lang="en">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="utf-8" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
|
+
<title>Authorization Successful</title>
|
|
43
|
+
<style>
|
|
44
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
45
|
+
body {
|
|
46
|
+
min-height: 100vh;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
51
|
+
-webkit-font-smoothing: antialiased;
|
|
52
|
+
background: radial-gradient(1200px 600px at 50% -10%, #1c2028 0%, #101218 55%, #0c0e13 100%);
|
|
53
|
+
color: #e8ebf2;
|
|
54
|
+
padding: 24px;
|
|
55
|
+
}
|
|
56
|
+
.card {
|
|
57
|
+
max-width: 420px;
|
|
58
|
+
width: 100%;
|
|
59
|
+
background: #161a21;
|
|
60
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
61
|
+
border-radius: 20px;
|
|
62
|
+
padding: 46px 38px;
|
|
63
|
+
text-align: center;
|
|
64
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
|
65
|
+
}
|
|
66
|
+
.badge {
|
|
67
|
+
width: 76px;
|
|
68
|
+
height: 76px;
|
|
69
|
+
margin: 0 auto 26px;
|
|
70
|
+
border-radius: 50%;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
background: rgba(94, 224, 154, 0.10);
|
|
75
|
+
border: 1px solid rgba(94, 224, 154, 0.28);
|
|
76
|
+
}
|
|
77
|
+
.badge svg { width: 36px; height: 36px; }
|
|
78
|
+
h1 { font-size: 22px; font-weight: 600; letter-spacing: 0.2px; color: #f2f4f8; margin-bottom: 12px; }
|
|
79
|
+
p { font-size: 14px; line-height: 1.65; color: #9aa3b2; }
|
|
80
|
+
.hint { margin-top: 24px; font-size: 12.5px; color: #6f7887; }
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
<div class="card">
|
|
85
|
+
<div class="badge">
|
|
86
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="#5ee09a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
87
|
+
<path d="M20 6 9 17l-5-5" />
|
|
88
|
+
</svg>
|
|
89
|
+
</div>
|
|
90
|
+
<h1>Authorization successful</h1>
|
|
91
|
+
<p>Your credentials were received and stored securely on this device.</p>
|
|
92
|
+
<div class="hint">You can now close this tab and return to the terminal.</div>
|
|
93
|
+
</div>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
server.close(() => {
|
|
99
|
+
resolve({ token, username: username || "" });
|
|
100
|
+
});
|
|
101
|
+
} else if (error) {
|
|
102
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
103
|
+
res.end(`
|
|
104
|
+
<!DOCTYPE html>
|
|
105
|
+
<html lang="en">
|
|
106
|
+
<head>
|
|
107
|
+
<meta charset="utf-8" />
|
|
108
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
109
|
+
<title>Authorization Failed</title>
|
|
110
|
+
<style>
|
|
111
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
112
|
+
body {
|
|
113
|
+
min-height: 100vh;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
118
|
+
-webkit-font-smoothing: antialiased;
|
|
119
|
+
background: radial-gradient(1200px 600px at 50% -10%, #1c2028 0%, #101218 55%, #0c0e13 100%);
|
|
120
|
+
color: #e8ebf2;
|
|
121
|
+
padding: 24px;
|
|
122
|
+
}
|
|
123
|
+
.card {
|
|
124
|
+
max-width: 400px;
|
|
125
|
+
width: 100%;
|
|
126
|
+
background: #161a21;
|
|
127
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
128
|
+
border-radius: 20px;
|
|
129
|
+
padding: 40px 34px;
|
|
130
|
+
text-align: center;
|
|
131
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
|
132
|
+
}
|
|
133
|
+
h1 { font-size: 20px; font-weight: 600; color: #f2f4f8; margin-bottom: 12px; }
|
|
134
|
+
p { font-size: 14px; line-height: 1.65; color: #9aa3b2; }
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<div class="card">
|
|
139
|
+
<h1>Authorization failed</h1>
|
|
140
|
+
<p>An error occurred during the login flow. Close this tab, return to the terminal, and try again.</p>
|
|
141
|
+
</div>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
`);
|
|
145
|
+
server.close(() => reject(new Error(`Authentication error: ${error}`)));
|
|
146
|
+
} else {
|
|
147
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
148
|
+
res.end("Not Found");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
server.on("error", (err) => {
|
|
153
|
+
reject(err);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
server.listen(port, "127.0.0.1", () => {
|
|
157
|
+
console.log(`\n [*] Waiting for authorization on local port http://localhost:${port}/...`);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function apiRequest(token, pathname, { method = "GET", body } = {}) {
|
|
163
|
+
const res = await fetch(`${TURSO_API_BASE()}${pathname}`, {
|
|
164
|
+
method,
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: `Bearer ${token}`,
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
Accept: "application/json",
|
|
169
|
+
},
|
|
170
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const text = await res.text();
|
|
174
|
+
let data = null;
|
|
175
|
+
try {
|
|
176
|
+
data = JSON.parse(text);
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const err = new Error(data?.error || `Turso API ${res.status}: ${text}`);
|
|
181
|
+
err.status = res.status;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate the account JWT obtained from OAuth and return current-user info.
|
|
188
|
+
export async function validateTursoToken(token) {
|
|
189
|
+
return apiRequest(token, "/v1/current-user");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function listOrganizations(token) {
|
|
193
|
+
const data = await apiRequest(token, "/v1/organizations");
|
|
194
|
+
const orgs = data?.organizations || [];
|
|
195
|
+
return orgs.map((o) => ({
|
|
196
|
+
slug: o.slug || o.Slug || o.id || o.Id || null,
|
|
197
|
+
name: o.name || o.Name || null,
|
|
198
|
+
id: o.id || o.Id || null,
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function listDatabases(token, org) {
|
|
203
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/databases`);
|
|
204
|
+
const dbs = data?.databases || [];
|
|
205
|
+
return dbs.map((d) => ({
|
|
206
|
+
name: d.name || d.Name,
|
|
207
|
+
hostname: d.hostname || d.Hostname,
|
|
208
|
+
id: d.id || d.Id,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function createDatabase(token, org, name) {
|
|
213
|
+
try {
|
|
214
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/databases`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: { name },
|
|
217
|
+
});
|
|
218
|
+
const d = data?.database || data;
|
|
219
|
+
return { name: d.name || d.Name, hostname: d.hostname || d.Hostname, id: d.id || d.Id };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// Fresh accounts have no default group; create one, then retry.
|
|
222
|
+
if (!String(err.message || "").toLowerCase().includes("group")) {
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
console.log(` [CLOUD] No group found. Creating group "default"...`);
|
|
226
|
+
await createGroup(token, org, "default");
|
|
227
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/databases`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
body: { name, group: "default" },
|
|
230
|
+
});
|
|
231
|
+
const d = data?.database || data;
|
|
232
|
+
return { name: d.name || d.Name, hostname: d.hostname || d.Hostname, id: d.id || d.Id };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Turso's public closest-region endpoint (no auth required).
|
|
237
|
+
// Returns e.g. { server: "aws-eu-west-1", client: "ams" }.
|
|
238
|
+
async function getClosestLocation() {
|
|
239
|
+
if (process.env.TURSO_LOCATION) return process.env.TURSO_LOCATION;
|
|
240
|
+
const fallback = "ams";
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch("https://region.turso.io/", { signal: AbortSignal.timeout(8000) });
|
|
243
|
+
const data = await res.json().catch(() => null);
|
|
244
|
+
const loc = data?.server || data?.client || null;
|
|
245
|
+
if (loc && /^[a-z0-9-]+$/i.test(loc)) return loc;
|
|
246
|
+
} catch {
|
|
247
|
+
// ignore
|
|
248
|
+
}
|
|
249
|
+
return fallback;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function createGroup(token, org, name) {
|
|
253
|
+
// Always provide an explicit location: Turso's internal auto-lookup fails
|
|
254
|
+
// with "invalid location: Host not found" when the group has no location.
|
|
255
|
+
const location = await getClosestLocation();
|
|
256
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/groups`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
body: { name, location },
|
|
259
|
+
});
|
|
260
|
+
return data?.group || data;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function createDatabaseToken(token, org, db, { expiration = "never", authorization = "full-access" } = {}) {
|
|
264
|
+
const data = await apiRequest(
|
|
265
|
+
token,
|
|
266
|
+
`/v1/organizations/${encodeURIComponent(org)}/databases/${encodeURIComponent(db)}/auth/tokens?expiration=${encodeURIComponent(expiration)}&authorization=${encodeURIComponent(authorization)}`,
|
|
267
|
+
{ method: "POST", body: {} }
|
|
268
|
+
);
|
|
269
|
+
return data?.jwt || null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function dbHostname(org, dbName) {
|
|
273
|
+
return `${dbName}-${org}.turso.io`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Perform the full cloud login flow:
|
|
277
|
+
// 1. OAuth browser flow against Turso (api.turso.tech).
|
|
278
|
+
// 2. Validate the received account JWT.
|
|
279
|
+
// 3. Resolve an organization and pick/create a database.
|
|
280
|
+
// 4. Mint a full-access token for that database.
|
|
281
|
+
// 5. Persist the encrypted token + dbUrl and mark the session as authorized.
|
|
282
|
+
export async function loginToCloud({
|
|
283
|
+
customPort = 48900,
|
|
284
|
+
simulated = false,
|
|
285
|
+
simulatedParams = null,
|
|
286
|
+
autoCreate = true,
|
|
287
|
+
databaseName = null,
|
|
288
|
+
} = {}) {
|
|
289
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
290
|
+
const loginUrl = `${TURSO_API_BASE()}/?port=${customPort}&redirect=true&state=${state}&type=cli`;
|
|
291
|
+
|
|
292
|
+
console.log(`\n [CLOUD] Please open your system browser to authorize:`);
|
|
293
|
+
console.log(` \x1b[36m${loginUrl}\x1b[0m\n`);
|
|
294
|
+
|
|
295
|
+
let received;
|
|
296
|
+
if (simulated && simulatedParams) {
|
|
297
|
+
received = await new Promise((resolve, reject) => {
|
|
298
|
+
const serverPromise = startAuthLoopbackServer(customPort);
|
|
299
|
+
const req = http.request(
|
|
300
|
+
`http://127.0.0.1:${customPort}/?jwt=${encodeURIComponent(simulatedParams.jwt)}&username=${encodeURIComponent(simulatedParams.username)}`,
|
|
301
|
+
{ method: "GET" },
|
|
302
|
+
(res) => {
|
|
303
|
+
res.resume();
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
req.on("error", (e) => reject(e));
|
|
307
|
+
req.end();
|
|
308
|
+
serverPromise.then(resolve).catch(reject);
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
openBrowser(loginUrl);
|
|
312
|
+
received = await startAuthLoopbackServer(customPort);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const { token, username } = received;
|
|
316
|
+
|
|
317
|
+
// Step 2: validate the account token
|
|
318
|
+
let userInfo = null;
|
|
319
|
+
try {
|
|
320
|
+
userInfo = await validateTursoToken(token);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
throw new Error(`Token validation failed: ${err.message}`);
|
|
323
|
+
}
|
|
324
|
+
const accountUsername = username || userInfo?.username || userInfo?.name || "user";
|
|
325
|
+
console.log(` [OK] Token is valid. User: ${accountUsername}`);
|
|
326
|
+
|
|
327
|
+
// Step 3: resolve organization + database
|
|
328
|
+
const orgs = await listOrganizations(token);
|
|
329
|
+
let org;
|
|
330
|
+
let orgName;
|
|
331
|
+
if (orgs && orgs.length > 0) {
|
|
332
|
+
org = orgs[0].slug || orgs[0].name || orgs[0].id || String(orgs[0]);
|
|
333
|
+
orgName = orgs[0].name || org;
|
|
334
|
+
} else {
|
|
335
|
+
// Personal accounts are not listed in /v1/organizations, but their own
|
|
336
|
+
// username acts as the organization namespace in the Platform API.
|
|
337
|
+
org = accountUsername;
|
|
338
|
+
orgName = accountUsername;
|
|
339
|
+
console.log(` [CLOUD] No organizations found. Using personal account "${org}" as the database namespace.`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const dbs = await listDatabases(token, org);
|
|
343
|
+
if (dbs.length > 0) {
|
|
344
|
+
console.log(`\n [CLOUD] Databases in organization "${orgName}":`);
|
|
345
|
+
dbs.forEach((d, i) => console.log(` ${i + 1}. ${d.name}`));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let dbName = databaseName;
|
|
349
|
+
if (!dbName) {
|
|
350
|
+
if (dbs.length > 0) {
|
|
351
|
+
dbName = dbs[0].name;
|
|
352
|
+
console.log(`\n [CLOUD] Using existing database: "${dbName}"`);
|
|
353
|
+
} else if (autoCreate) {
|
|
354
|
+
dbName = `memory-${accountUsername}`;
|
|
355
|
+
console.log(`\n [CLOUD] No database found. Creating "${dbName}"...`);
|
|
356
|
+
await createDatabase(token, org, dbName);
|
|
357
|
+
console.log(` [OK] Database "${dbName}" created.`);
|
|
358
|
+
} else {
|
|
359
|
+
throw new Error("No databases found and autoCreate is disabled.");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Step 4: mint a full-access token for the database
|
|
364
|
+
console.log(" [CLOUD] Issuing database access token...");
|
|
365
|
+
const dbJwt = await createDatabaseToken(token, org, dbName);
|
|
366
|
+
if (!dbJwt) {
|
|
367
|
+
throw new Error("Failed to create database auth token.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const dbUrl = `libsql://${dbHostname(org, dbName)}`;
|
|
371
|
+
|
|
372
|
+
// Step 5: persist secrets and mark authorized
|
|
373
|
+
saveSecrets({ token: dbJwt, dbUrl, username: accountUsername, org, db: dbName, authorized: true });
|
|
374
|
+
updateConfig({ tursoUrl: dbUrl, authorized: true, username: accountUsername });
|
|
375
|
+
|
|
376
|
+
console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Endpoint: ${dbUrl}\x1b[0m`);
|
|
377
|
+
return { token: dbJwt, dbUrl, username: accountUsername, org, db: dbName, authorized: true };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Logout and reset configurations
|
|
381
|
+
export function logoutFromCloud() {
|
|
382
|
+
const deleted = deleteSecrets();
|
|
383
|
+
updateConfig({ tursoUrl: "", mode: "only-local", authorized: false, username: "" });
|
|
384
|
+
return deleted;
|
|
385
|
+
}
|
|
@@ -36,13 +36,13 @@ export function listAvailableSnapshots() {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, outputPath = null } = {}) {
|
|
39
|
-
const db = customDb || getDatabase();
|
|
39
|
+
const db = customDb || await getDatabase();
|
|
40
40
|
|
|
41
|
-
const documents = db.prepare("SELECT * FROM documents").all();
|
|
42
|
-
const sections = db.prepare("SELECT * FROM sections").all();
|
|
43
|
-
const mediumChunks = db.prepare("SELECT * FROM medium_chunks").all();
|
|
44
|
-
const rawMicroChunks = db.prepare("SELECT * FROM micro_chunks").all();
|
|
45
|
-
const graphEdges = db.prepare("SELECT * FROM graph_edges").all();
|
|
41
|
+
const documents = await db.prepare("SELECT * FROM documents").all();
|
|
42
|
+
const sections = await db.prepare("SELECT * FROM sections").all();
|
|
43
|
+
const mediumChunks = await db.prepare("SELECT * FROM medium_chunks").all();
|
|
44
|
+
const rawMicroChunks = await db.prepare("SELECT * FROM micro_chunks").all();
|
|
45
|
+
const graphEdges = await db.prepare("SELECT * FROM graph_edges").all();
|
|
46
46
|
|
|
47
47
|
const microChunks = rawMicroChunks.map((mc) => {
|
|
48
48
|
let vecBase64 = "";
|
|
@@ -92,7 +92,7 @@ export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, snapshotPathOrData } = {}) {
|
|
95
|
-
const db = customDb || getDatabase();
|
|
95
|
+
const db = customDb || await getDatabase();
|
|
96
96
|
let snapshot;
|
|
97
97
|
|
|
98
98
|
if (typeof snapshotPathOrData === "string") {
|
|
@@ -181,11 +181,11 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
181
181
|
ON CONFLICT(source_id, target_id, relation_type) DO NOTHING
|
|
182
182
|
`);
|
|
183
183
|
|
|
184
|
-
db.exec("BEGIN IMMEDIATE;");
|
|
184
|
+
await db.exec("BEGIN IMMEDIATE;");
|
|
185
185
|
try {
|
|
186
186
|
if (Array.isArray(snapshot.documents)) {
|
|
187
187
|
for (const d of snapshot.documents) {
|
|
188
|
-
insertDoc.run(
|
|
188
|
+
await insertDoc.run(
|
|
189
189
|
d.id,
|
|
190
190
|
d.path,
|
|
191
191
|
d.blob_hash,
|
|
@@ -201,13 +201,13 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
201
201
|
|
|
202
202
|
if (Array.isArray(snapshot.sections)) {
|
|
203
203
|
for (const s of snapshot.sections) {
|
|
204
|
-
insertSection.run(s.id, s.doc_id, s.heading, s.breadcrumbs, s.content, s.token_count);
|
|
204
|
+
await insertSection.run(s.id, s.doc_id, s.heading, s.breadcrumbs, s.content, s.token_count);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
if (Array.isArray(snapshot.medium_chunks)) {
|
|
209
209
|
for (const m of snapshot.medium_chunks) {
|
|
210
|
-
insertMedium.run(m.id, m.section_id, m.doc_id, m.content, m.block_type, m.token_count, m.created_at || Date.now());
|
|
210
|
+
await insertMedium.run(m.id, m.section_id, m.doc_id, m.content, m.block_type, m.token_count, m.created_at || Date.now());
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -217,23 +217,23 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
217
217
|
if (mc.vector) {
|
|
218
218
|
vecBuf = Buffer.from(mc.vector, "base64");
|
|
219
219
|
}
|
|
220
|
-
insertChunk.run(mc.id, mc.section_id, mc.doc_id, mc.content, vecBuf, mc.token_count, mc.medium_id || null);
|
|
220
|
+
await insertChunk.run(mc.id, mc.section_id, mc.doc_id, mc.content, vecBuf, mc.token_count, mc.medium_id || null);
|
|
221
221
|
|
|
222
222
|
try {
|
|
223
|
-
deleteFts.run(mc.id);
|
|
223
|
+
await deleteFts.run(mc.id);
|
|
224
224
|
} catch {}
|
|
225
|
-
insertFts.run(mc.id, mc.content, mc.breadcrumbs || "");
|
|
225
|
+
await insertFts.run(mc.id, mc.content, mc.breadcrumbs || "");
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
if (Array.isArray(snapshot.graph_edges)) {
|
|
230
230
|
for (const e of snapshot.graph_edges) {
|
|
231
|
-
insertEdge.run(e.source_id, e.target_id, e.relation_type);
|
|
231
|
+
await insertEdge.run(e.source_id, e.target_id, e.relation_type);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
db.exec("COMMIT;");
|
|
234
|
+
await db.exec("COMMIT;");
|
|
235
235
|
} catch (err) {
|
|
236
|
-
db.exec("ROLLBACK;");
|
|
236
|
+
await db.exec("ROLLBACK;");
|
|
237
237
|
throw err;
|
|
238
238
|
}
|
|
239
239
|
|
|
@@ -247,30 +247,32 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
export function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR } = {}) {
|
|
251
|
-
const db = customDb || getDatabase();
|
|
250
|
+
export async function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR } = {}) {
|
|
251
|
+
const db = customDb || await getDatabase();
|
|
252
252
|
|
|
253
253
|
let docCount = 0;
|
|
254
254
|
let chunkCount = 0;
|
|
255
255
|
let blobCount = 0;
|
|
256
256
|
|
|
257
257
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
const docRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
|
|
259
|
+
docCount = docRow ? docRow.cnt : 0;
|
|
260
|
+
const chunkRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
|
|
261
|
+
chunkCount = chunkRow ? chunkRow.cnt : 0;
|
|
260
262
|
} catch {}
|
|
261
263
|
|
|
262
|
-
db.exec("BEGIN IMMEDIATE;");
|
|
264
|
+
await db.exec("BEGIN IMMEDIATE;");
|
|
263
265
|
try {
|
|
264
|
-
try { db.exec("DELETE FROM micro_chunks_fts;"); } catch {}
|
|
265
|
-
try { db.exec("DELETE FROM micro_chunks;"); } catch {}
|
|
266
|
-
try { db.exec("DELETE FROM medium_chunks;"); } catch {}
|
|
267
|
-
try { db.exec("DELETE FROM sections;"); } catch {}
|
|
268
|
-
try { db.exec("DELETE FROM graph_edges;"); } catch {}
|
|
269
|
-
try { db.exec("DELETE FROM knowledge_links;"); } catch {}
|
|
270
|
-
try { db.exec("DELETE FROM documents;"); } catch {}
|
|
271
|
-
db.exec("COMMIT;");
|
|
266
|
+
try { await db.exec("DELETE FROM micro_chunks_fts;"); } catch {}
|
|
267
|
+
try { await db.exec("DELETE FROM micro_chunks;"); } catch {}
|
|
268
|
+
try { await db.exec("DELETE FROM medium_chunks;"); } catch {}
|
|
269
|
+
try { await db.exec("DELETE FROM sections;"); } catch {}
|
|
270
|
+
try { await db.exec("DELETE FROM graph_edges;"); } catch {}
|
|
271
|
+
try { await db.exec("DELETE FROM knowledge_links;"); } catch {}
|
|
272
|
+
try { await db.exec("DELETE FROM documents;"); } catch {}
|
|
273
|
+
await db.exec("COMMIT;");
|
|
272
274
|
} catch (err) {
|
|
273
|
-
db.exec("ROLLBACK;");
|
|
275
|
+
await db.exec("ROLLBACK;");
|
|
274
276
|
throw err;
|
|
275
277
|
}
|
|
276
278
|
|
|
@@ -292,7 +294,7 @@ export function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR }
|
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
try {
|
|
295
|
-
db.exec("VACUUM;");
|
|
297
|
+
await db.exec("VACUUM;");
|
|
296
298
|
} catch {}
|
|
297
299
|
|
|
298
300
|
return {
|