@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.
@@ -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
- let existing = "";
58
- if (existsSync(target.filePath)) {
59
- existing = await readFile(target.filePath, "utf-8");
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
- const clean = stripPromptBlock(existing);
63
- const updated = clean ? `${clean}\n\n${PROMPT_BLOCK}\n` : `${PROMPT_BLOCK}\n`;
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 writeFile(target.filePath, updated, "utf-8");
66
- results.push({ name: target.name, filePath: target.filePath, status: "enabled" });
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
- await unlink(target.filePath);
91
- results.push({ name: target.name, filePath: target.filePath, status: "removed_file" });
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 writeFile(target.filePath, clean + "\n", "utf-8");
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
 
@@ -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.7",
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",