@lotargo/memory_plugin 1.2.7 → 1.2.8
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/mcp-server/prompt_manager.js +118 -11
- package/mcp-server/setup.js +6 -0
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
1
|
+
import { readFile, writeFile, mkdir, unlink, rename, copyFile } from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
+
import { createHash } from "crypto";
|
|
5
6
|
|
|
6
7
|
const START_MARKER = "<!-- START MEMORY AGENT PROMPT -->";
|
|
7
8
|
const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
|
|
@@ -13,24 +14,90 @@ export const PROMPT_BLOCK = `${START_MARKER}
|
|
|
13
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.
|
|
14
15
|
${END_MARKER}`;
|
|
15
16
|
|
|
17
|
+
// Plugin-owned files live here so we never destroy user-owned config content.
|
|
18
|
+
const AGENT_CONFIG_DIR = join(homedir(), ".config", "memory-agent");
|
|
19
|
+
export const PROMPT_FILE = join(AGENT_CONFIG_DIR, "prompt.md");
|
|
20
|
+
const BACKUP_DIR = join(AGENT_CONFIG_DIR, "backups");
|
|
21
|
+
const STATE_FILE = join(AGENT_CONFIG_DIR, "prompt-state.json");
|
|
22
|
+
|
|
23
|
+
function sha256(content) {
|
|
24
|
+
return createHash("sha256").update(content).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function atomicWrite(filePath, content) {
|
|
28
|
+
const tmp = `${filePath}.memory-tmp-${process.pid}`;
|
|
29
|
+
await writeFile(tmp, content, "utf-8");
|
|
30
|
+
await rename(tmp, filePath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function backupFile(filePath) {
|
|
34
|
+
await mkdir(BACKUP_DIR, { recursive: true });
|
|
35
|
+
const name = filePath.split(/[\\/]/).pop() || "config";
|
|
36
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
37
|
+
const dest = join(BACKUP_DIR, `${name}.${stamp}.bak`);
|
|
38
|
+
await copyFile(filePath, dest);
|
|
39
|
+
return dest;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadState() {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(await readFile(STATE_FILE, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function saveState(state) {
|
|
51
|
+
await mkdir(AGENT_CONFIG_DIR, { recursive: true });
|
|
52
|
+
await atomicWrite(STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
export function getGlobalPromptTargets() {
|
|
17
56
|
const home = homedir();
|
|
18
57
|
return [
|
|
19
58
|
{
|
|
20
59
|
name: "Antigravity",
|
|
21
60
|
filePath: join(home, ".gemini", "config", "AGENTS.md"),
|
|
61
|
+
// Antigravity resolves `@` imports only in GEMINI.md, not reliably in AGENTS.md
|
|
62
|
+
includeSupported: false,
|
|
22
63
|
},
|
|
23
64
|
{
|
|
24
65
|
name: "Codex",
|
|
25
66
|
filePath: join(home, ".codex", "AGENTS.md"),
|
|
67
|
+
// AGENTS.md standard has no import syntax
|
|
68
|
+
includeSupported: false,
|
|
26
69
|
},
|
|
27
70
|
{
|
|
28
71
|
name: "Claude Code",
|
|
29
72
|
filePath: join(home, ".claude", "CLAUDE.md"),
|
|
73
|
+
// @path imports are supported and trusted in user-scope CLAUDE.md
|
|
74
|
+
includeSupported: true,
|
|
30
75
|
},
|
|
31
76
|
];
|
|
32
77
|
}
|
|
33
78
|
|
|
79
|
+
// Ensure the plugin-owned prompt file exists and is up to date with PROMPT_BLOCK.
|
|
80
|
+
// This file is referenced via `@` includes; updating it never touches user configs.
|
|
81
|
+
export async function syncPromptFile() {
|
|
82
|
+
await mkdir(AGENT_CONFIG_DIR, { recursive: true });
|
|
83
|
+
const expected = `${PROMPT_BLOCK}\n`;
|
|
84
|
+
const current = existsSync(PROMPT_FILE) ? await readFile(PROMPT_FILE, "utf-8") : "";
|
|
85
|
+
if (current !== expected) {
|
|
86
|
+
await atomicWrite(PROMPT_FILE, expected);
|
|
87
|
+
}
|
|
88
|
+
return PROMPT_FILE;
|
|
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}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
34
101
|
function stripPromptBlock(content) {
|
|
35
102
|
const startIndex = content.indexOf(START_MARKER);
|
|
36
103
|
const endIndex = content.indexOf(END_MARKER);
|
|
@@ -44,7 +111,9 @@ function stripPromptBlock(content) {
|
|
|
44
111
|
}
|
|
45
112
|
|
|
46
113
|
export async function enableGlobalPrompt() {
|
|
114
|
+
const promptFile = await syncPromptFile();
|
|
47
115
|
const targets = getGlobalPromptTargets();
|
|
116
|
+
const state = await loadState();
|
|
48
117
|
const results = [];
|
|
49
118
|
|
|
50
119
|
for (const target of targets) {
|
|
@@ -54,26 +123,48 @@ export async function enableGlobalPrompt() {
|
|
|
54
123
|
await mkdir(parentDir, { recursive: true });
|
|
55
124
|
}
|
|
56
125
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
126
|
+
const existed = existsSync(target.filePath);
|
|
127
|
+
const existing = existed ? await readFile(target.filePath, "utf-8") : "";
|
|
128
|
+
const clean = stripPromptBlock(existing);
|
|
129
|
+
|
|
130
|
+
const block = target.includeSupported
|
|
131
|
+
? buildIncludeBlock(promptFile)
|
|
132
|
+
: PROMPT_BLOCK;
|
|
133
|
+
const updated = clean ? `${clean}\n\n${block}\n` : `${block}\n`;
|
|
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;
|
|
60
141
|
}
|
|
61
142
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|
|
64
148
|
|
|
65
|
-
await
|
|
66
|
-
|
|
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
|
+
});
|
|
67
156
|
} catch (err) {
|
|
68
157
|
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
69
158
|
}
|
|
70
159
|
}
|
|
71
160
|
|
|
161
|
+
await saveState(state);
|
|
72
162
|
return results;
|
|
73
163
|
}
|
|
74
164
|
|
|
75
165
|
export async function disableGlobalPrompt() {
|
|
76
166
|
const targets = getGlobalPromptTargets();
|
|
167
|
+
const state = await loadState();
|
|
77
168
|
const results = [];
|
|
78
169
|
|
|
79
170
|
for (const target of targets) {
|
|
@@ -84,20 +175,36 @@ export async function disableGlobalPrompt() {
|
|
|
84
175
|
}
|
|
85
176
|
|
|
86
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
|
+
|
|
87
183
|
const clean = stripPromptBlock(existing);
|
|
184
|
+
const key = target.filePath;
|
|
185
|
+
const prev = state[key];
|
|
88
186
|
|
|
89
187
|
if (clean.length === 0) {
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
196
|
} else {
|
|
93
|
-
await
|
|
197
|
+
await atomicWrite(target.filePath, clean + "\n");
|
|
94
198
|
results.push({ name: target.name, filePath: target.filePath, status: "disabled" });
|
|
95
199
|
}
|
|
200
|
+
|
|
201
|
+
delete state[key];
|
|
96
202
|
} catch (err) {
|
|
97
203
|
results.push({ name: target.name, filePath: target.filePath, status: "failed", error: err.message });
|
|
98
204
|
}
|
|
99
205
|
}
|
|
100
206
|
|
|
207
|
+
await saveState(state);
|
|
101
208
|
return results;
|
|
102
209
|
}
|
|
103
210
|
|
package/mcp-server/setup.js
CHANGED
|
@@ -176,6 +176,12 @@ export async function runSetup() {
|
|
|
176
176
|
promptResults.forEach((r) => {
|
|
177
177
|
if (r.status === "enabled") {
|
|
178
178
|
console.log(` [OK] ${r.name}: enabled global prompt instruction in ${r.filePath}`);
|
|
179
|
+
} else if (r.status === "created_new_file") {
|
|
180
|
+
console.log(` [OK] ${r.name}: created ${r.filePath} with global prompt instruction`);
|
|
181
|
+
} else if (r.status === "up_to_date") {
|
|
182
|
+
console.log(` [OK] ${r.name}: global prompt already up to date (${r.filePath})`);
|
|
183
|
+
} else if (r.status === "failed") {
|
|
184
|
+
console.log(` [WARN] ${r.name}: failed to enable global prompt (${r.error})`);
|
|
179
185
|
}
|
|
180
186
|
});
|
|
181
187
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotargo/memory_plugin",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Persistent memory agent for coding AI tools — remembers user preferences and project context across sessions. Works with Antigravity, OpenCode, Claude Code, and Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "opencode-plugin/index.js",
|