@lotargo/memory_plugin 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/README.md +406 -352
- package/mcp-server/admin/auth.js +13 -4
- package/mcp-server/admin/snapshot.js +24 -7
- package/mcp-server/boot.js +43 -0
- package/mcp-server/cli/direct_commands.js +334 -313
- package/mcp-server/cli/handlers/engine_actions.js +41 -0
- package/mcp-server/cli/handlers/storage_actions.js +58 -0
- package/mcp-server/cli/secret_input.js +44 -0
- package/mcp-server/cli/ui.js +564 -565
- package/mcp-server/cli.js +356 -324
- package/mcp-server/cli_boot.js +37 -0
- package/mcp-server/config/auth_store.js +74 -16
- package/mcp-server/config/config_manager.js +4 -0
- package/mcp-server/db/database.js +33 -14
- package/mcp-server/db/sync_queue.js +9 -19
- package/mcp-server/index.js +112 -42
- package/mcp-server/ingest/normalizer.js +116 -29
- package/mcp-server/ingest/pipeline.js +94 -6
- package/mcp-server/logger.js +49 -0
- package/mcp-server/memory.js +6 -9
- package/mcp-server/ml/gpu_monitor.js +169 -166
- package/mcp-server/ml/model_manager.js +17 -4
- package/mcp-server/preinstall.js +23 -2
- package/mcp-server/retrieval/retriever.js +35 -15
- package/mcp-server/security/path_guard.js +67 -0
- package/mcp-server/setup.js +10 -2
- package/mcp-server/storage/blob_store.js +15 -2
- package/mcp-server/tools/core/memory_core.js +393 -0
- package/mcp-server/tools/helpers.js +59 -39
- package/mcp-server/tools/memory_tools.js +123 -516
- package/mcp-server/tools/rag_tools.js +49 -1
- package/opencode-plugin/index.js +94 -397
- package/package.json +13 -4
- package/skills/using-memory/SKILL.md +7 -2
package/mcp-server/admin/auth.js
CHANGED
|
@@ -26,15 +26,24 @@ function openBrowser(url) {
|
|
|
26
26
|
|
|
27
27
|
// Starts a temporary loopback HTTP server to receive the OAuth callback.
|
|
28
28
|
// Turso redirects the browser back to the root path: /?jwt=<JWT>&username=<USERNAME>
|
|
29
|
-
|
|
29
|
+
// `expectedState` protects against OAuth CSRF: the callback is only accepted
|
|
30
|
+
// when it echoes the random `state` value that was embedded in the login URL.
|
|
31
|
+
export function startAuthLoopbackServer(port = 48900, expectedState = null) {
|
|
30
32
|
return new Promise((resolve, reject) => {
|
|
31
33
|
const server = http.createServer((req, res) => {
|
|
32
34
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
33
35
|
const token = url.searchParams.get("jwt") || url.searchParams.get("token");
|
|
34
36
|
const username = url.searchParams.get("username");
|
|
35
37
|
const error = url.searchParams.get("error");
|
|
38
|
+
const receivedState = url.searchParams.get("state");
|
|
36
39
|
|
|
37
40
|
if (token) {
|
|
41
|
+
if (expectedState && receivedState !== expectedState) {
|
|
42
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
43
|
+
res.end("Invalid state parameter");
|
|
44
|
+
server.close(() => reject(new Error("OAuth callback rejected: state parameter mismatch (possible CSRF attempt).")));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
38
47
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
39
48
|
res.end(`
|
|
40
49
|
<!DOCTYPE html>
|
|
@@ -379,9 +388,9 @@ export async function loginToCloud({
|
|
|
379
388
|
let received;
|
|
380
389
|
if (simulated && simulatedParams) {
|
|
381
390
|
received = await new Promise((resolve, reject) => {
|
|
382
|
-
const serverPromise = startAuthLoopbackServer(customPort);
|
|
391
|
+
const serverPromise = startAuthLoopbackServer(customPort, state);
|
|
383
392
|
const req = http.request(
|
|
384
|
-
`http://127.0.0.1:${customPort}/?jwt=${encodeURIComponent(simulatedParams.jwt)}&username=${encodeURIComponent(simulatedParams.username)}`,
|
|
393
|
+
`http://127.0.0.1:${customPort}/?jwt=${encodeURIComponent(simulatedParams.jwt)}&username=${encodeURIComponent(simulatedParams.username)}&state=${encodeURIComponent(state)}`,
|
|
385
394
|
{ method: "GET" },
|
|
386
395
|
(res) => {
|
|
387
396
|
res.resume();
|
|
@@ -393,7 +402,7 @@ export async function loginToCloud({
|
|
|
393
402
|
});
|
|
394
403
|
} else {
|
|
395
404
|
openBrowser(loginUrl);
|
|
396
|
-
received = await startAuthLoopbackServer(customPort);
|
|
405
|
+
received = await startAuthLoopbackServer(customPort, state);
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
const { token, username } = received;
|
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
2
|
-
import { gzipSync
|
|
3
|
-
import { join
|
|
2
|
+
import { gzipSync } from "node:zlib";
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
import { getDatabase, BLOBS_DIR } from "../db/database.js";
|
|
5
5
|
import { readBlob, saveBlob } from "../storage/blob_store.js";
|
|
6
6
|
import { ensureExportsDir } from "../ingest/exporter.js";
|
|
7
|
+
import { realResolve, isWithin } from "../security/path_guard.js";
|
|
8
|
+
import { toVectorBytes } from "../retrieval/retriever.js";
|
|
9
|
+
import { safeGunzip } from "../storage/blob_store.js";
|
|
10
|
+
|
|
11
|
+
const ALLOWED_SNAPSHOT_DIRS = new Set();
|
|
12
|
+
|
|
13
|
+
export function registerSnapshotDir(dir) {
|
|
14
|
+
ALLOWED_SNAPSHOT_DIRS.add(realResolve(dir));
|
|
15
|
+
}
|
|
7
16
|
|
|
8
17
|
export function validateSnapshotPath(pathStr, isExport = false) {
|
|
9
18
|
if (typeof pathStr !== "string" || !pathStr.trim()) {
|
|
10
19
|
throw new Error("Snapshot path cannot be empty.");
|
|
11
20
|
}
|
|
12
|
-
|
|
21
|
+
// realResolve follows symlinks/junctions, so a link inside the exports dir
|
|
22
|
+
// cannot be used to escape the allowlist.
|
|
23
|
+
const resolved = realResolve(pathStr.trim());
|
|
13
24
|
if (!resolved.endsWith(".json") && !resolved.endsWith(".json.gz")) {
|
|
14
25
|
throw new Error(`Invalid snapshot file extension for path '${pathStr}'. Path must end with .json or .json.gz`);
|
|
15
26
|
}
|
|
16
27
|
if (!isExport && !existsSync(resolved)) {
|
|
17
28
|
throw new Error(`Snapshot file not found: ${pathStr}`);
|
|
18
29
|
}
|
|
30
|
+
if (ALLOWED_SNAPSHOT_DIRS.size > 0) {
|
|
31
|
+
const allowed = [...ALLOWED_SNAPSHOT_DIRS].some((dir) => isWithin(resolved, dir));
|
|
32
|
+
if (!allowed) {
|
|
33
|
+
throw new Error(`Snapshot path '${pathStr}' is outside the allowed directories.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
19
36
|
return resolved;
|
|
20
37
|
}
|
|
21
38
|
|
|
@@ -60,9 +77,9 @@ export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
60
77
|
|
|
61
78
|
const microChunks = rawMicroChunks.map((mc) => {
|
|
62
79
|
let vecBase64 = "";
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
vecBase64 =
|
|
80
|
+
const vecBytes = toVectorBytes(mc.vector);
|
|
81
|
+
if (vecBytes && vecBytes.byteLength) {
|
|
82
|
+
vecBase64 = Buffer.from(vecBytes.buffer, vecBytes.byteOffset, vecBytes.byteLength).toString("base64");
|
|
66
83
|
}
|
|
67
84
|
return {
|
|
68
85
|
...mc,
|
|
@@ -113,7 +130,7 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
|
|
|
113
130
|
const validPath = validateSnapshotPath(snapshotPathOrData, false);
|
|
114
131
|
const raw = readFileSync(validPath);
|
|
115
132
|
if (validPath.endsWith(".gz")) {
|
|
116
|
-
const decompressed =
|
|
133
|
+
const decompressed = safeGunzip(raw);
|
|
117
134
|
snapshot = JSON.parse(decompressed.toString("utf-8"));
|
|
118
135
|
} else {
|
|
119
136
|
snapshot = JSON.parse(raw.toString("utf-8"));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ── Boot Guard ──────────────────────────────────────────────────────────────
|
|
4
|
+
// This file is the true entry point for both `memory_plugin` and `memory-agent`
|
|
5
|
+
// binaries. Its sole purpose is to verify the Node.js version BEFORE the ESM
|
|
6
|
+
// module graph is evaluated — because `mcp-server/index.js` transitively
|
|
7
|
+
// imports `node:sqlite` (a built-in available only from Node 22.5.0), and ESM
|
|
8
|
+
// static imports are hoisted, so a version check inside that file would never
|
|
9
|
+
// execute.
|
|
10
|
+
//
|
|
11
|
+
// By keeping this file free of any `node:sqlite` dependency we can print a
|
|
12
|
+
// clear, actionable error message instead of the cryptic
|
|
13
|
+
// "No such built-in module: node:sqlite"
|
|
14
|
+
// that users on Node 18/20/21 would otherwise see.
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const MIN_MAJOR = 22;
|
|
18
|
+
const MIN_MINOR = 5;
|
|
19
|
+
|
|
20
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
21
|
+
|
|
22
|
+
if (major < MIN_MAJOR || (major === MIN_MAJOR && minor < MIN_MINOR)) {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`\n` +
|
|
25
|
+
` ╔══════════════════════════════════════════════════════════════════╗\n` +
|
|
26
|
+
` ║ @lotargo/memory_plugin requires Node.js >= 22.5.0 ║\n` +
|
|
27
|
+
` ║ ║\n` +
|
|
28
|
+
` ║ Detected: Node.js ${process.versions.node.padEnd(44)}║\n` +
|
|
29
|
+
` ║ ║\n` +
|
|
30
|
+
` ║ The built-in node:sqlite module used by this plugin was ║\n` +
|
|
31
|
+
` ║ introduced in Node.js 22.5.0. Please upgrade your ║\n` +
|
|
32
|
+
` ║ Node.js installation: ║\n` +
|
|
33
|
+
` ║ ║\n` +
|
|
34
|
+
` ║ nvm install 22 # or: brew install node@22 ║\n` +
|
|
35
|
+
` ║ ║\n` +
|
|
36
|
+
` ╚══════════════════════════════════════════════════════════════════╝\n` +
|
|
37
|
+
`\n`
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Version is OK — hand off to the real entry point.
|
|
43
|
+
import("./index.js");
|