@lotargo/memory_plugin 1.6.1 → 1.6.3
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 +42 -1
- package/README.md +59 -20
- package/mcp-server/cli/direct_commands.js +10 -1
- package/mcp-server/cli.js +2 -1
- package/mcp-server/codex_config.js +171 -0
- package/mcp-server/codex_diagnostics.js +263 -0
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/migrations.js +15 -0
- package/mcp-server/index.js +4 -2
- package/mcp-server/ingest/chunker.js +179 -12
- package/mcp-server/ingest/pipeline.js +3 -3
- package/mcp-server/prompt_manager.js +221 -218
- package/mcp-server/retrieval/retriever.js +142 -12
- package/mcp-server/setup.js +37 -21
- package/mcp-server/tools/rag_tools.js +344 -283
- package/package.json +8 -4
- package/skills/using-memory/SKILL.md +11 -1
|
@@ -1,225 +1,228 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, unlink, rename, copyFile } from "fs/promises";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
import { createHash } from "crypto";
|
|
6
|
-
|
|
7
|
-
const START_MARKER = "<!-- START MEMORY AGENT PROMPT -->";
|
|
8
|
-
const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
|
|
9
|
-
|
|
10
|
-
export const PROMPT_BLOCK = `${START_MARKER}
|
|
11
|
-
[SYSTEM INSTRUCTION: PERSONAL CONTEXT & MEMORY AGENT]
|
|
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. 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".
|
|
14
|
-
3. 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.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
import { readFile, writeFile, mkdir, unlink, rename, copyFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
|
|
7
|
+
const START_MARKER = "<!-- START MEMORY AGENT PROMPT -->";
|
|
8
|
+
const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
|
|
9
|
+
|
|
10
|
+
export const PROMPT_BLOCK = `${START_MARKER}
|
|
11
|
+
[SYSTEM INSTRUCTION: PERSONAL CONTEXT & MEMORY AGENT]
|
|
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. 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".
|
|
14
|
+
3. 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.
|
|
15
|
+
4. 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.
|
|
16
|
+
5. 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
|
+
|
|
18
|
+
// Plugin-owned files live here so we never destroy user-owned config content.
|
|
19
|
+
const AGENT_CONFIG_DIR = join(homedir(), ".config", "memory-agent");
|
|
20
|
+
export const PROMPT_FILE = join(AGENT_CONFIG_DIR, "prompt.md");
|
|
21
|
+
const BACKUP_DIR = join(AGENT_CONFIG_DIR, "backups");
|
|
22
|
+
const STATE_FILE = join(AGENT_CONFIG_DIR, "prompt-state.json");
|
|
23
|
+
|
|
24
|
+
function sha256(content) {
|
|
25
|
+
return createHash("sha256").update(content).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function atomicWrite(filePath, content) {
|
|
29
|
+
const tmp = `${filePath}.memory-tmp-${process.pid}`;
|
|
30
|
+
await writeFile(tmp, content, "utf-8");
|
|
31
|
+
await rename(tmp, filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function backupFile(filePath) {
|
|
35
|
+
await mkdir(BACKUP_DIR, { recursive: true });
|
|
36
|
+
const name = filePath.split(/[\\/]/).pop() || "config";
|
|
37
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
38
|
+
const dest = join(BACKUP_DIR, `${name}.${stamp}.bak`);
|
|
39
|
+
await copyFile(filePath, dest);
|
|
40
|
+
return dest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadState() {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(await readFile(STATE_FILE, "utf-8"));
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function saveState(state) {
|
|
52
|
+
await mkdir(AGENT_CONFIG_DIR, { recursive: true });
|
|
53
|
+
await atomicWrite(STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGlobalPromptTargets() {
|
|
57
|
+
const home = homedir();
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
name: "Antigravity",
|
|
61
|
+
filePath: join(home, ".gemini", "config", "AGENTS.md"),
|
|
62
|
+
// Antigravity resolves `@` imports only in GEMINI.md, not reliably in AGENTS.md
|
|
63
|
+
includeSupported: false,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Codex",
|
|
67
|
+
filePath: join(home, ".codex", "AGENTS.md"),
|
|
68
|
+
// AGENTS.md standard has no import syntax
|
|
69
|
+
includeSupported: false,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "Claude Code",
|
|
73
|
+
filePath: join(home, ".claude", "CLAUDE.md"),
|
|
74
|
+
// @path imports are supported and trusted in user-scope CLAUDE.md
|
|
75
|
+
includeSupported: true,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Ensure the plugin-owned prompt file exists and is up to date with PROMPT_BLOCK.
|
|
81
|
+
// This file is referenced via `@` includes; updating it never touches user configs.
|
|
82
|
+
export async function syncPromptFile() {
|
|
83
|
+
await mkdir(AGENT_CONFIG_DIR, { recursive: true });
|
|
84
|
+
const expected = `${PROMPT_BLOCK}\n`;
|
|
85
|
+
const current = existsSync(PROMPT_FILE) ? await readFile(PROMPT_FILE, "utf-8") : "";
|
|
86
|
+
if (current !== expected) {
|
|
87
|
+
await atomicWrite(PROMPT_FILE, expected);
|
|
88
|
+
}
|
|
89
|
+
return PROMPT_FILE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toIncludePath(filePath) {
|
|
93
|
+
return filePath.replace(/\\/g, "/");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildIncludeBlock(promptFile) {
|
|
97
|
+
return `${START_MARKER}
|
|
98
|
+
@${toIncludePath(promptFile)}
|
|
99
|
+
${END_MARKER}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function stripPromptBlock(content) {
|
|
103
|
+
let clean = String(content || "");
|
|
104
|
+
while (true) {
|
|
105
|
+
const startIndex = clean.indexOf(START_MARKER);
|
|
106
|
+
const endIndex = clean.indexOf(END_MARKER, startIndex + START_MARKER.length);
|
|
107
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) break;
|
|
108
|
+
clean = clean.substring(0, startIndex) + clean.substring(endIndex + END_MARKER.length);
|
|
87
109
|
}
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function toIncludePath(filePath) {
|
|
92
|
-
return filePath.replace(/\\/g, "/");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildIncludeBlock(promptFile) {
|
|
96
|
-
return `${START_MARKER}
|
|
97
|
-
@${toIncludePath(promptFile)}
|
|
98
|
-
${END_MARKER}`;
|
|
110
|
+
return clean.replace(/\r?\n(?:[ \t]*\r?\n){2,}/g, "\n\n").trim();
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
function
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
106
|
-
const before = content.substring(0, startIndex);
|
|
107
|
-
const after = content.substring(endIndex + END_MARKER.length);
|
|
108
|
-
return (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
109
|
-
}
|
|
110
|
-
return content.trim();
|
|
113
|
+
export function upsertPromptBlock(content, block = PROMPT_BLOCK) {
|
|
114
|
+
const clean = stripPromptBlock(content);
|
|
115
|
+
return clean ? `${clean}\n\n${block}\n` : `${block}\n`;
|
|
111
116
|
}
|
|
112
|
-
|
|
113
|
-
export async function enableGlobalPrompt() {
|
|
114
|
-
const promptFile = await syncPromptFile();
|
|
115
|
-
const targets = getGlobalPromptTargets();
|
|
116
|
-
const state = await loadState();
|
|
117
|
-
const results = [];
|
|
118
|
-
|
|
119
|
-
for (const target of targets) {
|
|
120
|
-
try {
|
|
121
|
-
const parentDir = join(target.filePath, "..");
|
|
122
|
-
if (!existsSync(parentDir)) {
|
|
123
|
-
await mkdir(parentDir, { recursive: true });
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const existed = existsSync(target.filePath);
|
|
127
|
-
const existing = existed ? await readFile(target.filePath, "utf-8") : "";
|
|
128
|
-
const clean = stripPromptBlock(existing);
|
|
129
|
-
|
|
117
|
+
|
|
118
|
+
export async function enableGlobalPrompt() {
|
|
119
|
+
const promptFile = await syncPromptFile();
|
|
120
|
+
const targets = getGlobalPromptTargets();
|
|
121
|
+
const state = await loadState();
|
|
122
|
+
const results = [];
|
|
123
|
+
|
|
124
|
+
for (const target of targets) {
|
|
125
|
+
try {
|
|
126
|
+
const parentDir = join(target.filePath, "..");
|
|
127
|
+
if (!existsSync(parentDir)) {
|
|
128
|
+
await mkdir(parentDir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const existed = existsSync(target.filePath);
|
|
132
|
+
const existing = existed ? await readFile(target.filePath, "utf-8") : "";
|
|
130
133
|
const block = target.includeSupported
|
|
131
134
|
? buildIncludeBlock(promptFile)
|
|
132
135
|
: PROMPT_BLOCK;
|
|
133
|
-
const updated =
|
|
134
|
-
|
|
135
|
-
const key = target.filePath;
|
|
136
|
-
const prev = state[key];
|
|
137
|
-
|
|
138
|
-
if (existed && existing === updated) {
|
|
139
|
-
results.push({ name: target.name, filePath: target.filePath, status: "up_to_date" });
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Hash guard: if the user modified the file since we last wrote it,
|
|
144
|
-
// back it up before overwriting their edits.
|
|
145
|
-
if (existed && prev && prev.hash && prev.hash !== sha256(existing)) {
|
|
146
|
-
await backupFile(target.filePath);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
await atomicWrite(target.filePath, updated);
|
|
150
|
-
state[key] = { hash: sha256(updated), existedBefore: existed };
|
|
151
|
-
results.push({
|
|
152
|
-
name: target.name,
|
|
153
|
-
filePath: target.filePath,
|
|
154
|
-
status: existed ? "enabled" : "created_new_file",
|
|
155
|
-
});
|
|
156
|
-
} catch (err) {
|
|
157
|
-
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
await saveState(state);
|
|
162
|
-
return results;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function disableGlobalPrompt() {
|
|
166
|
-
const targets = getGlobalPromptTargets();
|
|
167
|
-
const state = await loadState();
|
|
168
|
-
const results = [];
|
|
169
|
-
|
|
170
|
-
for (const target of targets) {
|
|
171
|
-
try {
|
|
172
|
-
if (!existsSync(target.filePath)) {
|
|
173
|
-
results.push({ name: target.name, filePath: target.filePath, status: "skipped" });
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const existing = await readFile(target.filePath, "utf-8");
|
|
178
|
-
if (!existing.includes(START_MARKER)) {
|
|
179
|
-
results.push({ name: target.name, filePath: target.filePath, status: "skipped" });
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const clean = stripPromptBlock(existing);
|
|
184
|
-
const key = target.filePath;
|
|
185
|
-
const prev = state[key];
|
|
186
|
-
|
|
187
|
-
if (clean.length === 0) {
|
|
188
|
-
// Only delete the file if we created it; otherwise leave the user's file in place.
|
|
189
|
-
if (prev && prev.existedBefore === false) {
|
|
190
|
-
await unlink(target.filePath);
|
|
191
|
-
results.push({ name: target.name, filePath: target.filePath, status: "removed_file" });
|
|
192
|
-
} else {
|
|
193
|
-
await atomicWrite(target.filePath, "");
|
|
194
|
-
results.push({ name: target.name, filePath: target.filePath, status: "disabled" });
|
|
195
|
-
}
|
|
196
|
-
} else {
|
|
197
|
-
await atomicWrite(target.filePath, clean + "\n");
|
|
198
|
-
results.push({ name: target.name, filePath: target.filePath, status: "disabled" });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
delete state[key];
|
|
202
|
-
} catch (err) {
|
|
203
|
-
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
await saveState(state);
|
|
208
|
-
return results;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export async function getGlobalPromptStatus() {
|
|
212
|
-
const targets = getGlobalPromptTargets();
|
|
213
|
-
const status = [];
|
|
214
|
-
|
|
215
|
-
for (const target of targets) {
|
|
216
|
-
let enabled = false;
|
|
217
|
-
if (existsSync(target.filePath)) {
|
|
218
|
-
const content = await readFile(target.filePath, "utf-8");
|
|
219
|
-
enabled = content.includes(START_MARKER) && content.includes(END_MARKER);
|
|
220
|
-
}
|
|
221
|
-
status.push({ name: target.name, filePath: target.filePath, enabled });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return status;
|
|
225
|
-
}
|
|
136
|
+
const updated = upsertPromptBlock(existing, block);
|
|
137
|
+
|
|
138
|
+
const key = target.filePath;
|
|
139
|
+
const prev = state[key];
|
|
140
|
+
|
|
141
|
+
if (existed && existing === updated) {
|
|
142
|
+
results.push({ name: target.name, filePath: target.filePath, status: "up_to_date" });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Hash guard: if the user modified the file since we last wrote it,
|
|
147
|
+
// back it up before overwriting their edits.
|
|
148
|
+
if (existed && prev && prev.hash && prev.hash !== sha256(existing)) {
|
|
149
|
+
await backupFile(target.filePath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await atomicWrite(target.filePath, updated);
|
|
153
|
+
state[key] = { hash: sha256(updated), existedBefore: existed };
|
|
154
|
+
results.push({
|
|
155
|
+
name: target.name,
|
|
156
|
+
filePath: target.filePath,
|
|
157
|
+
status: existed ? "enabled" : "created_new_file",
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
|
+
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await saveState(state);
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function disableGlobalPrompt() {
|
|
169
|
+
const targets = getGlobalPromptTargets();
|
|
170
|
+
const state = await loadState();
|
|
171
|
+
const results = [];
|
|
172
|
+
|
|
173
|
+
for (const target of targets) {
|
|
174
|
+
try {
|
|
175
|
+
if (!existsSync(target.filePath)) {
|
|
176
|
+
results.push({ name: target.name, filePath: target.filePath, status: "skipped" });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const existing = await readFile(target.filePath, "utf-8");
|
|
181
|
+
if (!existing.includes(START_MARKER)) {
|
|
182
|
+
results.push({ name: target.name, filePath: target.filePath, status: "skipped" });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const clean = stripPromptBlock(existing);
|
|
187
|
+
const key = target.filePath;
|
|
188
|
+
const prev = state[key];
|
|
189
|
+
|
|
190
|
+
if (clean.length === 0) {
|
|
191
|
+
// Only delete the file if we created it; otherwise leave the user's file in place.
|
|
192
|
+
if (prev && prev.existedBefore === false) {
|
|
193
|
+
await unlink(target.filePath);
|
|
194
|
+
results.push({ name: target.name, filePath: target.filePath, status: "removed_file" });
|
|
195
|
+
} else {
|
|
196
|
+
await atomicWrite(target.filePath, "");
|
|
197
|
+
results.push({ name: target.name, filePath: target.filePath, status: "disabled" });
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
await atomicWrite(target.filePath, clean + "\n");
|
|
201
|
+
results.push({ name: target.name, filePath: target.filePath, status: "disabled" });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
delete state[key];
|
|
205
|
+
} catch (err) {
|
|
206
|
+
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await saveState(state);
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function getGlobalPromptStatus() {
|
|
215
|
+
const targets = getGlobalPromptTargets();
|
|
216
|
+
const status = [];
|
|
217
|
+
|
|
218
|
+
for (const target of targets) {
|
|
219
|
+
let enabled = false;
|
|
220
|
+
if (existsSync(target.filePath)) {
|
|
221
|
+
const content = await readFile(target.filePath, "utf-8");
|
|
222
|
+
enabled = content.includes(START_MARKER) && content.includes(END_MARKER);
|
|
223
|
+
}
|
|
224
|
+
status.push({ name: target.name, filePath: target.filePath, enabled });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return status;
|
|
228
|
+
}
|