@lotargo/memory_plugin 1.6.6 → 1.6.7
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 +28 -0
- package/README.md +576 -443
- package/mcp-server/cli/direct_commands.js +39 -0
- package/mcp-server/cli.js +16 -5
- package/mcp-server/cli_boot.js +4 -1
- package/mcp-server/client_cli.js +73 -0
- package/mcp-server/client_paths.js +44 -0
- package/mcp-server/client_registration.js +38 -0
- package/mcp-server/codex_config.js +86 -8
- package/mcp-server/db/database.js +14 -21
- package/mcp-server/db/migrations.js +66 -77
- package/mcp-server/db/rag_blob_transport.js +143 -0
- package/mcp-server/db/rag_sync.js +284 -0
- package/mcp-server/db/sync_queue.js +219 -307
- package/mcp-server/dev_link.js +142 -0
- package/mcp-server/fact_format.js +44 -12
- package/mcp-server/index.js +17 -7
- package/mcp-server/ingest/exporter.js +44 -38
- package/mcp-server/ingest/pipeline.js +260 -248
- package/mcp-server/persona_migration.js +39 -0
- package/mcp-server/prompt_manager.js +162 -55
- package/mcp-server/retrieval/retriever.js +99 -64
- package/mcp-server/setup.js +150 -100
- package/mcp-server/storage/blob_store.js +53 -1
- package/mcp-server/tools/core/knowledge_read_core.js +163 -0
- package/mcp-server/tools/core/memory_core.js +24 -4
- package/mcp-server/tools/core/memory_routing.js +10 -0
- package/mcp-server/tools/core/note_core.js +53 -0
- package/mcp-server/tools/core/rag_query_core.js +169 -0
- package/mcp-server/tools/index.js +11 -9
- package/mcp-server/tools/memory_tools.js +4 -1
- package/mcp-server/tools/note_tools.js +35 -0
- package/mcp-server/tools/rag_tools.js +211 -364
- package/mcp-server/uninstall.js +627 -0
- package/opencode-plugin/index.js +80 -12
- package/opencode-plugin/main.js +136 -0
- package/package.json +16 -12
- package/skills/using-memory/SKILL.md +28 -19
|
@@ -12,6 +12,45 @@ import {
|
|
|
12
12
|
import { factBody } from "../fact_format.js";
|
|
13
13
|
|
|
14
14
|
export async function handleDirectCommands(cliArgs) {
|
|
15
|
+
if (cliArgs[0] === "uninstall" || cliArgs.includes("uninstall") || cliArgs.includes("--uninstall")) {
|
|
16
|
+
const { runUninstall } = await import("../uninstall.js");
|
|
17
|
+
await runUninstall();
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (cliArgs[0] === "dev-link" || cliArgs[0] === "dev_link") {
|
|
22
|
+
const { runDevLink } = await import("../dev_link.js");
|
|
23
|
+
await runDevLink();
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (cliArgs[0] === "sync-persona" || cliArgs[0] === "sync_persona") {
|
|
28
|
+
const { syncPersonaPrompts } = await import("../prompt_manager.js");
|
|
29
|
+
const results = await syncPersonaPrompts();
|
|
30
|
+
console.log("\nPersona overlay synchronization:\n");
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
console.log(` - ${result.name}: ${result.status}${result.error ? ` (${result.error})` : ""}`);
|
|
33
|
+
}
|
|
34
|
+
console.log("");
|
|
35
|
+
if (results.some((result) => result.status === "failed")) process.exitCode = 1;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (cliArgs[0] === "migrate-persona" || cliArgs[0] === "migrate_persona") {
|
|
40
|
+
const { migrateLegacyPersonaDirectives } = await import("../persona_migration.js");
|
|
41
|
+
const dryRun = cliArgs.includes("--dry-run");
|
|
42
|
+
const result = await migrateLegacyPersonaDirectives({ dryRun });
|
|
43
|
+
console.log(`\nPersona metadata migration${dryRun ? " (dry run)" : ""}:\n`);
|
|
44
|
+
console.log(` Legacy directives found: ${result.changed}`);
|
|
45
|
+
for (const item of result.migrated) {
|
|
46
|
+
console.log(` - ${item.title}${item.id ? ` (${item.id})` : ""}`);
|
|
47
|
+
}
|
|
48
|
+
console.log(dryRun
|
|
49
|
+
? "\nNo files were changed.\n"
|
|
50
|
+
: "\nGlobal directives and managed client prompts are synchronized.\n");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
if (cliArgs[0] === "doctor" && cliArgs.includes("--codex")) {
|
|
16
55
|
const { runCodexDoctor } = await import("../codex_diagnostics.js");
|
|
17
56
|
console.log("\nCodex memory-agent diagnostics\n");
|
package/mcp-server/cli.js
CHANGED
|
@@ -13,6 +13,12 @@ import { handleDiagnosticsAction } from "./cli/handlers/diagnostics_actions.js";
|
|
|
13
13
|
export async function runCli() {
|
|
14
14
|
const cliArgs = process.argv.slice(2);
|
|
15
15
|
|
|
16
|
+
if (cliArgs.includes("uninstall") || cliArgs.includes("--uninstall")) {
|
|
17
|
+
const { runUninstall } = await import("./uninstall.js");
|
|
18
|
+
await runUninstall();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
if (cliArgs.includes("--help") || cliArgs.includes("-h") || cliArgs[0] === "help") {
|
|
17
23
|
console.log(`memory-cli — interactive control panel for @lotargo/memory_plugin
|
|
18
24
|
|
|
@@ -23,13 +29,18 @@ Usage:
|
|
|
23
29
|
memory-cli auth-status
|
|
24
30
|
memory-cli link|unlink|relink|identity [--dir <path>] [--remote <url>]
|
|
25
31
|
memory-cli migrate_titles [--key <key>]
|
|
26
|
-
memory-cli
|
|
27
|
-
memory-cli
|
|
32
|
+
memory-cli dev-link Link the working repository for local development
|
|
33
|
+
memory-cli sync-persona Synchronize global directives into client prompts
|
|
34
|
+
memory-cli migrate-persona [--dry-run]
|
|
35
|
+
Mark legacy global persona entries as directives
|
|
36
|
+
memory-cli enable-prompt | disable-prompt
|
|
37
|
+
memory-cli uninstall [--purge] [--purge-cache] [--dry-run] [--yes] [--opencode|--claude|--codex|--gemini|--antigravity]
|
|
38
|
+
memory-cli doctor --codex
|
|
28
39
|
|
|
29
40
|
Options:
|
|
30
41
|
-h, --help Show this help text`);
|
|
31
42
|
return;
|
|
32
|
-
}
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
const handled = await handleDirectCommands(cliArgs);
|
|
35
46
|
if (handled) return;
|
|
@@ -279,9 +290,9 @@ async function showCategorySubmenu(category, config, stats, initialIndex = 0) {
|
|
|
279
290
|
case "prompt":
|
|
280
291
|
items = [
|
|
281
292
|
{
|
|
282
|
-
label: "[PROMPT ENABLE] Enable Global Prompt (Antigravity / Codex / Claude)",
|
|
293
|
+
label: "[PROMPT ENABLE] Enable Global Prompt (Gemini / Antigravity / Codex / Claude)",
|
|
283
294
|
value: "enable_prompt",
|
|
284
|
-
info: "Inject memory instructions into
|
|
295
|
+
info: "Inject managed memory instructions into each supported client's global prompt file",
|
|
285
296
|
},
|
|
286
297
|
{
|
|
287
298
|
label: "[PROMPT DISABLE] Disable Global Prompt",
|
package/mcp-server/cli_boot.js
CHANGED
|
@@ -32,6 +32,9 @@ if (major < MIN_MAJOR || (major === MIN_MAJOR && minor < MIN_MINOR)) {
|
|
|
32
32
|
// Version is OK — hand off to the real CLI.
|
|
33
33
|
import("./cli.js").then(m => {
|
|
34
34
|
if (process.argv[1] && process.argv[1].includes("cli_boot.js")) {
|
|
35
|
-
m.runCli().catch((err) =>
|
|
35
|
+
m.runCli().catch((err) => {
|
|
36
|
+
console.error("CLI error:", err);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
});
|
|
36
39
|
}
|
|
37
40
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { delimiter, extname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function envValue(env, name) {
|
|
6
|
+
if (Object.prototype.hasOwnProperty.call(env || {}, name)) return env[name];
|
|
7
|
+
const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
8
|
+
return key ? env[key] : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveClientExecutable(name, {
|
|
12
|
+
env = process.env,
|
|
13
|
+
platform = process.platform,
|
|
14
|
+
} = {}) {
|
|
15
|
+
if (String(env.MEMORY_PLUGIN_DISABLE_NATIVE_CLI || "") === "1") return null;
|
|
16
|
+
const pathValue = envValue(env, "PATH") || "";
|
|
17
|
+
const directories = pathValue.split(delimiter).filter(Boolean);
|
|
18
|
+
const extensions = platform === "win32"
|
|
19
|
+
? (envValue(env, "PATHEXT") || ".EXE;.COM;.CMD;.BAT").split(";").filter(Boolean)
|
|
20
|
+
: [""];
|
|
21
|
+
|
|
22
|
+
for (const directory of directories) {
|
|
23
|
+
const base = join(directory, name);
|
|
24
|
+
const candidates = platform === "win32" && extname(base)
|
|
25
|
+
? [base]
|
|
26
|
+
: extensions.map((extension) => `${base}${extension.toLowerCase()}`)
|
|
27
|
+
.concat(extensions.map((extension) => `${base}${extension.toUpperCase()}`));
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
if (existsSync(candidate)) return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function runClientCli(name, args, {
|
|
36
|
+
cwd = process.cwd(),
|
|
37
|
+
env = process.env,
|
|
38
|
+
platform = process.platform,
|
|
39
|
+
timeout = 20_000,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const executable = resolveClientExecutable(name, { env, platform });
|
|
42
|
+
if (!executable) {
|
|
43
|
+
return { available: false, ok: false, status: null, executable: null, stdout: "", stderr: "" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isWindowsShim = platform === "win32" && /\.(?:cmd|bat)$/i.test(executable);
|
|
47
|
+
const command = isWindowsShim ? (envValue(env, "ComSpec") || "cmd.exe") : executable;
|
|
48
|
+
const commandArgs = isWindowsShim ? ["/d", "/s", "/c", executable, ...args] : args;
|
|
49
|
+
const result = spawnSync(command, commandArgs, {
|
|
50
|
+
cwd,
|
|
51
|
+
env,
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout,
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
shell: false,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
available: true,
|
|
59
|
+
ok: result.status === 0 && !result.error,
|
|
60
|
+
status: result.status,
|
|
61
|
+
executable,
|
|
62
|
+
stdout: result.stdout || "",
|
|
63
|
+
stderr: result.stderr || "",
|
|
64
|
+
error: result.error || null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function cliFailureMessage(result) {
|
|
69
|
+
if (!result?.available) return "client CLI is not installed or not available on PATH";
|
|
70
|
+
if (result.error) return result.error.message;
|
|
71
|
+
const detail = String(result.stderr || result.stdout || "").trim().split(/\r?\n/).pop();
|
|
72
|
+
return detail || `client CLI exited with status ${result.status}`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function resolveClientPaths({
|
|
5
|
+
home = homedir(),
|
|
6
|
+
cwd = process.cwd(),
|
|
7
|
+
env = process.env,
|
|
8
|
+
} = {}) {
|
|
9
|
+
const configHome = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
10
|
+
const cacheHome = env.XDG_CACHE_HOME || join(home, ".cache");
|
|
11
|
+
const opencodeDir = env.OPENCODE_CONFIG_DIR || join(configHome, "opencode");
|
|
12
|
+
const agentConfigDir = join(configHome, "memory-agent");
|
|
13
|
+
const geminiDir = join(home, ".gemini");
|
|
14
|
+
const antigravityConfigDir = join(geminiDir, "config");
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
home,
|
|
18
|
+
cwd,
|
|
19
|
+
configHome,
|
|
20
|
+
cacheHome,
|
|
21
|
+
opencodeDir,
|
|
22
|
+
opencodeConfigPath: join(opencodeDir, "opencode.json"),
|
|
23
|
+
opencodeCachePackages: join(cacheHome, "opencode", "packages"),
|
|
24
|
+
claudeConfigPath: join(home, ".claude.json"),
|
|
25
|
+
geminiDir,
|
|
26
|
+
geminiSettingsPath: join(geminiDir, "settings.json"),
|
|
27
|
+
geminiPromptPath: join(geminiDir, "GEMINI.md"),
|
|
28
|
+
geminiSkillsDir: join(geminiDir, "skills"),
|
|
29
|
+
// Antigravity intentionally uses its own legacy layout. Keep these paths
|
|
30
|
+
// separate from the real Gemini CLI settings above.
|
|
31
|
+
antigravityConfigDir,
|
|
32
|
+
antigravityMcpConfigPath: join(antigravityConfigDir, "mcp_config.json"),
|
|
33
|
+
geminiConfigDir: antigravityConfigDir,
|
|
34
|
+
geminiMcpConfigPath: join(antigravityConfigDir, "mcp_config.json"),
|
|
35
|
+
localAgentsDir: join(cwd, ".agents"),
|
|
36
|
+
localAgentsMcpConfigPath: join(cwd, ".agents", "mcp_config.json"),
|
|
37
|
+
codexDir: join(home, ".codex"),
|
|
38
|
+
codexConfigPath: join(home, ".codex", "config.toml"),
|
|
39
|
+
agentConfigDir,
|
|
40
|
+
promptFile: join(agentConfigDir, "prompt.md"),
|
|
41
|
+
promptStateFile: join(agentConfigDir, "prompt-state.json"),
|
|
42
|
+
promptBackupDir: join(agentConfigDir, "backups"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isMemoryPluginSpec } from "./dev_link.js";
|
|
4
|
+
|
|
5
|
+
export const MEMORY_MCP_ENTRY = Object.freeze({
|
|
6
|
+
command: "npx",
|
|
7
|
+
args: Object.freeze(["-y", "@lotargo/memory_plugin"]),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function isMemoryPluginEntry(entry) {
|
|
11
|
+
const spec = Array.isArray(entry)
|
|
12
|
+
? entry[0]
|
|
13
|
+
: entry && typeof entry === "object"
|
|
14
|
+
? entry.package
|
|
15
|
+
: entry;
|
|
16
|
+
return isMemoryPluginSpec(spec);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isMemoryMcpServerEntry(entry) {
|
|
20
|
+
if (!entry || typeof entry !== "object") return false;
|
|
21
|
+
const command = String(entry.command || "").replace(/\\/g, "/").toLowerCase();
|
|
22
|
+
const args = Array.isArray(entry.args)
|
|
23
|
+
? entry.args.map((value) => String(value).replace(/\\/g, "/").toLowerCase())
|
|
24
|
+
: [];
|
|
25
|
+
return args.some((arg) => /^@lotargo\/memory_plugin(?:@[^/]+)?$/i.test(arg) || /^opencode-memory-plugin(?:@[^/]+)?$/i.test(arg))
|
|
26
|
+
|| args.some((arg) => arg.endsWith("/mcp-server/boot.js") && arg.includes("memory"))
|
|
27
|
+
|| /(?:^|\/)(?:memory_plugin|memory-agent)(?:\.(?:cmd|exe|ps1|bat))?$/i.test(command);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function readJsonConfig(filePath) {
|
|
31
|
+
if (!existsSync(filePath)) return {};
|
|
32
|
+
const raw = (await readFile(filePath, "utf-8")).replace(/^\uFEFF/, "");
|
|
33
|
+
const value = JSON.parse(raw);
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
35
|
+
throw new Error(`Expected a JSON object in ${filePath}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
@@ -102,6 +102,44 @@ function sectionRanges(lines) {
|
|
|
102
102
|
}));
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function validateOwnedMemoryAgentTargets(lines, ranges) {
|
|
106
|
+
const targets = ranges.filter((range) => isMemoryAgentHeader(range.name));
|
|
107
|
+
if (targets.length === 0) return { targets, exactTargets: [], conflict: null };
|
|
108
|
+
|
|
109
|
+
const exactTargets = targets.filter((range) => isMemoryAgentHeader(range.name, { exact: true }));
|
|
110
|
+
const unowned = exactTargets.filter((range) => {
|
|
111
|
+
const text = lines.slice(range.start, range.end).join("\n");
|
|
112
|
+
return !isMemoryPluginOwnedSection(text);
|
|
113
|
+
});
|
|
114
|
+
if (unowned.length > 0 || exactTargets.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
targets,
|
|
117
|
+
exactTargets,
|
|
118
|
+
conflict: "Existing memory-agent TOML section is not recognized as owned by @lotargo/memory_plugin",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let activeOwnedRoot = false;
|
|
123
|
+
for (const range of ranges) {
|
|
124
|
+
if (isMemoryAgentHeader(range.name, { exact: true })) {
|
|
125
|
+
const text = lines.slice(range.start, range.end).join("\n");
|
|
126
|
+
activeOwnedRoot = isMemoryPluginOwnedSection(text);
|
|
127
|
+
} else if (isMemoryAgentHeader(range.name)) {
|
|
128
|
+
if (!activeOwnedRoot) {
|
|
129
|
+
return {
|
|
130
|
+
targets,
|
|
131
|
+
exactTargets,
|
|
132
|
+
conflict: "A memory-agent child TOML section is not attached to an owned plugin section",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
activeOwnedRoot = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { targets, exactTargets, conflict: null };
|
|
141
|
+
}
|
|
142
|
+
|
|
105
143
|
export function getCodexMemoryAgentSections(content) {
|
|
106
144
|
const lines = String(content || "").split(/\r?\n/);
|
|
107
145
|
return sectionRanges(lines)
|
|
@@ -113,25 +151,65 @@ export function getCodexMemoryAgentSections(content) {
|
|
|
113
151
|
}));
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
export function removeCodexMemoryAgentConfig(content) {
|
|
155
|
+
const source = String(content || "");
|
|
156
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
157
|
+
const lines = source.split(/\r?\n/);
|
|
158
|
+
const ranges = sectionRanges(lines);
|
|
159
|
+
const { targets, conflict } = validateOwnedMemoryAgentTargets(lines, ranges);
|
|
160
|
+
|
|
161
|
+
if (targets.length === 0) {
|
|
162
|
+
return { content: source, changed: false, status: "not_found", removed: 0 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (conflict) {
|
|
166
|
+
return {
|
|
167
|
+
content: source,
|
|
168
|
+
changed: false,
|
|
169
|
+
status: "conflict",
|
|
170
|
+
reason: conflict,
|
|
171
|
+
removed: 0,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const targetLineIndexes = new Set();
|
|
176
|
+
for (const target of targets) {
|
|
177
|
+
let contentEnd = target.end;
|
|
178
|
+
while (contentEnd > target.start + 1 && lines[contentEnd - 1] === "") contentEnd--;
|
|
179
|
+
for (let i = target.start; i < contentEnd; i++) targetLineIndexes.add(i);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = [];
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
if (targetLineIndexes.has(i)) continue;
|
|
185
|
+
result.push(lines[i]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let updated = result.join(eol);
|
|
189
|
+
if (updated.trim() === "") updated = "";
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
content: updated,
|
|
193
|
+
changed: updated !== source,
|
|
194
|
+
status: updated !== source ? "removed" : "unchanged",
|
|
195
|
+
removed: targets.length,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
116
199
|
export function updateCodexMemoryAgentConfig(content, options) {
|
|
117
200
|
const source = String(content || "");
|
|
118
201
|
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
119
202
|
const lines = source.split(/\r?\n/);
|
|
120
203
|
const desired = buildCodexMemoryAgentSection(options).split("\n");
|
|
121
204
|
const ranges = sectionRanges(lines);
|
|
122
|
-
const targets =
|
|
123
|
-
const exactTargets = targets.filter((range) => isMemoryAgentHeader(range.name, { exact: true }));
|
|
205
|
+
const { targets, conflict } = validateOwnedMemoryAgentTargets(lines, ranges);
|
|
124
206
|
|
|
125
|
-
|
|
126
|
-
const text = lines.slice(range.start, range.end).join("\n");
|
|
127
|
-
return !isMemoryPluginOwnedSection(text);
|
|
128
|
-
});
|
|
129
|
-
if (unowned.length > 0) {
|
|
207
|
+
if (conflict) {
|
|
130
208
|
return {
|
|
131
209
|
content: source,
|
|
132
210
|
changed: false,
|
|
133
211
|
status: "conflict",
|
|
134
|
-
reason:
|
|
212
|
+
reason: conflict,
|
|
135
213
|
};
|
|
136
214
|
}
|
|
137
215
|
|
|
@@ -10,7 +10,7 @@ import { createClient } from "@libsql/client";
|
|
|
10
10
|
let dbInstance = null;
|
|
11
11
|
let dbInitPromise = null;
|
|
12
12
|
let dbLastFailAt = 0;
|
|
13
|
-
const DB_FAIL_COOLDOWN_MS = 5_000;
|
|
13
|
+
const DB_FAIL_COOLDOWN_MS = 5_000;
|
|
14
14
|
|
|
15
15
|
export const STORAGE_DIR = join(MEMORY_DIR, "storage");
|
|
16
16
|
export const BLOBS_DIR = join(STORAGE_DIR, "blobs");
|
|
@@ -34,8 +34,6 @@ class DatabaseWrapper {
|
|
|
34
34
|
|
|
35
35
|
while (attempts < maxAttempts) {
|
|
36
36
|
attempts++;
|
|
37
|
-
// A plain timer is enough here; an AbortController per attempt leaked its
|
|
38
|
-
// "abort" listener because it was never removed.
|
|
39
37
|
let timeoutId = null;
|
|
40
38
|
const timeoutPromise = new Promise((_, reject) => {
|
|
41
39
|
timeoutId = setTimeout(
|
|
@@ -48,7 +46,6 @@ class DatabaseWrapper {
|
|
|
48
46
|
const client = (this.usingFailover && this.failoverClient) ? this.failoverClient : this.cloudClient;
|
|
49
47
|
const result = await Promise.race([fn(client), timeoutPromise]);
|
|
50
48
|
clearTimeout(timeoutId);
|
|
51
|
-
// Successful operation, reset consecutive failures
|
|
52
49
|
this.consecutiveFailures = 0;
|
|
53
50
|
return result;
|
|
54
51
|
} catch (err) {
|
|
@@ -58,12 +55,10 @@ class DatabaseWrapper {
|
|
|
58
55
|
if (this.consecutiveFailures >= 3 && this.failoverClient && !this.usingFailover) {
|
|
59
56
|
console.warn("[WARN] Turso is temporarily unreachable. Switching to LiteFS failover replica...");
|
|
60
57
|
this.usingFailover = true;
|
|
61
|
-
// Retry the operation on the failover client
|
|
62
58
|
return this.runWithRetry(fn);
|
|
63
59
|
}
|
|
64
60
|
throw err;
|
|
65
61
|
}
|
|
66
|
-
// Small delay before retrying (exponential backoff / fixed delay)
|
|
67
62
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
63
|
}
|
|
69
64
|
}
|
|
@@ -156,16 +151,12 @@ async function openDatabase(customPath, mode) {
|
|
|
156
151
|
localDb = new DatabaseSync(dbPath);
|
|
157
152
|
localDb.exec("PRAGMA foreign_keys = ON;");
|
|
158
153
|
localDb.exec("PRAGMA journal_mode = WAL;");
|
|
159
|
-
// Default busy_timeout is 0 => an immediate SQLITE_BUSY ("database is
|
|
160
|
-
// locked") whenever background sync, ingestion and MCP calls overlap.
|
|
161
154
|
localDb.exec("PRAGMA busy_timeout = 5000;");
|
|
162
155
|
}
|
|
163
156
|
|
|
164
157
|
let cloudClient = null;
|
|
165
158
|
let failoverClient = null;
|
|
166
159
|
if (mode === "only-cloud" || mode === "hybrid-sync") {
|
|
167
|
-
// Resolve working cloud credentials. An env TURSO_API_TOKEN (which can only
|
|
168
|
-
// call the Platform API) is lazily minted into a per-database JWT here.
|
|
169
160
|
const secrets = await resolveCloudSecrets();
|
|
170
161
|
const tursoUrl = customPath && customPath.startsWith("libsql:") ? customPath : (secrets?.dbUrl || config.tursoUrl);
|
|
171
162
|
const failoverUrl = config.failoverUrl || "";
|
|
@@ -182,8 +173,6 @@ async function openDatabase(customPath, mode) {
|
|
|
182
173
|
authToken: token || undefined,
|
|
183
174
|
});
|
|
184
175
|
}
|
|
185
|
-
// In hybrid-sync mode, ensure remote schema is also fully migrated and up to date.
|
|
186
|
-
// Use a dedicated short-lived client so closing it doesn't kill the shared one.
|
|
187
176
|
if (mode === "hybrid-sync") {
|
|
188
177
|
const remoteClient = createClient({
|
|
189
178
|
url: tursoUrl,
|
|
@@ -199,13 +188,22 @@ async function openDatabase(customPath, mode) {
|
|
|
199
188
|
}
|
|
200
189
|
|
|
201
190
|
const wrappedDb = new DatabaseWrapper(localDb, cloudClient, mode, failoverClient);
|
|
202
|
-
|
|
203
|
-
// Initialize/run migrations
|
|
204
191
|
await runMigrations(wrappedDb);
|
|
205
192
|
|
|
193
|
+
// Upgrade path for RAG content ingested before portable cloud blobs existed.
|
|
194
|
+
// The backfill is content-addressed and uploads only hashes absent in Turso.
|
|
195
|
+
// Missing local files are simply reported/skipped; database availability must
|
|
196
|
+
// never depend on a legacy raw blob still being present on this machine.
|
|
197
|
+
if ((mode === "only-cloud" || mode === "hybrid-sync") && cloudClient) {
|
|
198
|
+
try {
|
|
199
|
+
const { backfillCloudBlobsFromLocal } = await import("./rag_blob_transport.js");
|
|
200
|
+
await backfillCloudBlobsFromLocal(wrappedDb);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.warn("[WARN] RAG cloud blob backfill skipped:", err.message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
206
|
if (!customPath) {
|
|
207
|
-
// Closing the previous instance before replacing it: otherwise a mode switch
|
|
208
|
-
// leaked the old DatabaseSync handle and Turso client.
|
|
209
207
|
if (dbInstance && dbInstance !== wrappedDb) {
|
|
210
208
|
try {
|
|
211
209
|
dbInstance.close();
|
|
@@ -225,15 +223,10 @@ export async function getDatabase(customPath = null, forceMode = null) {
|
|
|
225
223
|
if (dbInstance && dbInstance.mode === mode) {
|
|
226
224
|
return dbInstance;
|
|
227
225
|
}
|
|
228
|
-
// After a failed init, wait before retrying to avoid hammering cloud auth.
|
|
229
|
-
// Only cloud modes are throttled — reopening a local SQLite file is cheap
|
|
230
|
-
// and must never be blocked by an unrelated cloud failure.
|
|
231
226
|
const isCloudMode = mode === "only-cloud" || mode === "hybrid-sync";
|
|
232
227
|
if (isCloudMode && !dbInitPromise && dbLastFailAt && (Date.now() - dbLastFailAt) < DB_FAIL_COOLDOWN_MS) {
|
|
233
228
|
throw new Error("Database initialization failed recently. Retrying in a few seconds...");
|
|
234
229
|
}
|
|
235
|
-
// Deduplicate concurrent default-DB initialization so migrations never run
|
|
236
|
-
// on multiple connections at once (avoids "database is locked" crashes).
|
|
237
230
|
if (!dbInitPromise) {
|
|
238
231
|
dbInitPromise = openDatabase(null, mode).then((result) => {
|
|
239
232
|
dbLastFailAt = 0;
|