@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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { factMeta, factTitle, isDirectiveFact, withMeta } from "./fact_format.js";
|
|
2
|
+
import { GLOBAL_KEY, readMemory, writeMemory } from "./memory.js";
|
|
3
|
+
import { syncPersonaPrompts } from "./prompt_manager.js";
|
|
4
|
+
|
|
5
|
+
// Convert compatibility-only persona detection into explicit semantic metadata.
|
|
6
|
+
// Explicit kind values are authoritative and are therefore never changed.
|
|
7
|
+
export function markLegacyPersonaDirectives(entries) {
|
|
8
|
+
const migrated = [];
|
|
9
|
+
const nextEntries = entries.map((entry) => {
|
|
10
|
+
const meta = factMeta(entry);
|
|
11
|
+
if (meta.kind || !isDirectiveFact(entry)) return entry;
|
|
12
|
+
|
|
13
|
+
migrated.push({
|
|
14
|
+
id: meta.id || null,
|
|
15
|
+
title: factTitle(entry) || "Untitled directive",
|
|
16
|
+
});
|
|
17
|
+
return withMeta(entry, { kind: "directive" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
entries: nextEntries,
|
|
22
|
+
migrated,
|
|
23
|
+
changed: migrated.length,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function migrateLegacyPersonaDirectives({
|
|
28
|
+
dryRun = false,
|
|
29
|
+
readGlobal = () => readMemory(GLOBAL_KEY),
|
|
30
|
+
writeGlobal = (entries) => writeMemory(GLOBAL_KEY, entries),
|
|
31
|
+
syncPersona = () => syncPersonaPrompts(),
|
|
32
|
+
} = {}) {
|
|
33
|
+
const result = markLegacyPersonaDirectives(await readGlobal());
|
|
34
|
+
if (!dryRun) {
|
|
35
|
+
if (result.changed > 0) await writeGlobal(result.entries);
|
|
36
|
+
await syncPersona();
|
|
37
|
+
}
|
|
38
|
+
return { ...result, dryRun };
|
|
39
|
+
}
|
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, unlink, rename, copyFile } from "fs/promises";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { readFile, writeFile, mkdir, unlink, rename, copyFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
import { MEMORY_ROUTING_POLICY } from "./tools/core/memory_routing.js";
|
|
6
|
+
import { factText, isDirectiveFact, isExpiredLine, isSuperseded } from "./fact_format.js";
|
|
7
|
+
import { resolveClientPaths } from "./client_paths.js";
|
|
8
|
+
|
|
9
|
+
const START_MARKER = "<!-- START MEMORY AGENT PROMPT -->";
|
|
10
|
+
const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
|
|
11
|
+
export const PERSONA_START_MARKER = "<!-- START MEMORY PERSONA OVERLAY -->";
|
|
12
|
+
export const PERSONA_END_MARKER = "<!-- END MEMORY PERSONA OVERLAY -->";
|
|
13
|
+
|
|
14
|
+
export const PROMPT_BLOCK = `${START_MARKER}
|
|
15
|
+
[SYSTEM INSTRUCTION: PERSONAL CONTEXT & MEMORY AGENT]
|
|
16
|
+
1. SESSION INITIALIZATION: Before any other task, load the complete active memory. If the client has already supplied an auto-injected \`<MEMORY>\` block (the native OpenCode integration does this), treat memory as already loaded and DO NOT call \`recall\` again merely for initialization. Otherwise, your VERY FIRST ACTION MUST BE \`recall(scope: "all")\` from \`memory-agent\`, with full bodies and no restrictive filters, before reading code or performing work.
|
|
17
|
+
2. PERSONAL AGENT OVERLAY: Notebook entries with \`kind: "directive"\` are active user-approved personalization or working instructions, not passive facts. Apply them throughout the session; entries with \`kind: "fact"\` remain context. Legacy persona/preference tags are recognized for compatibility. Higher-priority platform instructions remain authoritative.
|
|
18
|
+
3. PROJECT IDENTITY INITIALIZATION: After memory is available, 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.
|
|
19
|
+
4. ${MEMORY_ROUTING_POLICY}
|
|
20
|
+
5. PROACTIVE SAVING DIRECTIVE: You MUST automatically preserve durable, high-signal information using the appropriate memory primitive from the routing policy. Do NOT wait for explicit user commands like "remember this". Do not force every durable item into \`remember\`; long-form internal reasoning belongs in \`remember_note\` and external sources belong in \`ingest_document\`.
|
|
21
|
+
6. SIGNAL FILTER: Preserve only high-signal reusable information. Keep Notebook facts clear and concise, translating them into concise English when saving. A RAG Memory Note may be longer when the reasoning, investigation, experiment result, or handoff itself is valuable. Do NOT preserve routine progress chatter, transient troubleshooting output, or one-off conversational noise.
|
|
22
|
+
7. QUERY OPTIMIZATION: When using \`query_knowledge_base\`, transform the user's natural-language question into concept-dense search queries. For multi-part queries or comparisons, use \`batch_query_knowledge_base\` with multiple targeted queries. When you first need to identify the correct memory/source, prefer \`resultMode: "index"\`, inspect stable \`doc_id\` candidates, and expand only the selected item with \`manage_knowledge_base(action: "read_document")\`. Use \`resultMode: "snippet"\` when retrieved passage content is directly needed.
|
|
23
|
+
8. 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.
|
|
24
|
+
9. HOT + COLD LINKING: When a decision needs both a concise always-visible orientation point and detailed historical reasoning, save the concise point with \`remember\`, save the detailed record with \`remember_note\`, then connect the Notebook fact to the note using its returned \`docId\` via \`link_knowledge\` (or the optional document-link fields on \`remember\`). Never duplicate the full note body into Notebook memory.
|
|
25
|
+
10. POLICY EXPANSION: The knowledge base automatically expands table summaries and code signatures for content-rich retrieval (config \`policyExpansion\`, default: ON). Semantic TOC/index retrieval disables large policy expansion automatically. If you need raw micro_chunk precision in normal snippet retrieval, pass \`policyExpansion: false\` per-call or set it via config.${END_MARKER}`;
|
|
19
26
|
|
|
20
27
|
// Plugin-owned files live here so we never destroy user-owned config content.
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
28
|
+
const CLIENT_PATHS = resolveClientPaths();
|
|
29
|
+
const AGENT_CONFIG_DIR = CLIENT_PATHS.agentConfigDir;
|
|
30
|
+
export const PROMPT_FILE = CLIENT_PATHS.promptFile;
|
|
31
|
+
const BACKUP_DIR = CLIENT_PATHS.promptBackupDir;
|
|
32
|
+
const STATE_FILE = CLIENT_PATHS.promptStateFile;
|
|
25
33
|
|
|
26
34
|
function sha256(content) {
|
|
27
35
|
return createHash("sha256").update(content).digest("hex");
|
|
@@ -55,15 +63,21 @@ async function saveState(state) {
|
|
|
55
63
|
await atomicWrite(STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
export function getGlobalPromptTargets() {
|
|
59
|
-
const home =
|
|
60
|
-
return [
|
|
61
|
-
{
|
|
62
|
-
name: "Antigravity",
|
|
63
|
-
filePath: join(home, ".gemini", "config", "AGENTS.md"),
|
|
64
|
-
// Antigravity resolves `@` imports only in GEMINI.md, not reliably in AGENTS.md
|
|
65
|
-
includeSupported: false,
|
|
66
|
-
},
|
|
66
|
+
export function getGlobalPromptTargets() {
|
|
67
|
+
const { home } = CLIENT_PATHS;
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
name: "Antigravity",
|
|
71
|
+
filePath: join(home, ".gemini", "config", "AGENTS.md"),
|
|
72
|
+
// Antigravity resolves `@` imports only in GEMINI.md, not reliably in AGENTS.md
|
|
73
|
+
includeSupported: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "Gemini CLI",
|
|
77
|
+
filePath: join(home, ".gemini", "GEMINI.md"),
|
|
78
|
+
// Gemini CLI supports @file imports in its global GEMINI.md context file.
|
|
79
|
+
includeSupported: true,
|
|
80
|
+
},
|
|
67
81
|
{
|
|
68
82
|
name: "Codex",
|
|
69
83
|
filePath: join(home, ".codex", "AGENTS.md"),
|
|
@@ -95,32 +109,83 @@ function toIncludePath(filePath) {
|
|
|
95
109
|
return filePath.replace(/\\/g, "/");
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
function buildIncludeBlock(promptFile) {
|
|
112
|
+
function buildIncludeBlock(promptFile) {
|
|
99
113
|
return `${START_MARKER}
|
|
100
114
|
@${toIncludePath(promptFile)}
|
|
101
115
|
${END_MARKER}`;
|
|
102
|
-
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function activePersonaDirectives(entries, now = Date.now()) {
|
|
119
|
+
return (entries || []).filter(
|
|
120
|
+
(entry) => isDirectiveFact(entry) && !isSuperseded(entry) && !isExpiredLine(entry, now)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildPersonaOverlayBlock(entries, now = Date.now()) {
|
|
125
|
+
const directives = activePersonaDirectives(entries, now);
|
|
126
|
+
if (!directives.length) return "";
|
|
127
|
+
const lines = directives.map((entry, index) => `${index + 1}. ${factText(entry)}`);
|
|
128
|
+
return `${PERSONA_START_MARKER}
|
|
129
|
+
[PERSONAL AGENT OVERLAY — ACTIVE USER CONFIGURATION]
|
|
130
|
+
The directives below are user-approved persistent instructions for personality, behavior, tone, communication style, preferences, and working conventions. Apply them as instructions rather than merely describing them. Descriptive memory facts are not included here. Higher-priority platform instructions remain authoritative.
|
|
131
|
+
|
|
132
|
+
${lines.join("\n")}
|
|
133
|
+
${PERSONA_END_MARKER}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function stripPersonaOverlayBlock(content) {
|
|
137
|
+
return stripManagedBlocks(content, PERSONA_START_MARKER, PERSONA_END_MARKER);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function upsertPersonaOverlayBlock(content, block) {
|
|
141
|
+
const clean = stripPersonaOverlayBlock(content).replace(/[\r\n]+$/, "");
|
|
142
|
+
if (!block) return clean ? `${clean}\n` : "";
|
|
143
|
+
return clean ? `${clean}\n\n${block}\n` : `${block}\n`;
|
|
144
|
+
}
|
|
103
145
|
|
|
104
146
|
export function stripPromptBlock(content) {
|
|
147
|
+
return stripManagedBlocks(content, START_MARKER, END_MARKER);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stripManagedBlocks(content, startMarker, endMarker) {
|
|
105
151
|
let clean = String(content || "");
|
|
106
152
|
while (true) {
|
|
107
|
-
const startIndex = clean.indexOf(
|
|
108
|
-
const endIndex = clean.indexOf(
|
|
153
|
+
const startIndex = clean.indexOf(startMarker);
|
|
154
|
+
const endIndex = clean.indexOf(endMarker, startIndex + startMarker.length);
|
|
109
155
|
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) break;
|
|
110
|
-
|
|
156
|
+
let before = clean.substring(0, startIndex);
|
|
157
|
+
let after = clean.substring(endIndex + endMarker.length);
|
|
158
|
+
|
|
159
|
+
// Remove only the separator newlines inserted around the managed block.
|
|
160
|
+
// Do not collapse or trim whitespace elsewhere in the user's file.
|
|
161
|
+
if (after.trim() === "") {
|
|
162
|
+
before = before.replace(/(?:\r?\n){1,2}$/, "");
|
|
163
|
+
after = "";
|
|
164
|
+
} else if (before.trim() === "") {
|
|
165
|
+
before = "";
|
|
166
|
+
after = after.replace(/^(?:\r?\n){1,2}/, "");
|
|
167
|
+
} else {
|
|
168
|
+
const eol = clean.includes("\r\n") ? "\r\n" : "\n";
|
|
169
|
+
before = before.replace(/(?:\r?\n){1,2}$/, "");
|
|
170
|
+
after = after.replace(/^(?:\r?\n){1,2}/, "");
|
|
171
|
+
before += `${eol}${eol}`;
|
|
172
|
+
}
|
|
173
|
+
clean = before + after;
|
|
111
174
|
}
|
|
112
|
-
return clean
|
|
175
|
+
return clean;
|
|
113
176
|
}
|
|
114
|
-
|
|
177
|
+
|
|
115
178
|
export function upsertPromptBlock(content, block = PROMPT_BLOCK) {
|
|
116
|
-
const clean = stripPromptBlock(content);
|
|
117
|
-
return clean ? `${clean}\n\n${block}\n` : `${block}\n`;
|
|
118
|
-
}
|
|
179
|
+
const clean = stripPromptBlock(content).replace(/[\r\n]+$/, "");
|
|
180
|
+
return clean ? `${clean}\n\n${block}\n` : `${block}\n`;
|
|
181
|
+
}
|
|
119
182
|
|
|
120
183
|
export async function enableGlobalPrompt(targetNames = null) {
|
|
121
184
|
const promptFile = await syncPromptFile();
|
|
122
|
-
const
|
|
123
|
-
const
|
|
185
|
+
const { readMemory, GLOBAL_KEY } = await import("./memory.js");
|
|
186
|
+
const personaBlock = buildPersonaOverlayBlock(await readMemory(GLOBAL_KEY));
|
|
187
|
+
const requested = Array.isArray(targetNames) ? new Set(targetNames) : null;
|
|
188
|
+
const targets = getGlobalPromptTargets().filter((target) => !requested || requested.has(target.name));
|
|
124
189
|
const state = await loadState();
|
|
125
190
|
const results = [];
|
|
126
191
|
|
|
@@ -133,10 +198,10 @@ export async function enableGlobalPrompt(targetNames = null) {
|
|
|
133
198
|
|
|
134
199
|
const existed = existsSync(target.filePath);
|
|
135
200
|
const existing = existed ? await readFile(target.filePath, "utf-8") : "";
|
|
136
|
-
const block = target.includeSupported
|
|
137
|
-
? buildIncludeBlock(promptFile)
|
|
138
|
-
: PROMPT_BLOCK;
|
|
139
|
-
const updated = upsertPromptBlock(existing, block);
|
|
201
|
+
const block = target.includeSupported
|
|
202
|
+
? buildIncludeBlock(promptFile)
|
|
203
|
+
: PROMPT_BLOCK;
|
|
204
|
+
const updated = upsertPersonaOverlayBlock(upsertPromptBlock(existing, block), personaBlock);
|
|
140
205
|
|
|
141
206
|
const key = target.filePath;
|
|
142
207
|
const prev = state[key];
|
|
@@ -168,8 +233,9 @@ export async function enableGlobalPrompt(targetNames = null) {
|
|
|
168
233
|
return results;
|
|
169
234
|
}
|
|
170
235
|
|
|
171
|
-
export async function disableGlobalPrompt() {
|
|
172
|
-
const
|
|
236
|
+
export async function disableGlobalPrompt(targetNames = null) {
|
|
237
|
+
const requested = Array.isArray(targetNames) ? new Set(targetNames) : null;
|
|
238
|
+
const targets = getGlobalPromptTargets().filter((target) => !requested || requested.has(target.name));
|
|
173
239
|
const state = await loadState();
|
|
174
240
|
const results = [];
|
|
175
241
|
|
|
@@ -181,12 +247,12 @@ export async function disableGlobalPrompt() {
|
|
|
181
247
|
}
|
|
182
248
|
|
|
183
249
|
const existing = await readFile(target.filePath, "utf-8");
|
|
184
|
-
if (!existing.includes(START_MARKER)) {
|
|
250
|
+
if (!existing.includes(START_MARKER) && !existing.includes(PERSONA_START_MARKER)) {
|
|
185
251
|
results.push({ name: target.name, filePath: target.filePath, status: "skipped" });
|
|
186
252
|
continue;
|
|
187
253
|
}
|
|
188
254
|
|
|
189
|
-
const clean = stripPromptBlock(existing);
|
|
255
|
+
const clean = stripPersonaOverlayBlock(stripPromptBlock(existing));
|
|
190
256
|
const key = target.filePath;
|
|
191
257
|
const prev = state[key];
|
|
192
258
|
|
|
@@ -212,17 +278,58 @@ export async function disableGlobalPrompt() {
|
|
|
212
278
|
|
|
213
279
|
await saveState(state);
|
|
214
280
|
return results;
|
|
215
|
-
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function syncPersonaPrompts(targetNames = null, entries = null) {
|
|
284
|
+
if (!entries) {
|
|
285
|
+
const { readMemory, GLOBAL_KEY } = await import("./memory.js");
|
|
286
|
+
entries = await readMemory(GLOBAL_KEY);
|
|
287
|
+
}
|
|
288
|
+
const block = buildPersonaOverlayBlock(entries);
|
|
289
|
+
const requested = Array.isArray(targetNames) ? new Set(targetNames) : null;
|
|
290
|
+
const targets = getGlobalPromptTargets().filter((target) => !requested || requested.has(target.name));
|
|
291
|
+
const state = await loadState();
|
|
292
|
+
const results = [];
|
|
293
|
+
|
|
294
|
+
for (const target of targets) {
|
|
295
|
+
try {
|
|
296
|
+
const existed = existsSync(target.filePath);
|
|
297
|
+
if (!existed && !block) {
|
|
298
|
+
results.push({ name: target.name, filePath: target.filePath, status: "skipped" });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
await mkdir(join(target.filePath, ".."), { recursive: true });
|
|
302
|
+
const existing = existed ? await readFile(target.filePath, "utf-8") : "";
|
|
303
|
+
const updated = upsertPersonaOverlayBlock(existing, block);
|
|
304
|
+
if (existing === updated) {
|
|
305
|
+
results.push({ name: target.name, filePath: target.filePath, status: "up_to_date" });
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const prev = state[target.filePath];
|
|
309
|
+
if (existed && prev?.hash && prev.hash !== sha256(existing)) await backupFile(target.filePath);
|
|
310
|
+
await atomicWrite(target.filePath, updated);
|
|
311
|
+
state[target.filePath] = { hash: sha256(updated), existedBefore: existed };
|
|
312
|
+
results.push({ name: target.name, filePath: target.filePath, status: block ? "synced" : "removed" });
|
|
313
|
+
} catch (err) {
|
|
314
|
+
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await saveState(state);
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
216
321
|
|
|
217
|
-
export async function getGlobalPromptStatus() {
|
|
218
|
-
const
|
|
322
|
+
export async function getGlobalPromptStatus(targetNames = null) {
|
|
323
|
+
const requested = Array.isArray(targetNames) ? new Set(targetNames) : null;
|
|
324
|
+
const targets = getGlobalPromptTargets().filter((target) => !requested || requested.has(target.name));
|
|
219
325
|
const status = [];
|
|
220
326
|
|
|
221
327
|
for (const target of targets) {
|
|
222
328
|
let enabled = false;
|
|
223
329
|
if (existsSync(target.filePath)) {
|
|
224
330
|
const content = await readFile(target.filePath, "utf-8");
|
|
225
|
-
enabled = content.includes(START_MARKER) && content.includes(END_MARKER)
|
|
331
|
+
enabled = (content.includes(START_MARKER) && content.includes(END_MARKER))
|
|
332
|
+
|| (content.includes(PERSONA_START_MARKER) && content.includes(PERSONA_END_MARKER));
|
|
226
333
|
}
|
|
227
334
|
status.push({ name: target.name, filePath: target.filePath, enabled });
|
|
228
335
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { GLOBAL_KEY, projectKey } from "./memory.js";
|
|
2
|
+
|
|
3
|
+
export async function resolveRagScopeKey(scope = "project", ctx = {}) {
|
|
4
|
+
if (scope === "global") return GLOBAL_KEY;
|
|
5
|
+
const dir = ctx.directory || ctx.project || null;
|
|
6
|
+
const key = await projectKey(ctx.worktree ?? null, dir);
|
|
7
|
+
if (!key) {
|
|
8
|
+
if (scope === "project") {
|
|
9
|
+
throw new Error("Project-scoped RAG requires a Git repository. Use scope='global' outside Git.");
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return key;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveRagScopeKeys(scope = "all", ctx = {}) {
|
|
17
|
+
if (scope === "global") return [GLOBAL_KEY];
|
|
18
|
+
const dir = ctx.directory || ctx.project || null;
|
|
19
|
+
const project = await projectKey(ctx.worktree ?? null, dir);
|
|
20
|
+
if (scope === "project") {
|
|
21
|
+
if (!project) {
|
|
22
|
+
throw new Error("Project-scoped RAG requires a Git repository. Use scope='global' outside Git.");
|
|
23
|
+
}
|
|
24
|
+
return [project];
|
|
25
|
+
}
|
|
26
|
+
return project ? [GLOBAL_KEY, project] : [GLOBAL_KEY];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function resolveManageRagScopeKeys(action, scope, ctx = {}) {
|
|
30
|
+
if (action !== "delete" || scope) {
|
|
31
|
+
return await resolveRagScopeKeys(scope || "all", ctx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Browsing defaults to the combined visible view, but a destructive action
|
|
35
|
+
// defaults to the narrowest current ownership boundary. Inside Git that is
|
|
36
|
+
// the project; outside Git the only visible boundary is global.
|
|
37
|
+
const visible = await resolveRagScopeKeys("all", ctx);
|
|
38
|
+
return visible.length > 1 ? [visible[visible.length - 1]] : visible;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function addDocumentScope(db, docId, scopeKey) {
|
|
42
|
+
const key = scopeKey || GLOBAL_KEY;
|
|
43
|
+
await db
|
|
44
|
+
.prepare(
|
|
45
|
+
"INSERT OR IGNORE INTO document_scopes (doc_id, scope_key, created_at) VALUES (?, ?, ?);"
|
|
46
|
+
)
|
|
47
|
+
.run(docId, key, Date.now());
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function removeDocumentScopes(db, docId, scopeKeys) {
|
|
52
|
+
const keys = Array.isArray(scopeKeys) ? [...new Set(scopeKeys.filter(Boolean))] : [];
|
|
53
|
+
if (keys.length === 0) return { removedScopes: [], remainingScopes: 0 };
|
|
54
|
+
const placeholders = keys.map(() => "?").join(",");
|
|
55
|
+
const existing = await db
|
|
56
|
+
.prepare(`SELECT scope_key FROM document_scopes WHERE doc_id = ? AND scope_key IN (${placeholders})`)
|
|
57
|
+
.all(docId, ...keys);
|
|
58
|
+
await db
|
|
59
|
+
.prepare(`DELETE FROM document_scopes WHERE doc_id = ? AND scope_key IN (${placeholders})`)
|
|
60
|
+
.run(docId, ...keys);
|
|
61
|
+
const remaining = await db
|
|
62
|
+
.prepare("SELECT COUNT(*) AS cnt FROM document_scopes WHERE doc_id = ?")
|
|
63
|
+
.get(docId);
|
|
64
|
+
const remainingScopes = remaining?.cnt || 0;
|
|
65
|
+
if (existing.length > 0 && remainingScopes > 0) {
|
|
66
|
+
const { queueDocumentSyncIfNeeded } = await import("./graph/knowledge_linker.js");
|
|
67
|
+
await queueDocumentSyncIfNeeded(db, docId);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
removedScopes: existing.map((row) => row.scope_key),
|
|
71
|
+
remainingScopes,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function scopeFilterSql(scopeKeys, column = "ds.scope_key") {
|
|
76
|
+
if (!Array.isArray(scopeKeys) || scopeKeys.length === 0) {
|
|
77
|
+
return { clause: "", params: [] };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
clause: `${column} IN (${scopeKeys.map(() => "?").join(",")})`,
|
|
81
|
+
params: scopeKeys,
|
|
82
|
+
};
|
|
83
|
+
}
|