@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
@@ -10,29 +10,51 @@ export function sanitizeFtsQuery(query) {
10
10
  return words.join(" OR ");
11
11
  }
12
12
 
13
- export async function bm25Search(db, query, limit = 30, scopeKeys = null) {
14
- const ftsQuery = sanitizeFtsQuery(query);
15
- if (!ftsQuery) return [];
16
-
17
- try {
18
- const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
19
- const scopeClause = scoped
20
- ? `AND EXISTS (
21
- SELECT 1 FROM micro_chunks scoped_m
22
- JOIN document_scopes scoped_ds ON scoped_ds.doc_id = scoped_m.doc_id
23
- WHERE scoped_m.id = micro_chunks_fts.id
24
- AND scoped_ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
25
- )`
26
- : "";
27
- const stmt = db.prepare(`
28
- SELECT id, content, breadcrumbs, rank
29
- FROM micro_chunks_fts
30
- WHERE micro_chunks_fts MATCH ?
31
- ${scopeClause}
32
- ORDER BY rank
33
- LIMIT ?;
34
- `);
35
- const rows = await stmt.all(ftsQuery, ...(scoped ? scopeKeys : []), limit);
13
+ export function parseDocumentMetadata(value) {
14
+ if (!value) return {};
15
+ if (typeof value === "object" && !Array.isArray(value)) return value;
16
+ if (typeof value !== "string") return {};
17
+ try {
18
+ const parsed = JSON.parse(value);
19
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ function normalizeRetrievedTags(tags) {
26
+ if (Array.isArray(tags)) {
27
+ return [...new Set(tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean))].sort();
28
+ }
29
+ if (typeof tags === "string") {
30
+ return [...new Set(tags.split(",").map((tag) => tag.trim().toLowerCase()).filter(Boolean))].sort();
31
+ }
32
+ return [];
33
+ }
34
+
35
+ export async function bm25Search(db, query, limit = 30, scopeKeys = null) {
36
+ const ftsQuery = sanitizeFtsQuery(query);
37
+ if (!ftsQuery) return [];
38
+
39
+ try {
40
+ const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
41
+ const scopeClause = scoped
42
+ ? `AND EXISTS (
43
+ SELECT 1 FROM micro_chunks scoped_m
44
+ JOIN document_scopes scoped_ds ON scoped_ds.doc_id = scoped_m.doc_id
45
+ WHERE scoped_m.id = micro_chunks_fts.id
46
+ AND scoped_ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
47
+ )`
48
+ : "";
49
+ const stmt = db.prepare(`
50
+ SELECT id, content, breadcrumbs, rank
51
+ FROM micro_chunks_fts
52
+ WHERE micro_chunks_fts MATCH ?
53
+ ${scopeClause}
54
+ ORDER BY rank
55
+ LIMIT ?;
56
+ `);
57
+ const rows = await stmt.all(ftsQuery, ...(scoped ? scopeKeys : []), limit);
36
58
  return rows.map((r, i) => ({
37
59
  id: r.id,
38
60
  content: r.content,
@@ -60,7 +82,7 @@ export function toVectorBytes(value) {
60
82
  return null;
61
83
  }
62
84
 
63
- export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25, scopeKeys = null) {
85
+ export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25, scopeKeys = null) {
64
86
  if (!queryVector || queryVector.length === 0) return [];
65
87
 
66
88
  const vectorDim = queryVector.length;
@@ -69,32 +91,32 @@ export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25, s
69
91
  const tempVec = new Float32Array(tempBuf);
70
92
 
71
93
  const scanLimit = Number(getConfig().vectorScanLimit) || 0;
72
- const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
73
- const scopeClause = scoped
74
- ? `WHERE EXISTS (
75
- SELECT 1 FROM document_scopes scoped_ds
76
- WHERE scoped_ds.doc_id = m.doc_id
77
- AND scoped_ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
78
- )`
79
- : "";
80
- const scanSql = scanLimit > 0
81
- ? `
82
- SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
83
- FROM micro_chunks m
84
- JOIN sections s ON m.section_id = s.id
85
- ${scopeClause}
86
- LIMIT ?;
87
- `
88
- : `
89
- SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
90
- FROM micro_chunks m
91
- JOIN sections s ON m.section_id = s.id
92
- ${scopeClause};
93
- `;
94
-
95
- const stmt = db.prepare(scanSql);
96
- const scopeParams = scoped ? scopeKeys : [];
97
- const rows = scanLimit > 0 ? await stmt.all(...scopeParams, scanLimit) : await stmt.all(...scopeParams);
94
+ const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
95
+ const scopeClause = scoped
96
+ ? `WHERE EXISTS (
97
+ SELECT 1 FROM document_scopes scoped_ds
98
+ WHERE scoped_ds.doc_id = m.doc_id
99
+ AND scoped_ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
100
+ )`
101
+ : "";
102
+ const scanSql = scanLimit > 0
103
+ ? `
104
+ SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
105
+ FROM micro_chunks m
106
+ JOIN sections s ON m.section_id = s.id
107
+ ${scopeClause}
108
+ LIMIT ?;
109
+ `
110
+ : `
111
+ SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
112
+ FROM micro_chunks m
113
+ JOIN sections s ON m.section_id = s.id
114
+ ${scopeClause};
115
+ `;
116
+
117
+ const stmt = db.prepare(scanSql);
118
+ const scopeParams = scoped ? scopeKeys : [];
119
+ const rows = scanLimit > 0 ? await stmt.all(...scopeParams, scanLimit) : await stmt.all(...scopeParams);
98
120
  const scored = [];
99
121
  for (const r of rows) {
100
122
  // node:sqlite returns BLOBs as plain Uint8Array (NOT Buffer), the Turso
@@ -265,8 +287,8 @@ export async function batchHybridQuery(queries, options = {}) {
265
287
  rerankerEnabled = null,
266
288
  instruction = null,
267
289
  generateEmbeddings = true,
268
- policyExpansion = null,
269
- scopeKeys = null,
290
+ policyExpansion = null,
291
+ scopeKeys = null,
270
292
  } = options;
271
293
 
272
294
  const db = customDb || await getDatabase();
@@ -299,8 +321,8 @@ export async function batchHybridQuery(queries, options = {}) {
299
321
  rerankerEnabled: useReranker,
300
322
  instruction,
301
323
  generateEmbeddings,
302
- policyExpansion: usePolicyExpansion,
303
- scopeKeys,
324
+ policyExpansion: usePolicyExpansion,
325
+ scopeKeys,
304
326
  _precomputedVector: queryVectors[i] || null,
305
327
  })
306
328
  )
@@ -322,8 +344,8 @@ export async function hybridQuery({
322
344
  rerankerEnabled = null,
323
345
  instruction = null,
324
346
  generateEmbeddings = true,
325
- policyExpansion = null, // null = use config default
326
- scopeKeys = null, // null = all documents; tool surfaces pass global/current-project keys
347
+ policyExpansion = null, // null = use config default
348
+ scopeKeys = null, // null = all documents; tool surfaces pass global/current-project keys
327
349
  _precomputedVector = null, // internal: skip embedText if batch already computed
328
350
  }) {
329
351
  const db = customDb || await getDatabase();
@@ -351,28 +373,28 @@ export async function hybridQuery({
351
373
  let fusedHits = [];
352
374
 
353
375
  if (algo === "lexical_only" || algo === "bm25_only") {
354
- const bm25Hits = await bm25Search(db, query, limit * 4, scopeKeys);
376
+ const bm25Hits = await bm25Search(db, query, limit * 4, scopeKeys);
355
377
  fusedHits = bm25Hits.map((hit) => ({
356
378
  ...hit,
357
379
  score: 1.0 / hit.bm25_rank,
358
380
  }));
359
381
  } else if (algo === "semantic_only" || algo === "vector_only") {
360
382
  const queryVector = await getQueryVector();
361
- const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10, scopeKeys);
383
+ const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10, scopeKeys);
362
384
  fusedHits = vectorHits.map((hit) => ({
363
385
  ...hit,
364
386
  score: hit.cosine_sim,
365
387
  }));
366
388
  } else if (algo === "rrf") {
367
- const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
389
+ const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
368
390
  const queryVector = await getQueryVector();
369
- const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
391
+ const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
370
392
  fusedHits = rrfFusion(bm25Hits, vectorHits, 60, scoreThreshold);
371
393
  } else {
372
394
  // Default: RSF
373
- const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
395
+ const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
374
396
  const queryVector = await getQueryVector();
375
- const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
397
+ const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
376
398
  fusedHits = rsfFusion(bm25Hits, vectorHits, alphaWeight, scoreThreshold);
377
399
  }
378
400
 
@@ -432,9 +454,11 @@ export async function hybridQuery({
432
454
  const topIds = topHits.map((h) => h.id);
433
455
  const placeholders = topIds.map(() => "?").join(",");
434
456
  const details = await db.prepare(`
435
- SELECT m.id as micro_id, m.retrieval_policy, m.policy_source_id,
457
+ SELECT m.id as micro_id, m.doc_id as doc_id, m.retrieval_policy, m.policy_source_id,
436
458
  s.id as section_id, s.heading, s.breadcrumbs, s.content as section_content,
437
- med.content as medium_content, d.title as doc_title, d.path as doc_path
459
+ med.content as medium_content,
460
+ d.title as doc_title, d.path as doc_path, d.metadata_json,
461
+ d.created_at as doc_created_at, d.updated_at as doc_updated_at
438
462
  FROM micro_chunks m
439
463
  JOIN sections s ON m.section_id = s.id
440
464
  JOIN documents d ON m.doc_id = d.id
@@ -476,6 +500,10 @@ export async function hybridQuery({
476
500
  if (!detail) continue;
477
501
 
478
502
  const symbols = symbolsBySection.get(detail.section_id) || [];
503
+ const metadata = parseDocumentMetadata(detail.metadata_json);
504
+ const sourceType = metadata.source_type || (String(detail.doc_path || "").startsWith("memory://note/") ? "note" : null);
505
+ const noteKind = sourceType === "note" ? (metadata.note_kind || "note") : null;
506
+ const tags = normalizeRetrievedTags(metadata.tags);
479
507
 
480
508
  let snippet = hit.content;
481
509
  let paragraphContext = detail.medium_content || hit.content;
@@ -490,8 +518,15 @@ export async function hybridQuery({
490
518
 
491
519
  results.push({
492
520
  chunk_id: hit.id,
521
+ doc_id: detail.doc_id,
493
522
  doc_title: detail.doc_title,
494
523
  doc_path: detail.doc_path,
524
+ source_type: sourceType,
525
+ note_kind: noteKind,
526
+ tags,
527
+ metadata,
528
+ doc_created_at: detail.doc_created_at ?? null,
529
+ doc_updated_at: detail.doc_updated_at ?? null,
495
530
  heading: detail.heading,
496
531
  breadcrumbs: detail.breadcrumbs,
497
532
  snippet,
@@ -1,19 +1,59 @@
1
1
  import { readFile, writeFile, mkdir, cp, readdir } from "fs/promises";
2
2
  import { existsSync } from "fs";
3
- import { join, dirname } from "path";
4
- import { homedir } from "os";
5
- import { fileURLToPath } from "url";
6
-
7
- export async function runSetup() {
8
- const args = process.argv.slice(2);
9
- const hasSpecificFlag = args.some((a) =>
10
- ["--opencode", "--claude", "--codex", "--antigravity", "--gemini"].includes(a.toLowerCase())
11
- );
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { resolveClientPaths } from "./client_paths.js";
6
+ import { cliFailureMessage, runClientCli } from "./client_cli.js";
7
+ import { MEMORY_MCP_ENTRY, isMemoryMcpServerEntry, isMemoryPluginEntry, readJsonConfig } from "./client_registration.js";
8
+
9
+ async function configureJsonMcpClient({ label, cliName, cliArgs, configPath }) {
10
+ let config = await readJsonConfig(configPath);
11
+ const existing = config.mcpServers?.["memory-agent"];
12
+ if (existing && !isMemoryMcpServerEntry(existing)) {
13
+ throw new Error(`Existing mcpServers.memory-agent in ${configPath} is not owned by this plugin`);
14
+ }
15
+ if (existing) return { method: "existing", configPath };
16
+
17
+ const native = runClientCli(cliName, cliArgs);
18
+ if (native.ok) {
19
+ config = await readJsonConfig(configPath);
20
+ if (isMemoryMcpServerEntry(config.mcpServers?.["memory-agent"])) {
21
+ return { method: "native", configPath };
22
+ }
23
+ }
24
+
25
+ // Compatibility fallback for older/missing clients, and for wrappers that
26
+ // report success without writing the expected user-scope configuration.
27
+ config = await readJsonConfig(configPath);
28
+ const afterNative = config.mcpServers?.["memory-agent"];
29
+ if (afterNative && !isMemoryMcpServerEntry(afterNative)) {
30
+ throw new Error(`Native ${label} setup created an unrecognized memory-agent entry in ${configPath}`);
31
+ }
32
+ if (!config.mcpServers || typeof config.mcpServers !== "object" || Array.isArray(config.mcpServers)) {
33
+ config.mcpServers = {};
34
+ }
35
+ config.mcpServers["memory-agent"] = MEMORY_MCP_ENTRY;
36
+ await mkdir(dirname(configPath), { recursive: true });
37
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
38
+ return {
39
+ method: "fallback",
40
+ configPath,
41
+ nativeReason: native.ok ? "native command did not create the expected entry" : cliFailureMessage(native),
42
+ };
43
+ }
12
44
 
13
- const doOpenCode = !hasSpecificFlag || args.includes("--opencode");
14
- const doClaude = !hasSpecificFlag || args.includes("--claude");
15
- const doAntigravity = !hasSpecificFlag || args.includes("--antigravity") || args.includes("--gemini");
16
- const doCodex = !hasSpecificFlag || args.includes("--codex");
45
+ export async function runSetup() {
46
+ const args = process.argv.slice(2);
47
+ const lowerArgs = args.map((arg) => String(arg).toLowerCase());
48
+ const hasSpecificFlag = lowerArgs.some((a) =>
49
+ ["--opencode", "--claude", "--codex", "--antigravity", "--gemini"].includes(a.toLowerCase())
50
+ );
51
+
52
+ const doOpenCode = !hasSpecificFlag || lowerArgs.includes("--opencode");
53
+ const doClaude = !hasSpecificFlag || lowerArgs.includes("--claude");
54
+ const doAntigravity = !hasSpecificFlag || lowerArgs.includes("--antigravity");
55
+ const doGemini = !hasSpecificFlag || lowerArgs.includes("--gemini");
56
+ const doCodex = !hasSpecificFlag || lowerArgs.includes("--codex");
17
57
 
18
58
  // Headless cloud setup: --api-key <TURSO_API_TOKEN> and/or --mode <only-local|only-cloud|hybrid-sync>
19
59
  const VALID_MODES = ["only-local", "only-cloud", "hybrid-sync"];
@@ -24,7 +64,8 @@ export async function runSetup() {
24
64
  }
25
65
 
26
66
  console.log("\nSetting up @lotargo/memory_plugin...\n");
27
- const home = homedir();
67
+ const clientPaths = resolveClientPaths();
68
+ const { home } = clientPaths;
28
69
  let configuredCount = 0;
29
70
 
30
71
  // 0. Headless cloud authentication (Google Jules / CI / VPS)
@@ -62,28 +103,13 @@ export async function runSetup() {
62
103
  // 1. OpenCode (~/.config/opencode/opencode.json)
63
104
  if (doOpenCode) {
64
105
  try {
65
- const opencodeDir = process.env.OPENCODE_CONFIG_DIR || join(home, ".config", "opencode");
66
- const opencodeConfigPath = join(opencodeDir, "opencode.json");
106
+ const { opencodeDir, opencodeConfigPath } = clientPaths;
67
107
  await mkdir(opencodeDir, { recursive: true });
68
108
 
69
- let config = {};
70
- if (existsSync(opencodeConfigPath)) {
71
- try {
72
- config = JSON.parse(await readFile(opencodeConfigPath, "utf-8"));
73
- } catch (e) {}
74
- }
75
- if (!Array.isArray(config.plugin)) config.plugin = [];
76
- // Clean up legacy / obsolete / duplicate entries of OUR plugin only
77
- const obsoleteNames = ["opencode-memory-plugin", "memory_plugin", "memory-plugin", "@lotargo/memory_plugin"];
78
- config.plugin = config.plugin.filter((p) => {
79
- if (typeof p !== "string") return true;
80
- if (obsoleteNames.includes(p)) return false;
81
- const normalized = p.replace(/\\/g, "/").toLowerCase();
82
- if (normalized.endsWith("/memory") || normalized.endsWith("/memory_plugin") || normalized.endsWith("/memory-plugin")) {
83
- return false;
84
- }
85
- return true;
86
- });
109
+ const config = await readJsonConfig(opencodeConfigPath);
110
+ if (!Array.isArray(config.plugin)) config.plugin = [];
111
+ // Clean up legacy / obsolete / duplicate entries of OUR plugin only
112
+ config.plugin = config.plugin.filter((entry) => !isMemoryPluginEntry(entry));
87
113
  config.plugin.push("@lotargo/memory_plugin");
88
114
  // Clean up legacy mcp-helper.js standalone file plugin if present
89
115
  const legacyPluginFile = join(opencodeDir, "plugins", "mcp-helper.js");
@@ -92,11 +118,18 @@ export async function runSetup() {
92
118
  }
93
119
 
94
120
  // Purge stale OpenCode package cache for memory plugin so OpenCode downloads latest version
95
- const opencodeCachePackages = join(home, ".cache", "opencode", "packages");
121
+ const opencodeCachePackages = clientPaths.opencodeCachePackages;
96
122
  if (existsSync(opencodeCachePackages)) {
97
123
  try {
98
124
  const { rm } = await import("fs/promises");
99
- const targets = ["@lotargo", "memory_plugin", "memory_plugin@latest", "opencode-memory-plugin", "opencode-memory-plugin@latest"];
125
+ const targets = [
126
+ join("@lotargo", "memory_plugin"),
127
+ join("@lotargo", "memory_plugin@latest"),
128
+ "memory_plugin",
129
+ "memory_plugin@latest",
130
+ "opencode-memory-plugin",
131
+ "opencode-memory-plugin@latest",
132
+ ];
100
133
  for (const t of targets) {
101
134
  const p = join(opencodeCachePackages, t);
102
135
  if (existsSync(p)) await rm(p, { recursive: true, force: true });
@@ -112,52 +145,42 @@ export async function runSetup() {
112
145
  }
113
146
  }
114
147
 
115
- // 2. Claude Code (~/.claude.json)
116
- if (doClaude) {
117
- try {
118
- const claudePath = join(home, ".claude.json");
119
- let config = {};
120
- if (existsSync(claudePath)) {
121
- try {
122
- config = JSON.parse(await readFile(claudePath, "utf-8"));
123
- } catch (e) {}
124
- }
125
- if (!config.mcpServers) config.mcpServers = {};
126
- config.mcpServers["memory-agent"] = {
127
- command: "npx",
128
- args: ["-y", "@lotargo/memory_plugin"],
129
- };
130
- await writeFile(claudePath, JSON.stringify(config, null, 2));
131
- console.log(" [OK] Claude Code: configured MCP server in ~/.claude.json");
132
- configuredCount++;
133
- } catch (err) {
134
- console.log(" [SKIP] Claude Code setup skipped:", err.message);
135
- }
136
- }
137
-
138
- // 3. Antigravity / Gemini CLI (~/.gemini/config/mcp_config.json & .agents/mcp_config.json)
148
+ // 2. Claude Code (native user-scope MCP lifecycle, JSON fallback)
149
+ if (doClaude) {
150
+ try {
151
+ const result = await configureJsonMcpClient({
152
+ label: "Claude Code",
153
+ cliName: "claude",
154
+ cliArgs: ["mcp", "add", "--scope", "user", "memory-agent", "--", "npx", "-y", "@lotargo/memory_plugin"],
155
+ configPath: clientPaths.claudeConfigPath,
156
+ });
157
+ console.log(` [OK] Claude Code: configured MCP server via ${result.method === "native" ? "claude mcp add" : result.method === "existing" ? "existing owned registration" : "ownership-checked JSON fallback"}`);
158
+ if (result.nativeReason) console.log(` [INFO] Claude Code fallback: ${result.nativeReason}`);
159
+ configuredCount++;
160
+ } catch (err) {
161
+ console.log(" [SKIP] Claude Code setup skipped:", err.message);
162
+ }
163
+ }
164
+
165
+ // 3. Antigravity (~/.gemini/config/mcp_config.json & .agents/mcp_config.json)
139
166
  if (doAntigravity) {
140
167
  try {
141
168
  // Global Antigravity config
142
- const geminiConfigDir = join(home, ".gemini", "config");
169
+ const geminiConfigDir = clientPaths.geminiConfigDir;
143
170
  await mkdir(geminiConfigDir, { recursive: true });
144
171
  const geminiConfigFile = join(geminiConfigDir, "mcp_config.json");
145
172
 
146
- let config = {};
147
- if (existsSync(geminiConfigFile)) {
148
- try {
149
- config = JSON.parse(await readFile(geminiConfigFile, "utf-8"));
150
- } catch (e) {}
151
- }
152
- if (!config.mcpServers) config.mcpServers = {};
153
- config.mcpServers["memory-agent"] = {
154
- command: "npx",
155
- args: ["-y", "@lotargo/memory_plugin"],
156
- };
157
- await writeFile(geminiConfigFile, JSON.stringify(config, null, 2));
173
+ const config = await readJsonConfig(geminiConfigFile);
174
+ if (!config.mcpServers) config.mcpServers = {};
175
+ const existingGlobal = config.mcpServers["memory-agent"];
176
+ if (existingGlobal && !isMemoryMcpServerEntry(existingGlobal)) {
177
+ throw new Error(`Existing Antigravity mcpServers.memory-agent in ${geminiConfigFile} is not owned by this plugin`);
178
+ }
179
+ config.mcpServers["memory-agent"] = MEMORY_MCP_ENTRY;
180
+ await writeFile(geminiConfigFile, JSON.stringify(config, null, 2) + "\n", "utf-8");
158
181
 
159
182
  // Local workspace config (.agents/mcp_config.json) only if .agents exists or --local flag is set
160
- const cwd = process.cwd();
183
+ const cwd = clientPaths.cwd;
161
184
  const hasAgentsDir = existsSync(join(cwd, ".agents"));
162
185
  const isLocalRequested = args.includes("--local");
163
186
 
@@ -165,18 +188,14 @@ export async function runSetup() {
165
188
  const localAgentsDir = join(cwd, ".agents");
166
189
  await mkdir(localAgentsDir, { recursive: true });
167
190
  const localMcpFile = join(localAgentsDir, "mcp_config.json");
168
- let localConfig = {};
169
- if (existsSync(localMcpFile)) {
170
- try {
171
- localConfig = JSON.parse(await readFile(localMcpFile, "utf-8"));
172
- } catch (e) {}
173
- }
174
- if (!localConfig.mcpServers) localConfig.mcpServers = {};
175
- localConfig.mcpServers["memory-agent"] = {
176
- command: "npx",
177
- args: ["-y", "@lotargo/memory_plugin"],
178
- };
179
- await writeFile(localMcpFile, JSON.stringify(localConfig, null, 2));
191
+ const localConfig = await readJsonConfig(localMcpFile);
192
+ if (!localConfig.mcpServers) localConfig.mcpServers = {};
193
+ const existingLocal = localConfig.mcpServers["memory-agent"];
194
+ if (existingLocal && !isMemoryMcpServerEntry(existingLocal)) {
195
+ throw new Error(`Existing Antigravity mcpServers.memory-agent in ${localMcpFile} is not owned by this plugin`);
196
+ }
197
+ localConfig.mcpServers["memory-agent"] = MEMORY_MCP_ENTRY;
198
+ await writeFile(localMcpFile, JSON.stringify(localConfig, null, 2) + "\n", "utf-8");
180
199
  console.log(" [OK] Antigravity: configured MCP server in ~/.gemini/config/mcp_config.json and .agents/mcp_config.json");
181
200
  } else {
182
201
  console.log(" [OK] Antigravity: configured MCP server in ~/.gemini/config/mcp_config.json");
@@ -185,17 +204,34 @@ export async function runSetup() {
185
204
  } catch (err) {
186
205
  console.log(" [SKIP] Antigravity setup skipped:", err.message);
187
206
  }
188
- }
189
-
190
- // 4. Codex (~/.codex/config.toml)
207
+ }
208
+
209
+ // 4. Gemini CLI (native user-scope MCP lifecycle, settings.json fallback)
210
+ if (doGemini) {
211
+ try {
212
+ const result = await configureJsonMcpClient({
213
+ label: "Gemini CLI",
214
+ cliName: "gemini",
215
+ cliArgs: ["mcp", "add", "--scope", "user", "memory-agent", "npx", "--", "-y", "@lotargo/memory_plugin"],
216
+ configPath: clientPaths.geminiSettingsPath,
217
+ });
218
+ console.log(` [OK] Gemini CLI: configured MCP server via ${result.method === "native" ? "gemini mcp add" : result.method === "existing" ? "existing owned registration" : "ownership-checked settings.json fallback"}`);
219
+ if (result.nativeReason) console.log(` [INFO] Gemini CLI fallback: ${result.nativeReason}`);
220
+ configuredCount++;
221
+ } catch (err) {
222
+ console.log(" [SKIP] Gemini CLI setup skipped:", err.message);
223
+ }
224
+ }
225
+
226
+ // 5. Codex (~/.codex/config.toml)
191
227
  if (doCodex) {
192
228
  try {
193
229
  const {
194
230
  updateCodexMemoryAgentConfig,
195
231
  validateCodexRuntime,
196
232
  } = await import("./codex_config.js");
197
- const codexDir = join(home, ".codex");
198
- const codexConfig = join(codexDir, "config.toml");
233
+ const codexDir = clientPaths.codexDir;
234
+ const codexConfig = clientPaths.codexConfigPath;
199
235
  const nodePath = process.execPath;
200
236
  const bootPath = fileURLToPath(new URL("./boot.js", import.meta.url));
201
237
  const runtime = validateCodexRuntime({ nodePath, nodeVersion: process.versions.node, bootPath });
@@ -204,32 +240,45 @@ export async function runSetup() {
204
240
  }
205
241
 
206
242
  await mkdir(codexDir, { recursive: true });
207
- const content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
208
- const update = updateCodexMemoryAgentConfig(content, { nodePath, bootPath });
243
+ let content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
244
+ let update = updateCodexMemoryAgentConfig(content, { nodePath, bootPath });
209
245
  if (update.status === "conflict") {
210
246
  throw new Error(update.reason);
211
247
  }
248
+ let nativeReason = null;
249
+ if (update.status === "added") {
250
+ const native = runClientCli("codex", ["mcp", "add", "memory-agent", "--", nodePath, bootPath]);
251
+ if (native.ok) {
252
+ content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
253
+ update = updateCodexMemoryAgentConfig(content, { nodePath, bootPath });
254
+ if (update.status === "conflict") throw new Error(update.reason);
255
+ } else {
256
+ nativeReason = cliFailureMessage(native);
257
+ }
258
+ }
212
259
  if (update.changed) {
213
260
  await writeFile(codexConfig, update.content, "utf-8");
214
261
  console.log(
215
262
  update.status === "added"
216
- ? " [OK] Codex: added direct Node.js memory-agent launcher to ~/.codex/config.toml"
217
- : " [OK] Codex: migrated memory-agent to a direct Node.js launcher in ~/.codex/config.toml"
263
+ ? " [OK] Codex: added direct Node.js memory-agent launcher via ownership-checked TOML fallback"
264
+ : " [OK] Codex: normalized memory-agent registration after native setup"
218
265
  );
219
266
  } else {
220
- console.log(" [INFO] Codex: direct Node.js memory-agent launcher already configured");
267
+ console.log(" [INFO] Codex: direct Node.js memory-agent launcher configured via codex mcp add or already present");
221
268
  }
269
+ if (nativeReason) console.log(` [INFO] Codex fallback: ${nativeReason}`);
222
270
  configuredCount++;
223
271
  } catch (err) {
224
272
  console.log(" [FAIL] Codex setup failed:", err.message);
225
273
  }
226
274
  }
227
275
 
228
- // 5. Global Prompt Instructions (Antigravity, Codex, Claude Code)
276
+ // 6. Global Prompt Instructions
229
277
  try {
230
278
  const { enableGlobalPrompt } = await import("./prompt_manager.js");
231
279
  const promptTargets = [];
232
280
  if (doAntigravity) promptTargets.push("Antigravity");
281
+ if (doGemini) promptTargets.push("Gemini CLI");
233
282
  if (doCodex) promptTargets.push("Codex");
234
283
  if (doClaude) promptTargets.push("Claude Code");
235
284
  const promptResults = await enableGlobalPrompt(promptTargets);
@@ -248,21 +297,22 @@ export async function runSetup() {
248
297
  console.log(" [SKIP] Global prompt setup skipped:", err.message);
249
298
  }
250
299
 
251
- // 6. Global & Local Skill Installation (Antigravity, Codex, Claude Code)
300
+ // 7. Global & Local Skill Installation
252
301
  try {
253
302
  const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
254
303
  const packageSkillsDir = join(packageDir, "skills");
255
304
  if (existsSync(packageSkillsDir)) {
256
- const opencodeDir = process.env.OPENCODE_CONFIG_DIR || join(home, ".config", "opencode");
305
+ const opencodeDir = clientPaths.opencodeDir;
257
306
  const targets = [];
258
307
  if (doOpenCode) targets.push({ name: "OpenCode", dir: join(opencodeDir, "skills") });
259
308
  if (doAntigravity) targets.push({ name: "Antigravity", dir: join(home, ".gemini", "config", "skills") });
309
+ if (doGemini) targets.push({ name: "Gemini CLI", dir: clientPaths.geminiSkillsDir });
260
310
  if (doCodex) {
261
311
  targets.push({ name: "Codex", dir: join(home, ".codex", "skills") });
262
312
  targets.push({ name: "Codex shared agents", dir: join(home, ".agents", "skills") });
263
313
  }
264
314
  if (doClaude) targets.push({ name: "Claude Code", dir: join(home, ".claude", "skills") });
265
- const cwd = process.cwd();
315
+ const cwd = clientPaths.cwd;
266
316
  if (doAntigravity && existsSync(join(cwd, ".agents"))) {
267
317
  targets.push({ name: "Antigravity (local)", dir: join(cwd, ".agents", "skills") });
268
318
  }