@lotargo/memory_plugin 1.6.3 → 1.6.5
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 +64 -1
- package/README.md +28 -20
- package/mcp-server/admin/snapshot.js +93 -26
- package/mcp-server/cli/direct_commands.js +5 -3
- package/mcp-server/cli/handlers/storage_actions.js +3 -1
- package/mcp-server/config/config_manager.js +0 -1
- package/mcp-server/db/migrations.js +26 -5
- package/mcp-server/db/sync_queue.js +118 -44
- package/mcp-server/graph/knowledge_linker.js +126 -19
- package/mcp-server/ingest/exporter.js +38 -9
- package/mcp-server/ingest/pipeline.js +146 -48
- package/mcp-server/memory.js +13 -3
- package/mcp-server/prompt_manager.js +10 -7
- package/mcp-server/retrieval/retriever.js +62 -38
- package/mcp-server/setup.js +18 -12
- package/mcp-server/tools/core/memory_core.js +465 -393
- package/mcp-server/tools/identity_tools.js +25 -6
- package/mcp-server/tools/memory_tools.js +138 -123
- package/mcp-server/tools/rag_tools.js +313 -249
- package/opencode-plugin/index.js +558 -440
- package/package.json +5 -5
- package/skills/using-memory/SKILL.md +152 -117
|
@@ -8,8 +8,10 @@ import { buildTripleHierarchy } from "./chunker.js";
|
|
|
8
8
|
import { embedBatch, vectorToBuffer } from "../ml/model_manager.js";
|
|
9
9
|
import { buildGraphEdges, saveGraphEdges } from "../graph/graph_extractor.js";
|
|
10
10
|
import { getConfig } from "../config/config_manager.js";
|
|
11
|
-
import { assertIngestPathAllowed } from "../security/path_guard.js";
|
|
12
|
-
import { logger } from "../logger.js";
|
|
11
|
+
import { assertIngestPathAllowed } from "../security/path_guard.js";
|
|
12
|
+
import { logger } from "../logger.js";
|
|
13
|
+
import { GLOBAL_KEY } from "../memory.js";
|
|
14
|
+
import { addDocumentScope } from "../rag_scope.js";
|
|
13
15
|
|
|
14
16
|
export async function ingestDocument({
|
|
15
17
|
content,
|
|
@@ -17,9 +19,10 @@ export async function ingestDocument({
|
|
|
17
19
|
path = null,
|
|
18
20
|
title = null,
|
|
19
21
|
customDb = null,
|
|
20
|
-
customBlobDir = BLOBS_DIR,
|
|
21
|
-
generateEmbeddings = true,
|
|
22
|
-
|
|
22
|
+
customBlobDir = BLOBS_DIR,
|
|
23
|
+
generateEmbeddings = true,
|
|
24
|
+
projectScope = GLOBAL_KEY,
|
|
25
|
+
}) {
|
|
23
26
|
const db = customDb || await getDatabase();
|
|
24
27
|
|
|
25
28
|
let effectiveType = type;
|
|
@@ -47,14 +50,58 @@ export async function ingestDocument({
|
|
|
47
50
|
const { markdown, title: docTitle, metadata } = await normalizeContent({ content, type: effectiveType, path: effectivePath, title: effectiveTitle });
|
|
48
51
|
if (type === "url") metadata.source_type = "url";
|
|
49
52
|
|
|
50
|
-
const blobRes = await saveBlob(markdown, customBlobDir);
|
|
51
|
-
const blobHash = blobRes.hash;
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const docPath = effectivePath || `virtual://${type}/${
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
53
|
+
const blobRes = await saveBlob(markdown, customBlobDir);
|
|
54
|
+
const blobHash = blobRes.hash;
|
|
55
|
+
|
|
56
|
+
const generatedDocId = `doc_${randomUUID().replace(/-/g, "").substring(0, 12)}`;
|
|
57
|
+
const docPath = effectivePath || `virtual://${type}/${generatedDocId}`;
|
|
58
|
+
const existingDoc = await db.prepare("SELECT * FROM documents WHERE path = ?").get(docPath);
|
|
59
|
+
const docId = existingDoc?.id || generatedDocId;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
|
|
62
|
+
// Identical re-ingestion only adds the new project/global scope. Keeping the
|
|
63
|
+
// existing document id preserves every Notebook link and avoids recomputing vectors.
|
|
64
|
+
if (existingDoc && existingDoc.checksum === blobHash) {
|
|
65
|
+
await db.exec("BEGIN IMMEDIATE;");
|
|
66
|
+
try {
|
|
67
|
+
await db
|
|
68
|
+
.prepare("UPDATE documents SET title = ?, metadata_json = ?, updated_at = ? WHERE id = ?;")
|
|
69
|
+
.run(docTitle, JSON.stringify(metadata), now, docId);
|
|
70
|
+
const assignedScope = await addDocumentScope(db, docId, projectScope);
|
|
71
|
+
await db.exec("COMMIT;");
|
|
72
|
+
|
|
73
|
+
const sectionsRow = await db.prepare("SELECT COUNT(*) AS cnt FROM sections WHERE doc_id = ?").get(docId);
|
|
74
|
+
const chunksRow = await db.prepare("SELECT COUNT(*) AS cnt FROM micro_chunks WHERE doc_id = ?").get(docId);
|
|
75
|
+
if (getConfig().mode === "hybrid-sync") {
|
|
76
|
+
try {
|
|
77
|
+
const { exportDocumentData } = await import("./exporter.js");
|
|
78
|
+
const { enqueueSyncTask } = await import("../db/sync_queue.js");
|
|
79
|
+
await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.error("Failed to queue document scope sync task:", err.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
docId,
|
|
86
|
+
doc_id: docId,
|
|
87
|
+
path: docPath,
|
|
88
|
+
blobHash,
|
|
89
|
+
blob_hash: blobHash,
|
|
90
|
+
title: docTitle,
|
|
91
|
+
sectionsCount: sectionsRow?.cnt || 0,
|
|
92
|
+
sections_count: sectionsRow?.cnt || 0,
|
|
93
|
+
microChunksCount: chunksRow?.cnt || 0,
|
|
94
|
+
micro_chunks_count: chunksRow?.cnt || 0,
|
|
95
|
+
deduplicated: true,
|
|
96
|
+
projectScope: assignedScope,
|
|
97
|
+
};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
await db.exec("ROLLBACK;");
|
|
100
|
+
throw new Error(`Ingestion scope transaction failed: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const hierarchy = buildTripleHierarchy(markdown, docId, docTitle);
|
|
58
105
|
|
|
59
106
|
if (generateEmbeddings && hierarchy.microChunks.length > 0) {
|
|
60
107
|
const BATCH_SIZE = getConfig().batchSize || 12;
|
|
@@ -84,31 +131,49 @@ export async function ingestDocument({
|
|
|
84
131
|
}
|
|
85
132
|
}
|
|
86
133
|
|
|
87
|
-
await db.exec("BEGIN IMMEDIATE;");
|
|
88
|
-
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
await db.exec("BEGIN IMMEDIATE;");
|
|
135
|
+
try {
|
|
136
|
+
if (existingDoc) {
|
|
137
|
+
const ownedRows = await db.prepare(`
|
|
138
|
+
SELECT id FROM sections WHERE doc_id = ?
|
|
139
|
+
UNION SELECT id FROM medium_chunks WHERE doc_id = ?
|
|
140
|
+
UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
|
|
141
|
+
`).all(docId, docId, docId);
|
|
142
|
+
const ownedIds = [docId, ...ownedRows.map((row) => row.id)];
|
|
143
|
+
try {
|
|
144
|
+
await db.prepare("DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);").run(docId);
|
|
145
|
+
} catch {}
|
|
146
|
+
if (ownedIds.length > 0) {
|
|
147
|
+
const placeholders = ownedIds.map(() => "?").join(",");
|
|
148
|
+
await db
|
|
149
|
+
.prepare(`DELETE FROM graph_edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders});`)
|
|
150
|
+
.run(...ownedIds, ...ownedIds);
|
|
151
|
+
}
|
|
152
|
+
await db.prepare("DELETE FROM micro_chunks WHERE doc_id = ?;").run(docId);
|
|
153
|
+
await db.prepare("DELETE FROM medium_chunks WHERE doc_id = ?;").run(docId);
|
|
154
|
+
await db.prepare("DELETE FROM sections WHERE doc_id = ?;").run(docId);
|
|
155
|
+
await db.prepare(`
|
|
156
|
+
UPDATE documents
|
|
157
|
+
SET blob_hash = ?, title = ?, checksum = ?, toc_json = ?, metadata_json = ?, updated_at = ?
|
|
158
|
+
WHERE id = ?;
|
|
159
|
+
`).run(blobHash, docTitle, blobHash, hierarchy.toc, JSON.stringify(metadata), now, docId);
|
|
160
|
+
} else {
|
|
161
|
+
await db.prepare(`
|
|
162
|
+
INSERT INTO documents (id, path, blob_hash, title, checksum, toc_json, metadata_json, created_at, updated_at)
|
|
163
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
|
164
|
+
`).run(
|
|
165
|
+
docId,
|
|
166
|
+
docPath,
|
|
167
|
+
blobHash,
|
|
168
|
+
docTitle,
|
|
169
|
+
blobHash,
|
|
170
|
+
hierarchy.toc,
|
|
171
|
+
JSON.stringify(metadata),
|
|
172
|
+
now,
|
|
173
|
+
now
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const assignedScope = await addDocumentScope(db, docId, projectScope);
|
|
112
177
|
|
|
113
178
|
const insertSectionStmt = db.prepare(`
|
|
114
179
|
INSERT INTO sections (id, doc_id, heading, breadcrumbs, content, token_count)
|
|
@@ -142,14 +207,46 @@ export async function ingestDocument({
|
|
|
142
207
|
await insertFtsStmt.run(micro.id, micro.content, micro.breadcrumbs);
|
|
143
208
|
}
|
|
144
209
|
|
|
145
|
-
const edges = buildGraphEdges(docId, hierarchy);
|
|
146
|
-
await saveGraphEdges(db, edges);
|
|
147
|
-
|
|
148
|
-
|
|
210
|
+
const edges = buildGraphEdges(docId, hierarchy);
|
|
211
|
+
await saveGraphEdges(db, edges);
|
|
212
|
+
|
|
213
|
+
// Recreate graph projections for preserved Notebook links after replacing
|
|
214
|
+
// the document's structural chunks. The knowledge_links rows themselves
|
|
215
|
+
// remain stable because the document id remains stable.
|
|
216
|
+
if (existingDoc) {
|
|
217
|
+
const links = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(docId);
|
|
218
|
+
const insertLinkEdge = db.prepare(`
|
|
219
|
+
INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at)
|
|
220
|
+
VALUES (?, ?, ?, ?, ?);
|
|
221
|
+
`);
|
|
222
|
+
for (const link of links) {
|
|
223
|
+
const targetSpec = link.start_line
|
|
224
|
+
? `${docId}:L${link.start_line}-${link.end_line || link.start_line}`
|
|
225
|
+
: docId;
|
|
226
|
+
await insertLinkEdge.run(
|
|
227
|
+
`fact:${link.fact_key}:${link.fact_text.substring(0, 30)}`,
|
|
228
|
+
targetSpec,
|
|
229
|
+
link.relation_type || "LINKS_TO",
|
|
230
|
+
JSON.stringify({ linkId: link.id }),
|
|
231
|
+
link.created_at || now
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await db.exec("COMMIT;");
|
|
149
237
|
} catch (err) {
|
|
150
238
|
await db.exec("ROLLBACK;");
|
|
151
239
|
throw new Error(`Ingestion transaction failed: ${err.message}`);
|
|
152
|
-
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (existingDoc?.blob_hash && existingDoc.blob_hash !== blobHash) {
|
|
243
|
+
const refs = await db.prepare("SELECT COUNT(*) AS cnt FROM documents WHERE blob_hash = ?").get(existingDoc.blob_hash);
|
|
244
|
+
if (!refs?.cnt) {
|
|
245
|
+
try {
|
|
246
|
+
await deleteBlob(existingDoc.blob_hash, customBlobDir);
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
153
250
|
|
|
154
251
|
if (getConfig().mode === "hybrid-sync") {
|
|
155
252
|
try {
|
|
@@ -172,14 +269,15 @@ export async function ingestDocument({
|
|
|
172
269
|
sectionsCount: hierarchy.sections.length,
|
|
173
270
|
sections_count: hierarchy.sections.length,
|
|
174
271
|
microChunksCount: hierarchy.microChunks.length,
|
|
175
|
-
micro_chunks_count: hierarchy.microChunks.length,
|
|
176
|
-
deduplicated: blobRes.deduplicated,
|
|
177
|
-
|
|
178
|
-
}
|
|
272
|
+
micro_chunks_count: hierarchy.microChunks.length,
|
|
273
|
+
deduplicated: blobRes.deduplicated,
|
|
274
|
+
projectScope: projectScope || GLOBAL_KEY,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
179
277
|
|
|
180
278
|
export async function deleteDocument(docIdOrPath, customDb = null, customBlobDir = BLOBS_DIR) {
|
|
181
279
|
const db = customDb || await getDatabase();
|
|
182
|
-
const doc = await db.prepare("SELECT * FROM documents WHERE id = ? OR path = ?").get(docIdOrPath, docIdOrPath);
|
|
280
|
+
const doc = await db.prepare("SELECT * FROM documents WHERE id = ? OR path = ? OR title = ?").get(docIdOrPath, docIdOrPath, docIdOrPath);
|
|
183
281
|
if (!doc) {
|
|
184
282
|
return { deleted: false, reason: "Document not found" };
|
|
185
283
|
}
|
package/mcp-server/memory.js
CHANGED
|
@@ -48,8 +48,18 @@ export function ensureDirSync() {
|
|
|
48
48
|
if (!existsSync(exportsDir)) mkdirSync(exportsDir, { recursive: true });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export function getActiveWorkspaceDir() {
|
|
52
|
+
return (
|
|
53
|
+
process.env.WORKSPACE_DIR ||
|
|
54
|
+
process.env.PROJECT_DIR ||
|
|
55
|
+
process.env.OPENCODE_WORKSPACE ||
|
|
56
|
+
process.env.IDE_WORKSPACE ||
|
|
57
|
+
null
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
export function canonicalPath(dir) {
|
|
52
|
-
let p = resolve(dir || process.cwd());
|
|
62
|
+
let p = resolve(dir || getActiveWorkspaceDir() || process.cwd());
|
|
53
63
|
if (process.platform === "win32") {
|
|
54
64
|
p = p.replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_, d) => `${d.toLowerCase()}:`);
|
|
55
65
|
}
|
|
@@ -57,13 +67,13 @@ export function canonicalPath(dir) {
|
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
export async function projectKey(worktree, directory) {
|
|
60
|
-
const dir = worktree || directory || process.cwd();
|
|
70
|
+
const dir = worktree || directory || getActiveWorkspaceDir() || process.cwd();
|
|
61
71
|
const identity = await resolveProjectIdentity(dir);
|
|
62
72
|
return identity ? identity.key : null;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
export async function projectName(worktree, directory) {
|
|
66
|
-
const dir = worktree || directory || process.cwd();
|
|
76
|
+
const dir = worktree || directory || getActiveWorkspaceDir() || process.cwd();
|
|
67
77
|
const identity = await resolveProjectIdentity(dir);
|
|
68
78
|
return identity ? identity.name : (dir ? basename(resolve(dir)) : "default");
|
|
69
79
|
}
|
|
@@ -10,10 +10,12 @@ const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
|
|
|
10
10
|
export const PROMPT_BLOCK = `${START_MARKER}
|
|
11
11
|
[SYSTEM INSTRUCTION: PERSONAL CONTEXT & MEMORY AGENT]
|
|
12
12
|
1. MANDATORY FIRST STEP (SESSION INITIALIZATION): At the very start of every session or conversation, your VERY FIRST ACTION MUST BE to execute \`recall\` from \`memory-agent\` with \`scope: "all"\` (or without restrictive query filters) to load ALL saved global facts, user preferences, personal context, and project guidelines BEFORE performing any other tasks or reading code. Do NOT filter or restrict the initial \`recall\` call with specific query keywords or scoped parameters, so that no global facts are missed.
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
5.
|
|
13
|
+
2. PROJECT IDENTITY INITIALIZATION: After the initial recall, call \`memory_info\` for the current workspace. If it reports a Git identity with \`Registry: unlinked\`, immediately call \`link_project_memory\` for the current directory. If linking migrated legacy facts, repeat \`recall(scope: "all")\`. Outside a Git repository, do not create project memory; use global memory only.
|
|
14
|
+
3. PROACTIVE SAVING DIRECTIVE: You MUST automatically and proactively call \`remember\` from \`memory-agent\` whenever the user shares durable facts, personal preferences, coding guidelines, technology choices, or project architecture decisions. Do NOT wait for explicit user commands like "remember this".
|
|
15
|
+
4. SIGNAL FILTER: Save only high-signal facts (name, language, roles, constraints, tech stack preferences, architecture decisions, conventions). Translate facts into clear, concise English when saving. Do NOT save transient details or one-off conversation turns.
|
|
16
|
+
5. QUERY OPTIMIZATION: When using \`query_knowledge_base\` from \`memory-agent\`, transform the user's natural language question into concept-dense search queries. Do NOT send raw conversational questions ("как мне сделать X?", "что такое Y?"). Instead, extract key terms and use concise factual phrases. For multi-part queries or comparisons, use \`batch_query_knowledge_base\` with multiple targeted queries — this is more efficient (single API call, single ONNX inference pass) and yields higher retrieval quality than separate calls.
|
|
17
|
+
6. SELECTIVE RAG CURATION: When web research or current technical documentation yields reliable project knowledge likely to be needed again, ingest the relevant source or excerpt with project scope and link it to the project-scoped Notebook fact it supports. Use global RAG scope only for sources intentionally reusable across projects. Prioritize authoritative documentation and knowledge newer than model training. Do not ingest everything encountered, transient output, or duplicate low-value content.
|
|
18
|
+
7. POLICY EXPANSION: The knowledge base automatically expands table summaries and code signatures for better recall (config \`policyExpansion\`, default: ON). If you need raw micro_chunk precision without expansion, pass \`policyExpansion: false\` per-call or set via config.${END_MARKER}`;
|
|
17
19
|
|
|
18
20
|
// Plugin-owned files live here so we never destroy user-owned config content.
|
|
19
21
|
const AGENT_CONFIG_DIR = join(homedir(), ".config", "memory-agent");
|
|
@@ -115,9 +117,10 @@ export function upsertPromptBlock(content, block = PROMPT_BLOCK) {
|
|
|
115
117
|
return clean ? `${clean}\n\n${block}\n` : `${block}\n`;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
export async function enableGlobalPrompt() {
|
|
119
|
-
const promptFile = await syncPromptFile();
|
|
120
|
-
const
|
|
120
|
+
export async function enableGlobalPrompt(targetNames = null) {
|
|
121
|
+
const promptFile = await syncPromptFile();
|
|
122
|
+
const requested = Array.isArray(targetNames) ? new Set(targetNames) : null;
|
|
123
|
+
const targets = getGlobalPromptTargets().filter((target) => !requested || requested.has(target.name));
|
|
121
124
|
const state = await loadState();
|
|
122
125
|
const results = [];
|
|
123
126
|
|
|
@@ -10,19 +10,29 @@ export function sanitizeFtsQuery(query) {
|
|
|
10
10
|
return words.join(" OR ");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function bm25Search(db, query, limit = 30) {
|
|
14
|
-
const ftsQuery = sanitizeFtsQuery(query);
|
|
15
|
-
if (!ftsQuery) return [];
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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);
|
|
26
36
|
return rows.map((r, i) => ({
|
|
27
37
|
id: r.id,
|
|
28
38
|
content: r.content,
|
|
@@ -50,7 +60,7 @@ export function toVectorBytes(value) {
|
|
|
50
60
|
return null;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
|
|
63
|
+
export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25, scopeKeys = null) {
|
|
54
64
|
if (!queryVector || queryVector.length === 0) return [];
|
|
55
65
|
|
|
56
66
|
const vectorDim = queryVector.length;
|
|
@@ -59,21 +69,32 @@ export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
|
|
|
59
69
|
const tempVec = new Float32Array(tempBuf);
|
|
60
70
|
|
|
61
71
|
const scanLimit = Number(getConfig().vectorScanLimit) || 0;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
98
|
const scored = [];
|
|
78
99
|
for (const r of rows) {
|
|
79
100
|
// node:sqlite returns BLOBs as plain Uint8Array (NOT Buffer), the Turso
|
|
@@ -244,7 +265,8 @@ export async function batchHybridQuery(queries, options = {}) {
|
|
|
244
265
|
rerankerEnabled = null,
|
|
245
266
|
instruction = null,
|
|
246
267
|
generateEmbeddings = true,
|
|
247
|
-
policyExpansion = null,
|
|
268
|
+
policyExpansion = null,
|
|
269
|
+
scopeKeys = null,
|
|
248
270
|
} = options;
|
|
249
271
|
|
|
250
272
|
const db = customDb || await getDatabase();
|
|
@@ -277,7 +299,8 @@ export async function batchHybridQuery(queries, options = {}) {
|
|
|
277
299
|
rerankerEnabled: useReranker,
|
|
278
300
|
instruction,
|
|
279
301
|
generateEmbeddings,
|
|
280
|
-
policyExpansion: usePolicyExpansion,
|
|
302
|
+
policyExpansion: usePolicyExpansion,
|
|
303
|
+
scopeKeys,
|
|
281
304
|
_precomputedVector: queryVectors[i] || null,
|
|
282
305
|
})
|
|
283
306
|
)
|
|
@@ -299,7 +322,8 @@ export async function hybridQuery({
|
|
|
299
322
|
rerankerEnabled = null,
|
|
300
323
|
instruction = null,
|
|
301
324
|
generateEmbeddings = true,
|
|
302
|
-
policyExpansion = null, // null = use config default
|
|
325
|
+
policyExpansion = null, // null = use config default
|
|
326
|
+
scopeKeys = null, // null = all documents; tool surfaces pass global/current-project keys
|
|
303
327
|
_precomputedVector = null, // internal: skip embedText if batch already computed
|
|
304
328
|
}) {
|
|
305
329
|
const db = customDb || await getDatabase();
|
|
@@ -327,28 +351,28 @@ export async function hybridQuery({
|
|
|
327
351
|
let fusedHits = [];
|
|
328
352
|
|
|
329
353
|
if (algo === "lexical_only" || algo === "bm25_only") {
|
|
330
|
-
const bm25Hits = await bm25Search(db, query, limit * 4);
|
|
354
|
+
const bm25Hits = await bm25Search(db, query, limit * 4, scopeKeys);
|
|
331
355
|
fusedHits = bm25Hits.map((hit) => ({
|
|
332
356
|
...hit,
|
|
333
357
|
score: 1.0 / hit.bm25_rank,
|
|
334
358
|
}));
|
|
335
359
|
} else if (algo === "semantic_only" || algo === "vector_only") {
|
|
336
360
|
const queryVector = await getQueryVector();
|
|
337
|
-
const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10);
|
|
361
|
+
const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10, scopeKeys);
|
|
338
362
|
fusedHits = vectorHits.map((hit) => ({
|
|
339
363
|
...hit,
|
|
340
364
|
score: hit.cosine_sim,
|
|
341
365
|
}));
|
|
342
366
|
} else if (algo === "rrf") {
|
|
343
|
-
const bm25Hits = await bm25Search(db, query, 30);
|
|
367
|
+
const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
|
|
344
368
|
const queryVector = await getQueryVector();
|
|
345
|
-
const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
|
|
369
|
+
const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
|
|
346
370
|
fusedHits = rrfFusion(bm25Hits, vectorHits, 60, scoreThreshold);
|
|
347
371
|
} else {
|
|
348
372
|
// Default: RSF
|
|
349
|
-
const bm25Hits = await bm25Search(db, query, 30);
|
|
373
|
+
const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
|
|
350
374
|
const queryVector = await getQueryVector();
|
|
351
|
-
const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
|
|
375
|
+
const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
|
|
352
376
|
fusedHits = rsfFusion(bm25Hits, vectorHits, alphaWeight, scoreThreshold);
|
|
353
377
|
}
|
|
354
378
|
|
package/mcp-server/setup.js
CHANGED
|
@@ -216,19 +216,23 @@ export async function runSetup() {
|
|
|
216
216
|
? " [OK] Codex: added direct Node.js memory-agent launcher to ~/.codex/config.toml"
|
|
217
217
|
: " [OK] Codex: migrated memory-agent to a direct Node.js launcher in ~/.codex/config.toml"
|
|
218
218
|
);
|
|
219
|
-
configuredCount++;
|
|
220
219
|
} else {
|
|
221
220
|
console.log(" [INFO] Codex: direct Node.js memory-agent launcher already configured");
|
|
222
221
|
}
|
|
222
|
+
configuredCount++;
|
|
223
223
|
} catch (err) {
|
|
224
224
|
console.log(" [FAIL] Codex setup failed:", err.message);
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// 5. Global Prompt Instructions (Antigravity, Codex, Claude Code)
|
|
229
|
-
try {
|
|
230
|
-
const { enableGlobalPrompt } = await import("./prompt_manager.js");
|
|
231
|
-
const
|
|
229
|
+
try {
|
|
230
|
+
const { enableGlobalPrompt } = await import("./prompt_manager.js");
|
|
231
|
+
const promptTargets = [];
|
|
232
|
+
if (doAntigravity) promptTargets.push("Antigravity");
|
|
233
|
+
if (doCodex) promptTargets.push("Codex");
|
|
234
|
+
if (doClaude) promptTargets.push("Claude Code");
|
|
235
|
+
const promptResults = await enableGlobalPrompt(promptTargets);
|
|
232
236
|
promptResults.forEach((r) => {
|
|
233
237
|
if (r.status === "enabled") {
|
|
234
238
|
console.log(` [OK] ${r.name}: enabled global prompt instruction in ${r.filePath}`);
|
|
@@ -250,14 +254,16 @@ export async function runSetup() {
|
|
|
250
254
|
const packageSkillsDir = join(packageDir, "skills");
|
|
251
255
|
if (existsSync(packageSkillsDir)) {
|
|
252
256
|
const opencodeDir = process.env.OPENCODE_CONFIG_DIR || join(home, ".config", "opencode");
|
|
253
|
-
const targets = [
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
{ name: "
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
257
|
+
const targets = [];
|
|
258
|
+
if (doOpenCode) targets.push({ name: "OpenCode", dir: join(opencodeDir, "skills") });
|
|
259
|
+
if (doAntigravity) targets.push({ name: "Antigravity", dir: join(home, ".gemini", "config", "skills") });
|
|
260
|
+
if (doCodex) {
|
|
261
|
+
targets.push({ name: "Codex", dir: join(home, ".codex", "skills") });
|
|
262
|
+
targets.push({ name: "Codex shared agents", dir: join(home, ".agents", "skills") });
|
|
263
|
+
}
|
|
264
|
+
if (doClaude) targets.push({ name: "Claude Code", dir: join(home, ".claude", "skills") });
|
|
265
|
+
const cwd = process.cwd();
|
|
266
|
+
if (doAntigravity && existsSync(join(cwd, ".agents"))) {
|
|
261
267
|
targets.push({ name: "Antigravity (local)", dir: join(cwd, ".agents", "skills") });
|
|
262
268
|
}
|
|
263
269
|
|