@lotargo/memory_plugin 1.6.5 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +576 -443
  3. package/mcp-server/benchmarks/fetch_real_corpus.js +351 -0
  4. package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
  5. package/mcp-server/benchmarks/policy_dominance_test.js +221 -0
  6. package/mcp-server/benchmarks/quality_evaluator.js +598 -0
  7. package/mcp-server/benchmarks/raw_corpus_data.js +613 -0
  8. package/mcp-server/benchmarks/run_benchmarks.js +366 -0
  9. package/mcp-server/benchmarks/stress_ingestion.js +195 -0
  10. package/mcp-server/benchmarks/table_code_retrieval.js +453 -0
  11. package/mcp-server/benchmarks/test_dual_layer.js +141 -0
  12. package/mcp-server/cli/direct_commands.js +39 -0
  13. package/mcp-server/cli.js +16 -5
  14. package/mcp-server/cli_boot.js +4 -1
  15. package/mcp-server/client_cli.js +73 -0
  16. package/mcp-server/client_paths.js +44 -0
  17. package/mcp-server/client_registration.js +38 -0
  18. package/mcp-server/codex_config.js +86 -8
  19. package/mcp-server/db/database.js +14 -21
  20. package/mcp-server/db/migrations.js +66 -77
  21. package/mcp-server/db/rag_blob_transport.js +143 -0
  22. package/mcp-server/db/rag_sync.js +284 -0
  23. package/mcp-server/db/sync_queue.js +219 -307
  24. package/mcp-server/dev_link.js +142 -0
  25. package/mcp-server/fact_format.js +44 -12
  26. package/mcp-server/index.js +17 -7
  27. package/mcp-server/ingest/exporter.js +44 -38
  28. package/mcp-server/ingest/pipeline.js +260 -248
  29. package/mcp-server/persona_migration.js +39 -0
  30. package/mcp-server/prompt_manager.js +162 -55
  31. package/mcp-server/rag_scope.js +83 -0
  32. package/mcp-server/retrieval/retriever.js +99 -64
  33. package/mcp-server/setup.js +150 -100
  34. package/mcp-server/storage/blob_store.js +53 -1
  35. package/mcp-server/tools/core/knowledge_read_core.js +163 -0
  36. package/mcp-server/tools/core/memory_core.js +24 -4
  37. package/mcp-server/tools/core/memory_routing.js +10 -0
  38. package/mcp-server/tools/core/note_core.js +53 -0
  39. package/mcp-server/tools/core/rag_query_core.js +169 -0
  40. package/mcp-server/tools/index.js +11 -9
  41. package/mcp-server/tools/memory_tools.js +4 -1
  42. package/mcp-server/tools/note_tools.js +35 -0
  43. package/mcp-server/tools/rag_tools.js +211 -364
  44. package/mcp-server/uninstall.js +627 -0
  45. package/opencode-plugin/index.js +80 -12
  46. package/opencode-plugin/main.js +136 -0
  47. package/package.json +17 -34
  48. package/skills/using-memory/SKILL.md +28 -19
