@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +406 -352
  3. package/mcp-server/admin/auth.js +13 -4
  4. package/mcp-server/admin/snapshot.js +24 -7
  5. package/mcp-server/boot.js +43 -0
  6. package/mcp-server/cli/direct_commands.js +334 -313
  7. package/mcp-server/cli/handlers/engine_actions.js +41 -0
  8. package/mcp-server/cli/handlers/storage_actions.js +58 -0
  9. package/mcp-server/cli/secret_input.js +44 -0
  10. package/mcp-server/cli/ui.js +564 -565
  11. package/mcp-server/cli.js +356 -324
  12. package/mcp-server/cli_boot.js +37 -0
  13. package/mcp-server/config/auth_store.js +74 -16
  14. package/mcp-server/config/config_manager.js +4 -0
  15. package/mcp-server/db/database.js +33 -14
  16. package/mcp-server/db/sync_queue.js +9 -19
  17. package/mcp-server/index.js +112 -42
  18. package/mcp-server/ingest/normalizer.js +116 -29
  19. package/mcp-server/ingest/pipeline.js +94 -6
  20. package/mcp-server/logger.js +49 -0
  21. package/mcp-server/memory.js +6 -9
  22. package/mcp-server/ml/gpu_monitor.js +169 -166
  23. package/mcp-server/ml/model_manager.js +17 -4
  24. package/mcp-server/preinstall.js +23 -2
  25. package/mcp-server/retrieval/retriever.js +35 -15
  26. package/mcp-server/security/path_guard.js +67 -0
  27. package/mcp-server/setup.js +10 -2
  28. package/mcp-server/storage/blob_store.js +15 -2
  29. package/mcp-server/tools/core/memory_core.js +393 -0
  30. package/mcp-server/tools/helpers.js +59 -39
  31. package/mcp-server/tools/memory_tools.js +123 -516
  32. package/mcp-server/tools/rag_tools.js +49 -1
  33. package/opencode-plugin/index.js +94 -397
  34. package/package.json +13 -4
  35. package/skills/using-memory/SKILL.md +7 -2
@@ -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
- export function startAuthLoopbackServer(port = 48900) {
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, gunzipSync } from "node:zlib";
3
- import { join, resolve } from "node:path";
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
- const resolved = resolve(pathStr.trim());
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
- if (mc.vector) {
64
- const buf = Buffer.isBuffer(mc.vector) ? mc.vector : Buffer.from(mc.vector);
65
- vecBase64 = buf.toString("base64");
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 = gunzipSync(raw);
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");