@lotargo/memory_plugin 1.4.620 → 1.5.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/README.md +352 -334
- package/mcp-server/admin/auth.js +293 -42
- package/mcp-server/cli/direct_commands.js +313 -0
- package/mcp-server/cli/handlers/cloud_actions.js +138 -0
- package/mcp-server/cli/handlers/diagnostics_actions.js +107 -0
- package/mcp-server/cli/handlers/engine_actions.js +214 -0
- package/mcp-server/cli/handlers/prompt_actions.js +24 -0
- package/mcp-server/cli/handlers/storage_actions.js +749 -0
- package/mcp-server/cli/quick_stats.js +39 -0
- package/mcp-server/cli/ui.js +565 -0
- package/mcp-server/cli.js +324 -1945
- package/mcp-server/config/auth_store.js +178 -19
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/database.js +18 -3
- package/mcp-server/db/migrations.js +28 -0
- package/mcp-server/fact_format.js +244 -177
- package/mcp-server/identity.js +152 -0
- package/mcp-server/index.js +42 -679
- package/mcp-server/memory.js +50 -63
- package/mcp-server/prompt_manager.js +1 -1
- package/mcp-server/setup.js +41 -0
- package/mcp-server/tools/helpers.js +39 -0
- package/mcp-server/tools/identity_tools.js +277 -0
- package/mcp-server/tools/index.js +9 -0
- package/mcp-server/tools/memory_tools.js +506 -0
- package/mcp-server/tools/rag_tools.js +235 -0
- package/opencode-plugin/index.js +460 -48
- package/package.json +7 -3
- package/skills/using-memory/SKILL.md +31 -14
- package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
- package/mcp-server/benchmarks/quality_evaluator.js +0 -600
- package/mcp-server/benchmarks/run_benchmarks.js +0 -347
- package/mcp-server/benchmarks/stress_ingestion.js +0 -195
- package/mcp-server/benchmarks/test_dual_layer.js +0 -140
|
@@ -7,10 +7,28 @@ import { MEMORY_DIR, ensureDirSync } from "../memory.js";
|
|
|
7
7
|
|
|
8
8
|
const SECRETS_FILE = path.join(MEMORY_DIR, "auth_secrets.enc");
|
|
9
9
|
|
|
10
|
+
// ── Module-level caches ─────────────────────────────────────────────────────
|
|
11
|
+
// These avoid re-running expensive sync operations (execSync, PBKDF2, file I/O)
|
|
12
|
+
// on every CLI navigation or tool call. invalidateAuthCache() must be called
|
|
13
|
+
// whenever secrets are deleted or the API key is removed.
|
|
14
|
+
let _cachedMachineId = undefined; // undefined = not yet resolved
|
|
15
|
+
let _cachedFingerprint = null;
|
|
16
|
+
let _cachedEncryptionKey = null;
|
|
17
|
+
let _cachedSecrets = undefined; // undefined = not loaded, null = no secrets, object = cached
|
|
18
|
+
let _cachedSecretsMtime = 0;
|
|
19
|
+
|
|
20
|
+
export function invalidateAuthCache() {
|
|
21
|
+
_cachedSecrets = undefined;
|
|
22
|
+
_cachedSecretsMtime = 0;
|
|
23
|
+
// Keep machineId / fingerprint / key cached — they don't change per-session.
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
// Stable per-machine identifier. Must NOT rely on volatile values (e.g.
|
|
11
27
|
// os.networkInterfaces() — VPN adapters, hotspot IPs and IPv6 privacy
|
|
12
28
|
// addresses rotate constantly and would silently change the AES key).
|
|
13
29
|
function getMachineId() {
|
|
30
|
+
if (_cachedMachineId !== undefined) return _cachedMachineId;
|
|
31
|
+
let id = null;
|
|
14
32
|
try {
|
|
15
33
|
if (process.platform === "win32") {
|
|
16
34
|
const out = execSync("reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", {
|
|
@@ -18,12 +36,12 @@ function getMachineId() {
|
|
|
18
36
|
stdio: ["ignore", "pipe", "ignore"],
|
|
19
37
|
});
|
|
20
38
|
const m = out.match(/MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]{36})/i);
|
|
21
|
-
if (m)
|
|
39
|
+
if (m) id = m[1].toLowerCase();
|
|
22
40
|
} else if (process.platform === "linux") {
|
|
23
41
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
24
42
|
try {
|
|
25
43
|
const v = fs.readFileSync(p, "utf8").trim();
|
|
26
|
-
if (v)
|
|
44
|
+
if (v) { id = v; break; }
|
|
27
45
|
} catch {}
|
|
28
46
|
}
|
|
29
47
|
} else if (process.platform === "darwin") {
|
|
@@ -32,15 +50,17 @@ function getMachineId() {
|
|
|
32
50
|
stdio: ["ignore", "pipe", "ignore"],
|
|
33
51
|
});
|
|
34
52
|
const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
35
|
-
if (m)
|
|
53
|
+
if (m) id = m[1];
|
|
36
54
|
}
|
|
37
55
|
} catch {}
|
|
38
|
-
|
|
56
|
+
_cachedMachineId = id;
|
|
57
|
+
return _cachedMachineId;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
// Generate a deterministic hardware + system fingerprint (stable across reboots,
|
|
42
61
|
// network changes and user sessions on the same machine).
|
|
43
62
|
function getSystemFingerprint() {
|
|
63
|
+
if (_cachedFingerprint !== null && _cachedFingerprint !== undefined) return _cachedFingerprint;
|
|
44
64
|
const parts = [
|
|
45
65
|
getMachineId() || "no-machine-id",
|
|
46
66
|
os.hostname() || "localhost",
|
|
@@ -48,15 +68,18 @@ function getSystemFingerprint() {
|
|
|
48
68
|
os.platform() || "unknown",
|
|
49
69
|
os.arch() || "unknown",
|
|
50
70
|
];
|
|
51
|
-
|
|
71
|
+
_cachedFingerprint = parts.join("|");
|
|
72
|
+
return _cachedFingerprint;
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
// Derive a 256-bit (32 bytes) key using PBKDF2 with salt derived from fingerprint
|
|
55
76
|
function deriveEncryptionKey() {
|
|
77
|
+
if (_cachedEncryptionKey) return _cachedEncryptionKey;
|
|
56
78
|
const fingerprint = getSystemFingerprint();
|
|
57
79
|
const salt = crypto.createHash("sha256").update(fingerprint).digest();
|
|
58
80
|
// PBKDF2 with 10,000 iterations to derive a secure 32-byte key
|
|
59
|
-
|
|
81
|
+
_cachedEncryptionKey = crypto.pbkdf2Sync(fingerprint, salt, 10000, 32, "sha256");
|
|
82
|
+
return _cachedEncryptionKey;
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
// Encrypt data using AES-256-GCM
|
|
@@ -95,39 +118,175 @@ export function decryptData(encryptedStr) {
|
|
|
95
118
|
return decrypted;
|
|
96
119
|
}
|
|
97
120
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
// Load a KEY=VALUE .env file from MEMORY_DIR (global environment override for
|
|
122
|
+
// headless / Docker / CI deployments). Values may optionally be quoted.
|
|
123
|
+
function loadEnvFile() {
|
|
124
|
+
const envFile = path.join(MEMORY_DIR, ".env");
|
|
125
|
+
if (!fs.existsSync(envFile)) return {};
|
|
126
|
+
try {
|
|
127
|
+
const out = {};
|
|
128
|
+
const raw = fs.readFileSync(envFile, "utf-8");
|
|
129
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
132
|
+
const eq = trimmed.indexOf("=");
|
|
133
|
+
if (eq <= 0) continue;
|
|
134
|
+
const key = trimmed.slice(0, eq).trim();
|
|
135
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
136
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
137
|
+
value = value.slice(1, -1);
|
|
138
|
+
}
|
|
139
|
+
if (key) out[key] = value;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
} catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
104
145
|
}
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
const ENV_DB_URL_KEYS = ["TURSO_DB_URL", "TURSO_URL"];
|
|
148
|
+
const ENV_DB_TOKEN_KEYS = ["TURSO_DB_TOKEN", "TURSO_TOKEN"];
|
|
149
|
+
|
|
150
|
+
function firstDefined(source, keys) {
|
|
151
|
+
for (const k of keys) {
|
|
152
|
+
const v = source[k];
|
|
153
|
+
if (v && String(v).trim()) return String(v).trim();
|
|
110
154
|
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Resolve cloud credentials from the environment (process.env) or a
|
|
159
|
+
// MEMORY_DIR/.env file — the headless alternative to browser OAuth login.
|
|
160
|
+
// Supported vars: TURSO_DB_URL / TURSO_URL, TURSO_DB_TOKEN / TURSO_TOKEN,
|
|
161
|
+
// TURSO_API_TOKEN, TURSO_ORG, TURSO_DATABASE / TURSO_DB_NAME, TURSO_USERNAME.
|
|
162
|
+
// Returns null when no cloud secrets are present at all.
|
|
163
|
+
export function resolveEnvSecrets() {
|
|
164
|
+
const fileVars = loadEnvFile();
|
|
165
|
+
const merged = { ...fileVars, ...process.env };
|
|
166
|
+
const dbUrl = firstDefined(merged, ENV_DB_URL_KEYS);
|
|
167
|
+
const token = firstDefined(merged, ENV_DB_TOKEN_KEYS);
|
|
168
|
+
const apiToken = firstDefined(merged, ["TURSO_API_TOKEN"]);
|
|
169
|
+
const org = firstDefined(merged, ["TURSO_ORG"]);
|
|
170
|
+
const database = firstDefined(merged, ["TURSO_DATABASE", "TURSO_DB_NAME"]);
|
|
171
|
+
const username = firstDefined(merged, ["TURSO_USERNAME"]);
|
|
172
|
+
if (!dbUrl && !token && !apiToken) return null;
|
|
173
|
+
return { dbUrl, token, apiToken, org, database, username, source: "env" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Read the encrypted store file ONLY (no environment merge). Returns the raw
|
|
177
|
+
// parsed record, or null when the file is missing / undecryptable.
|
|
178
|
+
function readStoredSecrets() {
|
|
179
|
+
try {
|
|
180
|
+
if (fs.existsSync(SECRETS_FILE)) {
|
|
181
|
+
const mtime = fs.statSync(SECRETS_FILE).mtimeMs;
|
|
182
|
+
if (_cachedSecrets !== undefined && mtime === _cachedSecretsMtime) {
|
|
183
|
+
return _cachedSecrets;
|
|
184
|
+
}
|
|
185
|
+
_cachedSecretsMtime = mtime;
|
|
186
|
+
} else {
|
|
187
|
+
// File doesn't exist — clear cache
|
|
188
|
+
if (_cachedSecrets !== undefined && _cachedSecrets === null) return null;
|
|
189
|
+
_cachedSecrets = null;
|
|
190
|
+
_cachedSecretsMtime = 0;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
} catch {}
|
|
111
194
|
try {
|
|
112
195
|
const encrypted = fs.readFileSync(SECRETS_FILE, "utf-8").trim();
|
|
113
|
-
if (!encrypted) return null;
|
|
114
|
-
|
|
115
|
-
return
|
|
196
|
+
if (!encrypted) { _cachedSecrets = null; return null; }
|
|
197
|
+
_cachedSecrets = JSON.parse(decryptData(encrypted));
|
|
198
|
+
return _cachedSecrets;
|
|
116
199
|
} catch (err) {
|
|
117
200
|
console.error(
|
|
118
201
|
"Failed to decrypt or load cloud secrets:",
|
|
119
202
|
err.message,
|
|
120
203
|
"— the file was encrypted with a different machine key. Re-run login to recreate it."
|
|
121
204
|
);
|
|
205
|
+
_cachedSecrets = null;
|
|
122
206
|
return null;
|
|
123
207
|
}
|
|
124
208
|
}
|
|
125
209
|
|
|
210
|
+
// Where are cloud credentials coming from right now?
|
|
211
|
+
// "env" — TURSO_API_TOKEN / TURSO_DB_URL + TURSO_DB_TOKEN from env or .env
|
|
212
|
+
// "api-key" — a stored Turso account API-token session (takes priority over browser)
|
|
213
|
+
// "store" — a stored browser OAuth / database-token session
|
|
214
|
+
// null — nothing configured
|
|
215
|
+
export function getSecretsSource() {
|
|
216
|
+
const envSecrets = resolveEnvSecrets();
|
|
217
|
+
if (envSecrets && (envSecrets.apiToken || envSecrets.dbUrl)) return "env";
|
|
218
|
+
const stored = readStoredSecrets();
|
|
219
|
+
if (stored) return stored.apiToken ? "api-key" : "store";
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Save secrets securely
|
|
224
|
+
export function saveSecrets(secrets) {
|
|
225
|
+
ensureDirSync();
|
|
226
|
+
const plainText = JSON.stringify(secrets);
|
|
227
|
+
const encrypted = encryptData(plainText);
|
|
228
|
+
fs.writeFileSync(SECRETS_FILE, encrypted, "utf-8");
|
|
229
|
+
_cachedSecrets = undefined; // invalidate so readStoredSecrets re-reads
|
|
230
|
+
_cachedSecretsMtime = 0;
|
|
231
|
+
if (typeof _onSecretsChanged === "function") _onSecretsChanged();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let _onSecretsChanged = null;
|
|
235
|
+
export function onSecretsChanged(cb) { _onSecretsChanged = cb; }
|
|
236
|
+
|
|
237
|
+
// Load secrets securely. Priority (highest first):
|
|
238
|
+
// 1. Env account API token (TURSO_API_TOKEN) — reused from the store when a
|
|
239
|
+
// session was already minted for this exact token, else returned with
|
|
240
|
+
// needsResolution: true so callers can mint a DB JWT asynchronously.
|
|
241
|
+
// 2. Env database URL + token (TURSO_DB_URL + TURSO_DB_TOKEN).
|
|
242
|
+
// 3. Encrypted store: an API-key session beats a browser/database-token session.
|
|
243
|
+
// The env sources let Docker, Google Jules and VPS deployments work without
|
|
244
|
+
// any interactive login step.
|
|
245
|
+
export function loadSecrets() {
|
|
246
|
+
const envSecrets = resolveEnvSecrets();
|
|
247
|
+
if (envSecrets && envSecrets.apiToken) {
|
|
248
|
+
const stored = readStoredSecrets();
|
|
249
|
+
if (stored && stored.apiToken === envSecrets.apiToken && stored.dbUrl) {
|
|
250
|
+
return { ...stored, source: "api-key" };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
token: envSecrets.apiToken,
|
|
254
|
+
apiToken: envSecrets.apiToken,
|
|
255
|
+
dbUrl: envSecrets.dbUrl || "",
|
|
256
|
+
org: envSecrets.org || "",
|
|
257
|
+
db: envSecrets.database || "",
|
|
258
|
+
username: envSecrets.username || "",
|
|
259
|
+
authorized: true,
|
|
260
|
+
source: "env",
|
|
261
|
+
needsResolution: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (envSecrets && envSecrets.dbUrl && envSecrets.token) {
|
|
265
|
+
return {
|
|
266
|
+
token: envSecrets.token,
|
|
267
|
+
dbUrl: envSecrets.dbUrl,
|
|
268
|
+
org: envSecrets.org || "",
|
|
269
|
+
db: envSecrets.database || "",
|
|
270
|
+
username: envSecrets.username || "",
|
|
271
|
+
authorized: true,
|
|
272
|
+
source: "env",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const stored = readStoredSecrets();
|
|
276
|
+
if (stored) {
|
|
277
|
+
if (stored.apiToken) return { ...stored, source: "api-key" };
|
|
278
|
+
return stored;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
126
283
|
// Delete secrets from disk
|
|
127
284
|
export function deleteSecrets() {
|
|
128
285
|
if (fs.existsSync(SECRETS_FILE)) {
|
|
129
286
|
try {
|
|
130
287
|
fs.unlinkSync(SECRETS_FILE);
|
|
288
|
+
_cachedSecrets = null;
|
|
289
|
+
_cachedSecretsMtime = 0;
|
|
131
290
|
return true;
|
|
132
291
|
} catch (err) {
|
|
133
292
|
console.error("Failed to delete secrets file:", err.message);
|
|
@@ -15,6 +15,7 @@ export const DEFAULT_CONFIG = {
|
|
|
15
15
|
onnxThreads: 0, // ONNX WASM threads: 0 = auto-detect CPU cores, or 1-16
|
|
16
16
|
executionDevice: "cpu", // "cpu" | "webgpu"
|
|
17
17
|
mode: "only-local", // "only-local" | "only-cloud" | "hybrid-sync"
|
|
18
|
+
injectLimit: 10,
|
|
18
19
|
conflictStrategy: "merge", // "merge" | "cloud-wins" | "local-wins"
|
|
19
20
|
tursoUrl: "", // Connection endpoint URL for Turso DB
|
|
20
21
|
failoverUrl: "", // Failover connection endpoint URL (Fly.io + LiteFS)
|
|
@@ -4,11 +4,13 @@ import { existsSync, mkdirSync } from "fs";
|
|
|
4
4
|
import { MEMORY_DIR } from "../memory.js";
|
|
5
5
|
import { runMigrations } from "./migrations.js";
|
|
6
6
|
import { getConfig } from "../config/config_manager.js";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveCloudSecrets } from "../admin/auth.js";
|
|
8
8
|
import { createClient } from "@libsql/client";
|
|
9
9
|
|
|
10
10
|
let dbInstance = null;
|
|
11
11
|
let dbInitPromise = null;
|
|
12
|
+
let dbLastFailAt = 0;
|
|
13
|
+
const DB_FAIL_COOLDOWN_MS = 5_000; // don't retry cloud init within 5s of a failure
|
|
12
14
|
|
|
13
15
|
export const STORAGE_DIR = join(MEMORY_DIR, "storage");
|
|
14
16
|
export const BLOBS_DIR = join(STORAGE_DIR, "blobs");
|
|
@@ -159,7 +161,9 @@ async function openDatabase(customPath, mode) {
|
|
|
159
161
|
let cloudClient = null;
|
|
160
162
|
let failoverClient = null;
|
|
161
163
|
if (mode === "only-cloud" || mode === "hybrid-sync") {
|
|
162
|
-
|
|
164
|
+
// Resolve working cloud credentials. An env TURSO_API_TOKEN (which can only
|
|
165
|
+
// call the Platform API) is lazily minted into a per-database JWT here.
|
|
166
|
+
const secrets = await resolveCloudSecrets();
|
|
163
167
|
const tursoUrl = customPath && customPath.startsWith("libsql:") ? customPath : (secrets?.dbUrl || config.tursoUrl);
|
|
164
168
|
const failoverUrl = config.failoverUrl || "";
|
|
165
169
|
const token = secrets?.token;
|
|
@@ -205,10 +209,20 @@ export async function getDatabase(customPath = null, forceMode = null) {
|
|
|
205
209
|
if (dbInstance && dbInstance.mode === mode) {
|
|
206
210
|
return dbInstance;
|
|
207
211
|
}
|
|
212
|
+
// After a failed init, wait before retrying to avoid hammering cloud auth
|
|
213
|
+
if (!dbInitPromise && dbLastFailAt && (Date.now() - dbLastFailAt) < DB_FAIL_COOLDOWN_MS) {
|
|
214
|
+
throw new Error("Database initialization failed recently. Retrying in a few seconds...");
|
|
215
|
+
}
|
|
208
216
|
// Deduplicate concurrent default-DB initialization so migrations never run
|
|
209
217
|
// on multiple connections at once (avoids "database is locked" crashes).
|
|
210
218
|
if (!dbInitPromise) {
|
|
211
|
-
dbInitPromise = openDatabase(null, mode).
|
|
219
|
+
dbInitPromise = openDatabase(null, mode).then((result) => {
|
|
220
|
+
dbLastFailAt = 0;
|
|
221
|
+
return result;
|
|
222
|
+
}).catch((err) => {
|
|
223
|
+
dbLastFailAt = Date.now();
|
|
224
|
+
throw err;
|
|
225
|
+
}).finally(() => {
|
|
212
226
|
dbInitPromise = null;
|
|
213
227
|
});
|
|
214
228
|
}
|
|
@@ -220,6 +234,7 @@ export async function getDatabase(customPath = null, forceMode = null) {
|
|
|
220
234
|
|
|
221
235
|
export function closeDatabase() {
|
|
222
236
|
dbInitPromise = null;
|
|
237
|
+
dbLastFailAt = 0;
|
|
223
238
|
if (dbInstance) {
|
|
224
239
|
dbInstance.close();
|
|
225
240
|
dbInstance = null;
|
|
@@ -110,6 +110,34 @@ const MIGRATIONS = [
|
|
|
110
110
|
} catch (e) {}
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
version: 4,
|
|
115
|
+
name: "004_git_project_identity",
|
|
116
|
+
up: async (db) => {
|
|
117
|
+
await db.exec(`
|
|
118
|
+
CREATE TABLE IF NOT EXISTS project_identities (
|
|
119
|
+
key TEXT PRIMARY KEY,
|
|
120
|
+
name TEXT NOT NULL,
|
|
121
|
+
primary_remote TEXT,
|
|
122
|
+
created_at INTEGER NOT NULL,
|
|
123
|
+
updated_at INTEGER NOT NULL
|
|
124
|
+
);
|
|
125
|
+
`);
|
|
126
|
+
|
|
127
|
+
await db.exec(`
|
|
128
|
+
CREATE TABLE IF NOT EXISTS project_aliases (
|
|
129
|
+
alias TEXT PRIMARY KEY,
|
|
130
|
+
identity_key TEXT NOT NULL REFERENCES project_identities(key) ON DELETE CASCADE,
|
|
131
|
+
kind TEXT NOT NULL,
|
|
132
|
+
created_at INTEGER NOT NULL
|
|
133
|
+
);
|
|
134
|
+
`);
|
|
135
|
+
|
|
136
|
+
await db.exec(`
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_project_aliases_identity ON project_aliases(identity_key);
|
|
138
|
+
`);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
113
141
|
];
|
|
114
142
|
|
|
115
143
|
export async function runMigrations(db) {
|