@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.
- package/CHANGELOG.md +34 -0
- package/README.md +576 -443
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -0
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/policy_dominance_test.js +221 -0
- package/mcp-server/benchmarks/quality_evaluator.js +598 -0
- package/mcp-server/benchmarks/raw_corpus_data.js +613 -0
- package/mcp-server/benchmarks/run_benchmarks.js +366 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -0
- package/mcp-server/benchmarks/table_code_retrieval.js +453 -0
- package/mcp-server/benchmarks/test_dual_layer.js +141 -0
- 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/rag_scope.js +83 -0
- 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 +17 -34
- 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
|
|
14
|
-
|
|
15
|
-
if (!
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,
|
|
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,
|
package/mcp-server/setup.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
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
|
|
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
|
|
66
|
-
const opencodeConfigPath = join(opencodeDir, "opencode.json");
|
|
106
|
+
const { opencodeDir, opencodeConfigPath } = clientPaths;
|
|
67
107
|
await mkdir(opencodeDir, { recursive: true });
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
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 =
|
|
121
|
+
const opencodeCachePackages = clientPaths.opencodeCachePackages;
|
|
96
122
|
if (existsSync(opencodeCachePackages)) {
|
|
97
123
|
try {
|
|
98
124
|
const { rm } = await import("fs/promises");
|
|
99
|
-
const targets = [
|
|
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 (
|
|
116
|
-
if (doClaude) {
|
|
117
|
-
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
169
|
+
const geminiConfigDir = clientPaths.geminiConfigDir;
|
|
143
170
|
await mkdir(geminiConfigDir, { recursive: true });
|
|
144
171
|
const geminiConfigFile = join(geminiConfigDir, "mcp_config.json");
|
|
145
172
|
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
config
|
|
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 =
|
|
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
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
localConfig
|
|
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.
|
|
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 =
|
|
198
|
-
const codexConfig =
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
217
|
-
: " [OK] Codex:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 =
|
|
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 =
|
|
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
|
}
|