@@ -0,0 +1,627 @@
1
+ import { readFile, writeFile, mkdir, rm, realpath, readdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { isAbsolute, join, parse, relative, resolve } from "node:path";
4
+ import readline from "node:readline";
5
+ import { fileURLToPath } from "node:url";
6
+ import { resolveClientPaths } from "./client_paths.js";
7
+ import { isMemoryMcpServerEntry, isMemoryPluginEntry, readJsonConfig } from "./client_registration.js";
8
+ import { cliFailureMessage, runClientCli } from "./client_cli.js";
9
+
10
+ export { isMemoryMcpServerEntry, isMemoryPluginEntry } from "./client_registration.js";
11
+
12
+ export function openCodeCacheTargets(cachePackages) {
13
+ return [
14
+ join(cachePackages, "@lotargo", "memory_plugin"),
15
+ join(cachePackages, "@lotargo", "memory_plugin@latest"),
16
+ join(cachePackages, "memory_plugin"),
17
+ join(cachePackages, "memory_plugin@latest"),
18
+ join(cachePackages, "opencode-memory-plugin"),
19
+ join(cachePackages, "opencode-memory-plugin@latest"),
20
+ ];
21
+ }
22
+
23
+ export async function discoverOpenCodeCacheTargets(cachePackages) {
24
+ const targets = new Set(openCodeCacheTargets(cachePackages));
25
+ const locations = [
26
+ { dir: cachePackages, match: /^(?:memory_plugin|opencode-memory-plugin)(?:@[^/]+)?$/i },
27
+ { dir: join(cachePackages, "@lotargo"), match: /^memory_plugin(?:@[^/]+)?$/i },
28
+ ];
29
+ for (const location of locations) {
30
+ try {
31
+ const entries = await readdir(location.dir, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ if ((entry.isDirectory() || entry.isSymbolicLink()) && location.match.test(entry.name)) {
34
+ targets.add(join(location.dir, entry.name));
35
+ }
36
+ }
37
+ } catch {}
38
+ }
39
+ return [...targets];
40
+ }
41
+
42
+ const PACKAGED_SKILL_FILE = fileURLToPath(new URL("../skills/using-memory/SKILL.md", import.meta.url));
43
+
44
+ export async function isOwnedSkillDir(skillDir) {
45
+ try {
46
+ const entries = await readdir(skillDir, { withFileTypes: true });
47
+ if (entries.length !== 1 || !entries[0].isFile() || entries[0].name !== "SKILL.md") return false;
48
+ const [installed, packaged] = await Promise.all([
49
+ readFile(join(skillDir, "SKILL.md")),
50
+ readFile(PACKAGED_SKILL_FILE),
51
+ ]);
52
+ return installed.equals(packaged);
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function isSameOrAncestor(candidate, protectedPath) {
59
+ const rel = relative(candidate, protectedPath);
60
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
61
+ }
62
+
63
+ export function assertSafePurgeTarget(target, { protectedPaths = [] } = {}) {
64
+ if (!target || !String(target).trim()) throw new Error("Refusing to purge an empty path");
65
+ const resolved = resolve(String(target));
66
+ const root = parse(resolved).root;
67
+ if (resolved === root) throw new Error(`Refusing to purge filesystem root: ${resolved}`);
68
+ const depth = relative(root, resolved).split(/[\\/]+/).filter(Boolean).length;
69
+ if (depth < 2) throw new Error(`Refusing to purge broad top-level path: ${resolved}`);
70
+ for (const protectedPath of protectedPaths.filter(Boolean)) {
71
+ const protectedResolved = resolve(String(protectedPath));
72
+ if (isSameOrAncestor(resolved, protectedResolved)) {
73
+ throw new Error(`Refusing to purge ${resolved}; it contains protected path ${protectedResolved}`);
74
+ }
75
+ }
76
+ return resolved;
77
+ }
78
+
79
+ function parseArgs(rawArgs) {
80
+ const args = rawArgs.map((a) => String(a));
81
+ const lower = args.map((a) => a.toLowerCase());
82
+ const has = (name) => lower.includes(name.toLowerCase());
83
+ const hasSpecificFlag = ["--opencode", "--claude", "--codex", "--antigravity", "--gemini"].some((f) => has(f));
84
+ const doOpenCode = !hasSpecificFlag || has("--opencode");
85
+ const doClaude = !hasSpecificFlag || has("--claude");
86
+ const doAntigravity = !hasSpecificFlag || has("--antigravity");
87
+ const doGemini = !hasSpecificFlag || has("--gemini");
88
+ const doCodex = !hasSpecificFlag || has("--codex");
89
+ const purge = has("--purge") || has("--hard") || has("--with-data") || has("--purge-data") || has("--hard-purge");
90
+ const purgeCache = has("--purge-cache");
91
+ const dryRun = has("--dry-run") || has("--dry");
92
+ const yes = has("--yes") || has("-y") || has("--force") || has("-f") || has("--assume-yes");
93
+ const help = has("--help") || has("-h") || has("help");
94
+ return { args, lower, has, hasSpecificFlag, doOpenCode, doClaude, doAntigravity, doGemini, doCodex, purge, purgeCache, dryRun, yes, help };
95
+ }
96
+
97
+ function printHelp() {
98
+ console.log(`
99
+ memory_plugin uninstall — remove @lotargo/memory_plugin from all clients
100
+
101
+ Usage:
102
+ memory_plugin uninstall [options]
103
+ memory_plugin setup --uninstall [options]
104
+ memory-cli uninstall [options]
105
+ npx @lotargo/memory_plugin uninstall [options]
106
+
107
+ Options:
108
+ --opencode Only remove OpenCode plugin entry
109
+ --claude Only remove Claude Code MCP entry
110
+ --codex Only remove Codex MCP entry
111
+ --antigravity Only remove Antigravity MCP entry
112
+ --gemini Only remove Gemini CLI MCP entry
113
+ (no flag) Remove from all detected clients
114
+ --purge Also delete local data (MEMORY_DIR, memory-agent prompt state, blobs, SQLite)
115
+ Without --purge, Notebook facts / RAG / models are kept on disk.
116
+ Unsafe broad targets are rejected even with --yes.
117
+ --purge-cache Also delete only this plugin's OpenCode package-cache directories
118
+ --dry-run Preview what would be removed without writing
119
+ --yes, -y, --force Skip confirmation prompt for --purge
120
+ -h, --help Show this help
121
+
122
+ What is removed by default (without --purge):
123
+ • OpenCode: plugin entry from ~/.config/opencode/opencode.json (incl. file:// dev link)
124
+ • Claude: mcpServers.memory-agent from ~/.claude.json
125
+ • Gemini CLI: mcpServers.memory-agent from ~/.gemini/settings.json
126
+ • Antigravity: mcpServers.memory-agent from ~/.gemini/config/mcp_config.json + .agents/mcp_config.json
127
+ • Codex: [mcp_servers.memory-agent] from ~/.codex/config.toml (only if owned by this plugin)
128
+ • Prompts: managed blocks from Codex, Claude, Gemini CLI, and Antigravity global instruction files
129
+ • Skills: using-memory skill from each client's skills/ directory
130
+
131
+ With --purge also removes:
132
+ • Local Notebook & RAG storage (MEMORY_DIR) and prompt state (XDG_CONFIG_HOME/memory-agent)
133
+ With --purge-cache also removes:
134
+ • Exact OpenCode cache directories belonging to @lotargo/memory_plugin
135
+
136
+ The npm package itself is removed separately:
137
+ npm uninstall -g @lotargo/memory_plugin
138
+
139
+ Examples:
140
+ memory_plugin uninstall --dry-run
141
+ memory_plugin uninstall --purge --yes
142
+ memory_plugin uninstall --opencode --purge-cache
143
+ memory_plugin uninstall --opencode --claude
144
+ `);
145
+ }
146
+
147
+ async function confirmPurge(yes, targets = []) {
148
+ if (yes) return true;
149
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
150
+ const answer = await new Promise((resolve) => {
151
+ const targetList = targets.map((target) => `\n - ${target}`).join("");
152
+ rl.question(` This will PERMANENTLY delete:${targetList}\n Continue? [y/N] `, (a) => {
153
+ rl.close();
154
+ resolve(String(a).trim().toLowerCase());
155
+ });
156
+ });
157
+ return answer === "y" || answer === "yes";
158
+ }
159
+
160
+ async function removeJsonMcpClient({ label, configPath, cliName, cliArgs, dry }) {
161
+ if (!existsSync(configPath)) return { status: "not_found" };
162
+ let config = await readJsonConfig(configPath);
163
+ const entry = config.mcpServers?.["memory-agent"];
164
+ if (!entry) return { status: "not_found" };
165
+ if (!isMemoryMcpServerEntry(entry)) return { status: "conflict" };
166
+ if (dry) return { status: "removed", method: "preview" };
167
+
168
+ const native = runClientCli(cliName, cliArgs);
169
+ if (native.ok) {
170
+ config = await readJsonConfig(configPath);
171
+ if (!config.mcpServers?.["memory-agent"]) {
172
+ return { status: "removed", method: "native" };
173
+ }
174
+ if (!isMemoryMcpServerEntry(config.mcpServers["memory-agent"])) {
175
+ return { status: "conflict", reason: `native ${label} command left an unrecognized entry` };
176
+ }
177
+ }
178
+
179
+ config = await readJsonConfig(configPath);
180
+ const afterNative = config.mcpServers?.["memory-agent"];
181
+ if (!afterNative) return { status: "removed", method: "native" };
182
+ if (!isMemoryMcpServerEntry(afterNative)) return { status: "conflict" };
183
+ delete config.mcpServers["memory-agent"];
184
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
185
+ return {
186
+ status: "removed",
187
+ method: "fallback",
188
+ nativeReason: native.ok ? "native command did not remove the expected entry" : cliFailureMessage(native),
189
+ };
190
+ }
191
+
192
+ export async function runUninstall() {
193
+ const rawArgs = process.argv.slice(2);
194
+ // strip leading "uninstall" / "setup" / "--uninstall" tokens so parsing is uniform
195
+ // e.g. ["uninstall","--purge"] or ["setup","--uninstall","--purge"] or ["--uninstall"]
196
+ const filtered = rawArgs.filter((a, idx) => {
197
+ const low = String(a).toLowerCase();
198
+ if (low === "uninstall" || low === "--uninstall") return false;
199
+ if (low === "setup" && idx === 0) return false;
200
+ if (low === "install" && idx === 0) return false;
201
+ return true;
202
+ });
203
+ const opts = parseArgs(filtered);
204
+
205
+ if (opts.help) {
206
+ printHelp();
207
+ return;
208
+ }
209
+
210
+ const clientPaths = resolveClientPaths();
211
+ const { home } = clientPaths;
212
+ const dry = opts.dryRun;
213
+ const tag = dry ? "[DRY-RUN]" : "[OK]";
214
+
215
+ let purgeDirs = [];
216
+ if (opts.purge) {
217
+ const { MEMORY_DIR } = await import("./memory.js");
218
+ const candidates = [
219
+ { path: MEMORY_DIR, label: "Local data (MEMORY_DIR)" },
220
+ { path: clientPaths.agentConfigDir, label: "Prompt state (memory-agent config)" },
221
+ ];
222
+ const seen = new Set();
223
+ for (const candidate of candidates) {
224
+ const protectedPaths = [home, clientPaths.cwd, clientPaths.configHome, clientPaths.opencodeDir, clientPaths.cacheHome];
225
+ let safePath = assertSafePurgeTarget(candidate.path, { protectedPaths });
226
+ if (existsSync(safePath)) {
227
+ const real = await realpath(safePath);
228
+ assertSafePurgeTarget(real, { protectedPaths });
229
+ }
230
+ const key = process.platform === "win32" ? safePath.toLowerCase() : safePath;
231
+ if (!seen.has(key)) {
232
+ seen.add(key);
233
+ purgeDirs.push({ ...candidate, path: safePath });
234
+ }
235
+ }
236
+ }
237
+
238
+ console.log(`\n${dry ? "Previewing" : "Removing"} @lotargo/memory_plugin${opts.purge ? " (with --purge)" : ""}...\n`);
239
+
240
+ if (opts.purge) {
241
+ console.log(" Validated purge targets:");
242
+ for (const target of purgeDirs) console.log(` - ${target.path}`);
243
+ console.log("");
244
+ }
245
+
246
+ if (opts.purge && !dry) {
247
+ const ok = await confirmPurge(opts.yes, purgeDirs.map((item) => item.path));
248
+ if (!ok) {
249
+ console.log(" [CANCELLED] Purge aborted by user.\n");
250
+ return;
251
+ }
252
+ }
253
+
254
+ let removedCount = 0;
255
+ let skippedCount = 0;
256
+ let failureCount = 0;
257
+
258
+ // 1. OpenCode
259
+ if (opts.doOpenCode) {
260
+ try {
261
+ const { opencodeDir, opencodeConfigPath } = clientPaths;
262
+ if (!existsSync(opencodeConfigPath)) {
263
+ console.log(` [SKIP] OpenCode: no config at ${opencodeConfigPath}`);
264
+ skippedCount++;
265
+ } else {
266
+ let config = await readJsonConfig(opencodeConfigPath);
267
+ const fields = ["plugin", "plugins"];
268
+ const ownedCount = fields.reduce((count, field) => count + (
269
+ Array.isArray(config[field]) ? config[field].filter(isMemoryPluginEntry).length : 0
270
+ ), 0);
271
+ if (ownedCount === 0) {
272
+ console.log(` [SKIP] OpenCode: no plugin entries in ${opencodeConfigPath}`);
273
+ skippedCount++;
274
+ } else {
275
+ let method = "preview";
276
+ let nativeReason = null;
277
+ if (!dry) {
278
+ const native = runClientCli("opencode2", ["plugin", "remove", "@lotargo/memory_plugin"]);
279
+ if (native.ok) {
280
+ config = await readJsonConfig(opencodeConfigPath);
281
+ method = fields.every((field) => !Array.isArray(config[field]) || !config[field].some(isMemoryPluginEntry))
282
+ ? "native"
283
+ : "fallback";
284
+ if (method === "fallback") nativeReason = "native command did not remove every owned config entry";
285
+ } else {
286
+ method = "fallback";
287
+ nativeReason = cliFailureMessage(native);
288
+ }
289
+
290
+ if (method === "fallback") {
291
+ for (const field of fields) {
292
+ if (!Array.isArray(config[field])) continue;
293
+ const filtered = config[field].filter((entry) => !isMemoryPluginEntry(entry));
294
+ if (filtered.length === 0) delete config[field];
295
+ else config[field] = filtered;
296
+ }
297
+ await mkdir(opencodeDir, { recursive: true });
298
+ await writeFile(opencodeConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
299
+ }
300
+ }
301
+ console.log(` ${tag} OpenCode: removed ${ownedCount} plugin entrie(s) via ${method === "native" ? "opencode2 plugin remove" : method === "preview" ? "native command or ownership-checked fallback" : "ownership-checked config fallback"}${dry ? " (would remove)" : ""}`);
302
+ if (nativeReason) console.log(` [INFO] OpenCode fallback: ${nativeReason}`);
303
+ removedCount++;
304
+ }
305
+ }
306
+
307
+ if (opts.purgeCache) {
308
+ let cacheRemoved = 0;
309
+ for (const target of await discoverOpenCodeCacheTargets(clientPaths.opencodeCachePackages)) {
310
+ if (!existsSync(target)) continue;
311
+ if (!dry) await rm(target, { recursive: true, force: true });
312
+ console.log(` ${tag} OpenCode cache: removed ${target}${dry ? " (would remove)" : ""}`);
313
+ cacheRemoved++;
314
+ }
315
+ if (cacheRemoved > 0) removedCount += cacheRemoved;
316
+ else {
317
+ console.log(` [SKIP] OpenCode cache: no owned package-cache directories found`);
318
+ skippedCount++;
319
+ }
320
+ }
321
+ } catch (err) {
322
+ console.log(` [FAIL] OpenCode: ${err.message}`);
323
+ failureCount++;
324
+ }
325
+ }
326
+
327
+ // 2. Claude Code
328
+ if (opts.doClaude) {
329
+ try {
330
+ const result = await removeJsonMcpClient({
331
+ label: "Claude Code",
332
+ configPath: clientPaths.claudeConfigPath,
333
+ cliName: "claude",
334
+ cliArgs: ["mcp", "remove", "--scope", "user", "memory-agent"],
335
+ dry,
336
+ });
337
+ if (result.status === "not_found") {
338
+ console.log(` [SKIP] Claude Code: owned mcpServers.memory-agent not found`);
339
+ skippedCount++;
340
+ } else if (result.status === "conflict") {
341
+ console.log(` [WARN] Claude Code: mcpServers.memory-agent is not owned by this plugin — skipped`);
342
+ skippedCount++;
343
+ } else {
344
+ console.log(` ${tag} Claude Code: removed MCP registration via ${result.method === "native" ? "claude mcp remove" : result.method === "preview" ? "native command or ownership-checked fallback" : "ownership-checked JSON fallback"}${dry ? " (would remove)" : ""}`);
345
+ if (result.nativeReason) console.log(` [INFO] Claude Code fallback: ${result.nativeReason}`);
346
+ removedCount++;
347
+ }
348
+ } catch (err) {
349
+ console.log(` [FAIL] Claude Code: ${err.message}`);
350
+ failureCount++;
351
+ }
352
+ }
353
+
354
+ // 3. Antigravity
355
+ if (opts.doAntigravity) {
356
+ try {
357
+ const geminiConfigFile = clientPaths.antigravityMcpConfigPath;
358
+ let didGlobal = false;
359
+ if (existsSync(geminiConfigFile)) {
360
+ const config = await readJsonConfig(geminiConfigFile);
361
+ const memoryEntry = config.mcpServers?.["memory-agent"];
362
+ if (memoryEntry && isMemoryMcpServerEntry(memoryEntry)) {
363
+ if (!dry) {
364
+ delete config.mcpServers["memory-agent"];
365
+ // keep empty mcpServers object (do not delete key) to preserve file structure
366
+ await mkdir(clientPaths.antigravityConfigDir, { recursive: true });
367
+ await writeFile(geminiConfigFile, JSON.stringify(config, null, 2) + "\n", "utf-8");
368
+ }
369
+ console.log(` ${tag} Antigravity: removed mcpServers.memory-agent from ${geminiConfigFile}${dry ? " (would remove)" : ""}`);
370
+ didGlobal = true;
371
+ removedCount++;
372
+ } else if (memoryEntry) {
373
+ console.log(` [WARN] Antigravity: mcpServers.memory-agent is not owned by this plugin — skipped`);
374
+ didGlobal = true;
375
+ skippedCount++;
376
+ }
377
+ }
378
+ if (!didGlobal) {
379
+ console.log(` [SKIP] Antigravity (global): mcpServers.memory-agent not found in ${geminiConfigFile}`);
380
+ skippedCount++;
381
+ }
382
+
383
+ // local .agents/mcp_config.json
384
+ const localMcpFile = clientPaths.localAgentsMcpConfigPath;
385
+ if (existsSync(localMcpFile)) {
386
+ try {
387
+ const localConfig = await readJsonConfig(localMcpFile);
388
+ const memoryEntry = localConfig.mcpServers?.["memory-agent"];
389
+ if (memoryEntry && isMemoryMcpServerEntry(memoryEntry)) {
390
+ if (!dry) {
391
+ delete localConfig.mcpServers["memory-agent"];
392
+ await writeFile(localMcpFile, JSON.stringify(localConfig, null, 2) + "\n", "utf-8");
393
+ }
394
+ console.log(` ${tag} Antigravity (local): removed mcpServers.memory-agent from ${localMcpFile}${dry ? " (would remove)" : ""}`);
395
+ removedCount++;
396
+ } else if (memoryEntry) {
397
+ console.log(` [WARN] Antigravity (local): mcpServers.memory-agent is not owned by this plugin — skipped`);
398
+ skippedCount++;
399
+ } else {
400
+ console.log(` [SKIP] Antigravity (local): mcpServers.memory-agent not found in ${localMcpFile}`);
401
+ skippedCount++;
402
+ }
403
+ } catch (e) {
404
+ console.log(` [FAIL] Antigravity (local): ${e.message}`);
405
+ failureCount++;
406
+ }
407
+ }
408
+ } catch (err) {
409
+ console.log(` [FAIL] Antigravity: ${err.message}`);
410
+ failureCount++;
411
+ }
412
+ }
413
+
414
+ // 4. Gemini CLI
415
+ if (opts.doGemini) {
416
+ try {
417
+ const result = await removeJsonMcpClient({
418
+ label: "Gemini CLI",
419
+ configPath: clientPaths.geminiSettingsPath,
420
+ cliName: "gemini",
421
+ cliArgs: ["mcp", "remove", "--scope", "user", "memory-agent"],
422
+ dry,
423
+ });
424
+ if (result.status === "not_found") {
425
+ console.log(` [SKIP] Gemini CLI: owned mcpServers.memory-agent not found`);
426
+ skippedCount++;
427
+ } else if (result.status === "conflict") {
428
+ console.log(` [WARN] Gemini CLI: mcpServers.memory-agent is not owned by this plugin — skipped`);
429
+ skippedCount++;
430
+ } else {
431
+ console.log(` ${tag} Gemini CLI: removed MCP registration via ${result.method === "native" ? "gemini mcp remove" : result.method === "preview" ? "native command or ownership-checked fallback" : "ownership-checked settings.json fallback"}${dry ? " (would remove)" : ""}`);
432
+ if (result.nativeReason) console.log(` [INFO] Gemini CLI fallback: ${result.nativeReason}`);
433
+ removedCount++;
434
+ }
435
+ } catch (err) {
436
+ console.log(` [FAIL] Gemini CLI: ${err.message}`);
437
+ failureCount++;
438
+ }
439
+ }
440
+
441
+ // 5. Codex
442
+ if (opts.doCodex) {
443
+ try {
444
+ const codexConfig = clientPaths.codexConfigPath;
445
+ if (!existsSync(codexConfig)) {
446
+ console.log(` [SKIP] Codex: no config at ${codexConfig}`);
447
+ skippedCount++;
448
+ } else {
449
+ let content = await readFile(codexConfig, "utf-8");
450
+ const { removeCodexMemoryAgentConfig } = await import("./codex_config.js");
451
+ let result = removeCodexMemoryAgentConfig(content);
452
+ if (result.status === "not_found") {
453
+ console.log(` [SKIP] Codex: [mcp_servers.memory-agent] not found`);
454
+ skippedCount++;
455
+ } else if (result.status === "conflict") {
456
+ console.log(` [WARN] Codex: ${result.reason} — skipped to avoid touching foreign config`);
457
+ skippedCount++;
458
+ } else if (result.changed) {
459
+ let method = "preview";
460
+ let nativeReason = null;
461
+ if (!dry) {
462
+ const native = runClientCli("codex", ["mcp", "remove", "memory-agent"]);
463
+ if (native.ok) {
464
+ content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
465
+ result = removeCodexMemoryAgentConfig(content);
466
+ if (result.status === "not_found") method = "native";
467
+ else if (result.status === "conflict") {
468
+ throw new Error("codex mcp remove left an unrecognized memory-agent section");
469
+ } else {
470
+ method = "fallback";
471
+ nativeReason = "native command did not remove the expected section";
472
+ }
473
+ } else {
474
+ method = "fallback";
475
+ nativeReason = cliFailureMessage(native);
476
+ }
477
+ if (method === "fallback" && result.changed) {
478
+ await writeFile(codexConfig, result.content, "utf-8");
479
+ }
480
+ }
481
+ console.log(` ${tag} Codex: removed memory-agent via ${method === "native" ? "codex mcp remove" : method === "preview" ? "native command or ownership-checked fallback" : "ownership-checked TOML fallback"}${dry ? " (would remove)" : ""}`);
482
+ if (nativeReason) console.log(` [INFO] Codex fallback: ${nativeReason}`);
483
+ removedCount++;
484
+ } else {
485
+ console.log(` [SKIP] Codex: already clean`);
486
+ skippedCount++;
487
+ }
488
+ }
489
+ } catch (err) {
490
+ console.log(` [FAIL] Codex: ${err.message}`);
491
+ failureCount++;
492
+ }
493
+ }
494
+
495
+ // 6. Global Prompt blocks
496
+ try {
497
+ const { disableGlobalPrompt, getGlobalPromptStatus } = await import("./prompt_manager.js");
498
+ const promptTargets = [];
499
+ if (opts.doCodex) promptTargets.push("Codex");
500
+ if (opts.doClaude) promptTargets.push("Claude Code");
501
+ if (opts.doAntigravity) promptTargets.push("Antigravity");
502
+ if (opts.doGemini) promptTargets.push("Gemini CLI");
503
+ if (promptTargets.length > 0) {
504
+ if (dry) {
505
+ const status = await getGlobalPromptStatus(promptTargets);
506
+ for (const s of status) {
507
+ if (s.enabled) {
508
+ console.log(` ${tag} ${s.name}: would remove prompt block from ${s.filePath}`);
509
+ removedCount++;
510
+ } else {
511
+ console.log(` [SKIP] ${s.name}: prompt block not present`);
512
+ skippedCount++;
513
+ }
514
+ }
515
+ } else {
516
+ const results = await disableGlobalPrompt(promptTargets);
517
+ for (const r of results) {
518
+ if (r.status === "disabled" || r.status === "removed_file") {
519
+ console.log(` [OK] ${r.name}: removed prompt block from ${r.filePath}`);
520
+ removedCount++;
521
+ } else if (r.status === "skipped") {
522
+ console.log(` [SKIP] ${r.name}: prompt block not found`);
523
+ skippedCount++;
524
+ } else if (r.status === "failed") {
525
+ console.log(` [FAIL] ${r.name}: ${r.error}`);
526
+ failureCount++;
527
+ }
528
+ }
529
+ }
530
+ } else {
531
+ console.log(` [SKIP] Prompts: --opencode only, no AGENTS.md prompts to remove`);
532
+ skippedCount++;
533
+ }
534
+ } catch (err) {
535
+ console.log(` [FAIL] Prompts: ${err.message}`);
536
+ failureCount++;
537
+ }
538
+
539
+ // 7. Skills
540
+ try {
541
+ const rawSkills = [
542
+ opts.doOpenCode ? { name: "OpenCode", dir: join(clientPaths.opencodeDir, "skills", "using-memory") } : null,
543
+ opts.doAntigravity ? { name: "Antigravity", dir: join(clientPaths.antigravityConfigDir, "skills", "using-memory") } : null,
544
+ opts.doGemini ? { name: "Gemini CLI", dir: join(clientPaths.geminiSkillsDir, "using-memory") } : null,
545
+ opts.doCodex ? { name: "Codex", dir: join(home, ".codex", "skills", "using-memory") } : null,
546
+ opts.doCodex ? { name: "Codex shared agents", dir: join(home, ".agents", "skills", "using-memory") } : null,
547
+ opts.doClaude ? { name: "Claude Code", dir: join(home, ".claude", "skills", "using-memory") } : null,
548
+ ].filter(Boolean);
549
+
550
+ // local Antigravity .agents/skills
551
+ const cwd = clientPaths.cwd;
552
+ if (opts.doAntigravity && existsSync(join(cwd, ".agents"))) {
553
+ rawSkills.push({ name: "Antigravity (local)", dir: join(cwd, ".agents", "skills", "using-memory") });
554
+ }
555
+
556
+ // Deduplicate by normalized path (e.g. Codex shared agents and Antigravity local may coincide)
557
+ const seen = new Set();
558
+ const homeSkills = [];
559
+ for (const target of rawSkills) {
560
+ const key = target.dir.replace(/\\/g, "/").toLowerCase();
561
+ if (seen.has(key)) continue;
562
+ seen.add(key);
563
+ homeSkills.push(target);
564
+ }
565
+
566
+ for (const target of homeSkills) {
567
+ if (existsSync(target.dir)) {
568
+ if (!await isOwnedSkillDir(target.dir)) {
569
+ console.log(` [WARN] ${target.name}: using-memory skill is modified or not owned by this plugin — skipped`);
570
+ skippedCount++;
571
+ continue;
572
+ }
573
+ if (!dry) {
574
+ await rm(target.dir, { recursive: true, force: true });
575
+ }
576
+ console.log(` ${tag} ${target.name}: removed skill at ${target.dir}${dry ? " (would remove)" : ""}`);
577
+ removedCount++;
578
+ } else {
579
+ console.log(` [SKIP] ${target.name}: skill not found at ${target.dir}`);
580
+ skippedCount++;
581
+ }
582
+ }
583
+ } catch (err) {
584
+ console.log(` [FAIL] Skills: ${err.message}`);
585
+ failureCount++;
586
+ }
587
+
588
+ // 8. Data purge
589
+ if (opts.purge) {
590
+ for (const d of purgeDirs) {
591
+ if (existsSync(d.path)) {
592
+ if (!dry) {
593
+ try {
594
+ await rm(d.path, { recursive: true, force: true });
595
+ } catch (e) {
596
+ console.log(` [FAIL] Purge ${d.label}: ${e.message}`);
597
+ failureCount++;
598
+ continue;
599
+ }
600
+ }
601
+ console.log(` ${tag} Purge: removed ${d.label} at ${d.path}${dry ? " (would remove)" : ""}`);
602
+ removedCount++;
603
+ } else {
604
+ console.log(` [SKIP] Purge: ${d.label} not found at ${d.path}`);
605
+ skippedCount++;
606
+ }
607
+ }
608
+ }
609
+
610
+ console.log("");
611
+ if (dry) {
612
+ console.log(`Preview complete. ${removedCount} item(s) would be removed, ${skippedCount} skipped. Re-run without --dry-run to apply.`);
613
+ if (!opts.purge) console.log(`Tip: add --purge to also delete local Notebook / RAG / blobs (kept by default).`);
614
+ } else {
615
+ console.log(`Uninstall complete. Removed ${removedCount} item(s), ${skippedCount} skipped.`);
616
+ if (!opts.purge) {
617
+ console.log(`Local Notebook / RAG data was kept. To delete it, run: memory_plugin uninstall --purge`);
618
+ }
619
+ console.log(`To remove the npm package itself (if installed globally), run: npm uninstall -g @lotargo/memory_plugin`);
620
+ console.log(`Restart OpenCode / Codex / Claude Code / Gemini CLI / Antigravity to apply changes.\n`);
621
+ }
622
+ if (failureCount > 0) {
623
+ process.exitCode = 1;
624
+ console.log(`Completed with ${failureCount} failure(s).\n`);
625
+ }
626
+ return { ok: failureCount === 0, removedCount, skippedCount, failureCount };
627
+ }