opencode-memoir 1.0.5 → 1.0.6
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.
Potentially problematic release.
This version of opencode-memoir might be problematic. Click here for more details.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +835 -422
- package/dist/index.js.map +7 -1
- package/package.json +6 -2
- package/dist/capture.js +0 -232
- package/dist/capture.js.map +0 -1
- package/dist/debug.js +0 -11
- package/dist/debug.js.map +0 -1
- package/dist/recall-gate.js +0 -65
- package/dist/recall-gate.js.map +0 -1
- package/dist/store.js +0 -304
- package/dist/store.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,464 +1,877 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir as mkdir2, readFile, rm as rm2, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join as join2 } from "node:path";
|
|
6
|
+
import { homedir as homedir2 } from "node:os";
|
|
7
|
+
import { tool } from "@opencode-ai/plugin";
|
|
8
|
+
|
|
9
|
+
// src/store.ts
|
|
10
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
11
|
+
import { access, mkdir, rm } from "node:fs/promises";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { homedir, tmpdir } from "node:os";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
|
|
16
|
+
// src/debug.ts
|
|
17
|
+
function debugLog(...args) {
|
|
18
|
+
if (process.env.MEMOIR_DEBUG !== "1") return;
|
|
19
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
20
|
+
process.stderr.write(`[memoir ${ts}] ${args.map((a) => String(a)).join(" ")}
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/store.ts
|
|
25
|
+
var execFileAsync = promisify(execFile);
|
|
26
|
+
var MEMOIR_PACKAGE = "memoir-ai";
|
|
27
|
+
var MEMOIR_AI_PIN = "0.2.2";
|
|
28
|
+
var _noGitCache = /* @__PURE__ */ new Set();
|
|
29
|
+
function _main_worktree_root(cwd) {
|
|
30
|
+
if (_noGitCache.has(cwd)) return "";
|
|
31
|
+
try {
|
|
32
|
+
const gitDir = execFileSync("git", ["rev-parse", "--git-dir"], { cwd, encoding: "utf8", timeout: 3e3 }).trim();
|
|
33
|
+
const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], { cwd, encoding: "utf8", timeout: 3e3 }).trim();
|
|
34
|
+
const resolvePath = (path) => {
|
|
35
|
+
if (path.startsWith("/")) return path;
|
|
36
|
+
return join(cwd, path);
|
|
37
|
+
};
|
|
38
|
+
const gitDirAbs = resolvePath(gitDir);
|
|
39
|
+
const gitCommonDirAbs = resolvePath(gitCommonDir);
|
|
40
|
+
if (gitDirAbs === gitCommonDirAbs) {
|
|
41
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", timeout: 3e3 }).trim();
|
|
42
|
+
}
|
|
43
|
+
const worktreeList = execFileSync("git", ["worktree", "list", "--porcelain"], { cwd, encoding: "utf8", timeout: 3e3 });
|
|
44
|
+
const firstLine = worktreeList.split("\n")[0];
|
|
45
|
+
if (firstLine.startsWith("worktree ")) {
|
|
46
|
+
const mainWorktree = firstLine.substring("worktree ".length).trim();
|
|
47
|
+
if (mainWorktree && mainWorktree !== "(bare)") {
|
|
48
|
+
return mainWorktree;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", timeout: 3e3 }).trim();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
debugLog("_main_worktree_root: not a git repo or git error:", e instanceof Error ? e.message : String(e));
|
|
54
|
+
_noGitCache.add(cwd);
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function deriveStorePath(cwd = process.cwd()) {
|
|
59
|
+
if (pluginStoreOverride) return pluginStoreOverride;
|
|
60
|
+
const configured = process.env.MEMOIR_STORE;
|
|
61
|
+
if (configured) return configured;
|
|
62
|
+
let projectDir;
|
|
63
|
+
try {
|
|
64
|
+
const gitRoot = _main_worktree_root(cwd);
|
|
65
|
+
if (gitRoot) {
|
|
66
|
+
projectDir = gitRoot;
|
|
67
|
+
} else {
|
|
68
|
+
projectDir = resolve(cwd);
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
debugLog("deriveStorePath: unexpected error:", e instanceof Error ? e.message : String(e));
|
|
72
|
+
projectDir = cwd;
|
|
73
|
+
}
|
|
74
|
+
const slug = projectDir.replace(/[/.]/g, "-");
|
|
75
|
+
return join(homedir(), ".memoir", slug);
|
|
76
|
+
}
|
|
77
|
+
var pluginStoreOverride;
|
|
78
|
+
function setPluginStoreOverride(store) {
|
|
79
|
+
pluginStoreOverride = store;
|
|
80
|
+
}
|
|
81
|
+
var ensuredStores = /* @__PURE__ */ new Set();
|
|
82
|
+
var storeCreations = /* @__PURE__ */ new Map();
|
|
83
|
+
async function ensureStore(store) {
|
|
84
|
+
if (ensuredStores.has(store)) return;
|
|
85
|
+
const inFlight = storeCreations.get(store);
|
|
86
|
+
if (inFlight) {
|
|
87
|
+
await inFlight;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await access(join(store, ".git"));
|
|
92
|
+
ensuredStores.add(store);
|
|
93
|
+
return;
|
|
94
|
+
} catch {
|
|
95
|
+
await mkdir(join(store, ".."), { recursive: true }).catch((e) => debugLog("ensureStore: mkdir failed:", e instanceof Error ? e.message : String(e)));
|
|
96
|
+
}
|
|
97
|
+
const creationPromise = (async () => {
|
|
98
|
+
const tmpDir = join(tmpdir(), `memoir-scratch-${Date.now()}`);
|
|
14
99
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
100
|
+
await mkdir(tmpDir, { recursive: true });
|
|
101
|
+
await execFileAsync("git", ["init", "-q", tmpDir], { timeout: 5e3 });
|
|
102
|
+
const result = await runMemoir(["new", store, "--taxonomy-builtin"], { cwd: tmpDir });
|
|
103
|
+
if (result.startsWith("Memoir command failed")) {
|
|
104
|
+
throw new Error(result);
|
|
105
|
+
}
|
|
106
|
+
ensuredStores.add(store);
|
|
107
|
+
} finally {
|
|
108
|
+
rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
storeCreations.set(store, creationPromise);
|
|
112
|
+
try {
|
|
113
|
+
await creationPromise;
|
|
114
|
+
} finally {
|
|
115
|
+
storeCreations.delete(store);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var memoirResolved = null;
|
|
119
|
+
async function runMemoir(args, options = {}) {
|
|
120
|
+
const specs = memoirSpawnSpecs(args);
|
|
121
|
+
if (memoirResolved) {
|
|
122
|
+
const idx = specs.findIndex((s) => s.label === memoirResolved);
|
|
123
|
+
if (idx > 0) {
|
|
124
|
+
const [cached] = specs.splice(idx, 1);
|
|
125
|
+
specs.unshift(cached);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const spec of specs) {
|
|
129
|
+
try {
|
|
130
|
+
const { stdout } = await execFileAsync(spec.command, spec.args, {
|
|
131
|
+
cwd: options.cwd ?? process.cwd(),
|
|
132
|
+
env: process.env,
|
|
133
|
+
maxBuffer: 1024 * 1024,
|
|
134
|
+
timeout: 15e3
|
|
135
|
+
// prevent hang if memoir CLI stalls
|
|
136
|
+
});
|
|
137
|
+
memoirResolved = spec.label;
|
|
138
|
+
return stdout.trim();
|
|
139
|
+
} catch (e) {
|
|
140
|
+
debugLog("runMemoir: fallback", spec.label, "failed:", e instanceof Error ? e.message : String(e));
|
|
141
|
+
if (memoirResolved === spec.label) memoirResolved = null;
|
|
19
142
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const { stdout } = await execFileAsync("memoir", args, {
|
|
146
|
+
cwd: options.cwd ?? process.cwd(),
|
|
147
|
+
env: process.env,
|
|
148
|
+
maxBuffer: 1024 * 1024,
|
|
149
|
+
timeout: 15e3
|
|
150
|
+
});
|
|
151
|
+
return stdout.trim();
|
|
152
|
+
} catch (memoirError) {
|
|
153
|
+
const err = memoirError;
|
|
154
|
+
const detail = (err.stderr || err.stdout || err.message).trim();
|
|
155
|
+
return `Memoir command failed${err.code ? ` (${err.code})` : ""}: ${detail}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function memoirSpawnSpecs(args) {
|
|
159
|
+
return [
|
|
160
|
+
{ command: "memoir", args, label: "memoir" },
|
|
161
|
+
{ command: "uvx", args: ["--from", `${MEMOIR_PACKAGE}==${MEMOIR_AI_PIN}`, "memoir", ...args], label: "uvx" },
|
|
162
|
+
{ command: "uv", args: ["tool", "run", "--from", `${MEMOIR_PACKAGE}==${MEMOIR_AI_PIN}`, "memoir", ...args], label: "uv tool run" }
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
async function getCurrentBranch(store) {
|
|
166
|
+
try {
|
|
167
|
+
const raw = await runMemoir(["--json", "-s", store, "status"], { cwd: store });
|
|
168
|
+
const data = JSON.parse(raw);
|
|
169
|
+
return data.branch || "unknown";
|
|
170
|
+
} catch (e) {
|
|
171
|
+
debugLog("getCurrentBranch: failed:", e instanceof Error ? e.message : String(e));
|
|
172
|
+
return "unknown";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function codeGitBranch() {
|
|
176
|
+
try {
|
|
177
|
+
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], { encoding: "utf8", timeout: 3e3 });
|
|
178
|
+
return stdout.trim();
|
|
179
|
+
} catch (e) {
|
|
180
|
+
debugLog("codeGitBranch: failed:", e instanceof Error ? e.message : String(e));
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function branchExistsInMemoir(store, name) {
|
|
185
|
+
if (!name) return false;
|
|
186
|
+
try {
|
|
187
|
+
const raw = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
|
|
188
|
+
const data = JSON.parse(raw);
|
|
189
|
+
const branches = data?.branches ?? [];
|
|
190
|
+
return branches.includes(name);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
debugLog("branchExistsInMemoir: failed:", e instanceof Error ? e.message : String(e));
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function autoMatchMemoirBranch(store) {
|
|
197
|
+
const codeBranch = await codeGitBranch();
|
|
198
|
+
if (!codeBranch) {
|
|
199
|
+
return getCurrentBranch(store);
|
|
200
|
+
}
|
|
201
|
+
const current = await getCurrentBranch(store);
|
|
202
|
+
if (current === codeBranch) return current;
|
|
203
|
+
if (!await branchExistsInMemoir(store, codeBranch)) {
|
|
204
|
+
const result2 = await runMemoir(["-s", store, "branch", codeBranch, "--from", "main"], { cwd: store });
|
|
205
|
+
if (result2.startsWith("Memoir command failed")) {
|
|
206
|
+
debugLog("autoMatchMemoirBranch: create branch failed:", result2);
|
|
207
|
+
return getCurrentBranch(store);
|
|
23
208
|
}
|
|
209
|
+
}
|
|
210
|
+
const result = await runMemoir(["-s", store, "checkout", codeBranch], { cwd: store });
|
|
211
|
+
if (result.startsWith("Memoir command failed")) {
|
|
212
|
+
debugLog("autoMatchMemoirBranch: checkout failed:", result);
|
|
213
|
+
return getCurrentBranch(store);
|
|
214
|
+
}
|
|
215
|
+
return codeBranch;
|
|
24
216
|
}
|
|
25
|
-
async function
|
|
217
|
+
async function readMemoirValue(store, key, namespace = "default") {
|
|
218
|
+
try {
|
|
219
|
+
const raw = await runMemoir(["--json", "-s", store, "get", key, "-n", namespace], { cwd: store });
|
|
220
|
+
const parsed = JSON.parse(raw);
|
|
221
|
+
const items = parsed?.items ?? [];
|
|
222
|
+
const value = items[0]?.value?.content;
|
|
223
|
+
return typeof value === "string" ? value : "";
|
|
224
|
+
} catch (e) {
|
|
225
|
+
debugLog("readMemoirValue: failed:", e instanceof Error ? e.message : String(e));
|
|
226
|
+
return "";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/capture.ts
|
|
231
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch", "ApplyPatch", "MultiFileEdit"]);
|
|
232
|
+
var METRICS_CODE_MAX = 1e3;
|
|
233
|
+
var pendingEditsBySession = /* @__PURE__ */ new Map();
|
|
234
|
+
var toolMetricsBySession = /* @__PURE__ */ new Map();
|
|
235
|
+
var cachedBranchBySession = /* @__PURE__ */ new Map();
|
|
236
|
+
function getCachedBranch(sessionID) {
|
|
237
|
+
return cachedBranchBySession.get(sessionID) ?? "unknown";
|
|
238
|
+
}
|
|
239
|
+
function setCachedBranch(sessionID, branch) {
|
|
240
|
+
cachedBranchBySession.set(sessionID, branch);
|
|
241
|
+
}
|
|
242
|
+
function recordEdit(sessionID, edit) {
|
|
243
|
+
let edits = pendingEditsBySession.get(sessionID);
|
|
244
|
+
if (!edits) {
|
|
245
|
+
edits = [];
|
|
246
|
+
pendingEditsBySession.set(sessionID, edits);
|
|
247
|
+
}
|
|
248
|
+
edits.push(edit);
|
|
249
|
+
}
|
|
250
|
+
function recordToolMetrics(sessionID, tool2, metrics) {
|
|
251
|
+
let sessionMetrics = toolMetricsBySession.get(sessionID);
|
|
252
|
+
if (!sessionMetrics) {
|
|
253
|
+
sessionMetrics = /* @__PURE__ */ new Map();
|
|
254
|
+
toolMetricsBySession.set(sessionID, sessionMetrics);
|
|
255
|
+
}
|
|
256
|
+
const prev = sessionMetrics.get(tool2) ?? { calls: 0, errors: 0 };
|
|
257
|
+
sessionMetrics.set(tool2, {
|
|
258
|
+
calls: prev.calls + metrics.calls,
|
|
259
|
+
errors: prev.errors + metrics.errors
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function parseTurnMetrics(text) {
|
|
263
|
+
const result = /* @__PURE__ */ new Map();
|
|
264
|
+
if (!text) return result;
|
|
265
|
+
const parts = text.split("|");
|
|
266
|
+
for (const part of parts) {
|
|
267
|
+
const trimmed = part.trim();
|
|
268
|
+
if (!trimmed) continue;
|
|
269
|
+
const [tool2, callsStr, errorsStr] = trimmed.split(":");
|
|
270
|
+
if (!tool2) continue;
|
|
271
|
+
result.set(tool2, {
|
|
272
|
+
calls: parseInt(callsStr, 10) || 0,
|
|
273
|
+
errors: parseInt(errorsStr, 10) || 0
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
function serializeTurnMetrics(map) {
|
|
279
|
+
return [...map.entries()].map(([tool2, m]) => `${tool2}:${m.calls}:${m.errors}`).join(" | ");
|
|
280
|
+
}
|
|
281
|
+
var flushQueues = /* @__PURE__ */ new Map();
|
|
282
|
+
async function acquireFlushLock(store) {
|
|
283
|
+
const prev = flushQueues.get(store) ?? Promise.resolve();
|
|
284
|
+
let release;
|
|
285
|
+
const next = new Promise((resolve2) => {
|
|
286
|
+
release = resolve2;
|
|
287
|
+
});
|
|
288
|
+
flushQueues.set(store, next);
|
|
289
|
+
await prev;
|
|
290
|
+
return () => {
|
|
291
|
+
release();
|
|
292
|
+
if (flushQueues.get(store) === next) flushQueues.delete(store);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function flushCapture(store, branch, sessionID) {
|
|
296
|
+
const targets = sessionID ? [sessionID] : [...pendingEditsBySession.keys()];
|
|
297
|
+
if (targets.length === 0) return;
|
|
298
|
+
try {
|
|
299
|
+
store = store ?? deriveStorePath();
|
|
300
|
+
if (!store) return;
|
|
26
301
|
await ensureStore(store);
|
|
27
|
-
const
|
|
28
|
-
await mkdir(pidDir, { recursive: true });
|
|
29
|
-
const hash = createHash('sha256').update(store).digest('hex').slice(0, 8);
|
|
30
|
-
const pidfile = join(pidDir, `${hash}.json`);
|
|
302
|
+
const releaseLock = await acquireFlushLock(store);
|
|
31
303
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
304
|
+
const perSession = [];
|
|
305
|
+
const swappedSids = /* @__PURE__ */ new Map();
|
|
306
|
+
for (const sid of targets) {
|
|
307
|
+
const idx = perSession.length;
|
|
308
|
+
const edits = pendingEditsBySession.get(sid) ?? [];
|
|
309
|
+
pendingEditsBySession.set(sid, []);
|
|
310
|
+
const metrics = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
311
|
+
toolMetricsBySession.set(sid, /* @__PURE__ */ new Map());
|
|
312
|
+
const sb = branch ?? cachedBranchBySession.get(sid);
|
|
313
|
+
perSession.push({ edits, metrics, sessionBranch: sb ?? "unknown" });
|
|
314
|
+
swappedSids.set(sid, idx);
|
|
315
|
+
}
|
|
316
|
+
if (!perSession.some((p) => p.edits.length > 0 || p.metrics.size > 0)) return;
|
|
317
|
+
try {
|
|
318
|
+
for (const p of perSession) {
|
|
319
|
+
if (p.sessionBranch === "unknown" || !p.sessionBranch) {
|
|
320
|
+
p.sessionBranch = await getCurrentBranch(store);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const p of perSession) {
|
|
324
|
+
const { edits, metrics, sessionBranch: branchKey } = p;
|
|
325
|
+
if (edits.length === 0 && metrics.size === 0) continue;
|
|
326
|
+
const [prevCodeRaw, prevTurnRaw] = await Promise.all([
|
|
327
|
+
edits.length > 0 ? readMemoirValue(store, `metrics.code.${branchKey}`) : Promise.resolve(""),
|
|
328
|
+
metrics.size > 0 ? readMemoirValue(store, `metrics.turn.${branchKey}`) : Promise.resolve("")
|
|
329
|
+
]);
|
|
330
|
+
let codeWrite;
|
|
331
|
+
if (edits.length > 0) {
|
|
332
|
+
const files = [...new Set(edits.map((e) => e.filePath))];
|
|
333
|
+
const entry = {
|
|
334
|
+
timestamp: Date.now() / 1e3,
|
|
335
|
+
summary: `Changed ${edits.length} block(s) across ${files.length} file(s): ${files.join(", ")}`,
|
|
336
|
+
files
|
|
337
|
+
};
|
|
338
|
+
let acc = {
|
|
339
|
+
schema_version: 2,
|
|
340
|
+
entries: []
|
|
341
|
+
};
|
|
342
|
+
if (prevCodeRaw) {
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(prevCodeRaw);
|
|
345
|
+
if (parsed?.entries && Array.isArray(parsed.entries)) {
|
|
346
|
+
acc = parsed;
|
|
347
|
+
if (acc.schema_version < 2) acc.schema_version = 2;
|
|
348
|
+
}
|
|
349
|
+
} catch (e) {
|
|
350
|
+
debugLog("flushCapture: failed to parse existing code metrics, starting fresh:", e instanceof Error ? e.message : String(e));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
acc.entries.push(entry);
|
|
354
|
+
if (acc.entries.length > METRICS_CODE_MAX) acc.entries = acc.entries.slice(-METRICS_CODE_MAX);
|
|
355
|
+
codeWrite = runMemoir(["-s", store, "remember", "--replace", JSON.stringify(acc), "-p", `metrics.code.${branchKey}`], { cwd: store });
|
|
356
|
+
}
|
|
357
|
+
let turnWrite;
|
|
358
|
+
if (metrics.size > 0) {
|
|
359
|
+
const existing = parseTurnMetrics(prevTurnRaw);
|
|
360
|
+
for (const [tool2, current] of metrics) {
|
|
361
|
+
const prev = existing.get(tool2) ?? { calls: 0, errors: 0 };
|
|
362
|
+
existing.set(tool2, { calls: prev.calls + current.calls, errors: prev.errors + current.errors });
|
|
37
363
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
364
|
+
turnWrite = runMemoir(["-s", store, "remember", "--replace", serializeTurnMetrics(existing), "-p", `metrics.turn.${branchKey}`], { cwd: store });
|
|
365
|
+
}
|
|
366
|
+
const results = await Promise.all([codeWrite, turnWrite].filter(Boolean));
|
|
367
|
+
for (const result of results) {
|
|
368
|
+
if (typeof result === "string" && result.startsWith("Memoir command failed")) {
|
|
369
|
+
throw new Error(`Memoir write failed: ${result}`);
|
|
41
370
|
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch (e) {
|
|
374
|
+
for (const [sid, idx] of swappedSids) {
|
|
375
|
+
const p = perSession[idx];
|
|
376
|
+
const currentEdits = pendingEditsBySession.get(sid) ?? [];
|
|
377
|
+
pendingEditsBySession.set(sid, [...p.edits, ...currentEdits]);
|
|
378
|
+
const merged = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
379
|
+
for (const [tool2, m] of p.metrics) {
|
|
380
|
+
const prev = merged.get(tool2) ?? { calls: 0, errors: 0 };
|
|
381
|
+
merged.set(tool2, { calls: prev.calls + m.calls, errors: prev.errors + m.errors });
|
|
382
|
+
}
|
|
383
|
+
toolMetricsBySession.set(sid, merged);
|
|
42
384
|
}
|
|
385
|
+
throw e;
|
|
386
|
+
}
|
|
387
|
+
} finally {
|
|
388
|
+
releaseLock();
|
|
43
389
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
390
|
+
} catch (e) {
|
|
391
|
+
debugLog("flushCapture: failed:", e instanceof Error ? e.message : String(e));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/recall-gate.ts
|
|
396
|
+
var SECRET_PATTERN = /(api[_-]?key|auth_token|access_token|\btoken\b|\bsecret\b|\bpassword\b|\bpasswd\b|private[_-]?key|-----BEGIN [A-Z ]*PRIVATE KEY-----)/i;
|
|
397
|
+
function isSecretSanitizationEnabled() {
|
|
398
|
+
return process.env.MEMOIR_SANITIZE_SECRETS !== "0";
|
|
399
|
+
}
|
|
400
|
+
var pendingRecall = /* @__PURE__ */ new Set();
|
|
401
|
+
var ACK_PATTERN = /^(ok|thanks|thank you|sounds good|got it|👍|🙏|perfect|great|cool|nice|awesome|understood|makes sense|agree|right|sure|yes|no|done|nvm|never mind|lgtm|looks good|proceed|continue|good|fine)\b/i;
|
|
402
|
+
var EXPLICIT_RECALL_PATTERN = /\b(memoir:recall|memoir:remember|memoir-recall|memoir-remember)\b|(\/recall|\/remember)\b/i;
|
|
403
|
+
var RECALL_TRIGGER_PATTERNS = [
|
|
404
|
+
// Action verbs and domain nouns
|
|
405
|
+
/\b(add|build|implement|refactor|redesign|design|create|write|set( |-)up|wire( |-)up|integrate|migrate|rewrite|extract|extend|plumb|hook( |-)up|ship|scaffold|optimize|fix|debug|review|architect|model|schema|API|service|feature|module|system|pipeline|workflow|make|move|replace|convert|swap|remove|clean( |-)up|transform|investigate|explore|figure( |-)out|plan|decide|choose|pick|compare|walk( |-)?me( |-)?through|take( |-)?a( |-)?stab|help( |-)?me|harness|hook|prompt|test)\b/i,
|
|
406
|
+
// Question starts (how/why/what/where/when/should/can/could/would/is it/are we/do I/does it)
|
|
407
|
+
/^(how|why|what|where|when|should|can|could|would|is it|are we|do I|does it)\b.*\?/im,
|
|
408
|
+
// Code blocks (triple backticks)
|
|
409
|
+
/```/,
|
|
410
|
+
// Code definitions
|
|
411
|
+
/\b(def|function|class|import|export)\s+/,
|
|
412
|
+
// Memoir/recall keywords
|
|
413
|
+
/\b(memoir|recall|remember|memory)\b/i,
|
|
414
|
+
// File extensions
|
|
415
|
+
/\b\w+\.(py|js|ts|tsx|scala|java|go|rs|rb|md|json|yaml|yml|toml|sh|bash|css|html|kt|swift|c|cpp|h|hpp)\b/i,
|
|
416
|
+
// File paths (slash-containing tokens)
|
|
417
|
+
/\w+\/\w+/
|
|
418
|
+
];
|
|
419
|
+
function isAcknowledgement(text) {
|
|
420
|
+
const trimmed = text.trim().toLowerCase();
|
|
421
|
+
const words = trimmed.split(/\s+/);
|
|
422
|
+
return words.length <= 5 && ACK_PATTERN.test(trimmed);
|
|
423
|
+
}
|
|
424
|
+
function shouldTriggerRecall(text) {
|
|
425
|
+
const trimmed = text.trim();
|
|
426
|
+
if (!trimmed) return false;
|
|
427
|
+
if (EXPLICIT_RECALL_PATTERN.test(trimmed)) return true;
|
|
428
|
+
if (trimmed.length < 10) return false;
|
|
429
|
+
if (isAcknowledgement(trimmed)) return false;
|
|
430
|
+
if (trimmed.length >= 40 && RECALL_TRIGGER_PATTERNS.some((p) => p.test(trimmed))) return true;
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
var DEFAULT_RECALL_NAMESPACES = ["default", "project:onboard", "codebase:onboard"];
|
|
434
|
+
|
|
435
|
+
// src/index.ts
|
|
436
|
+
async function statusJson(store) {
|
|
437
|
+
await ensureStore(store);
|
|
438
|
+
const raw = await runMemoir(["--json", "-s", store, "status"], { cwd: store });
|
|
439
|
+
try {
|
|
440
|
+
const data = JSON.parse(raw);
|
|
441
|
+
const branch = await codeGitBranch();
|
|
442
|
+
data.opencode = { store, project_git_root: process.cwd(), project_git_branch: branch };
|
|
443
|
+
return JSON.stringify(data, null, 2);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
debugLog("statusJson: failed to parse JSON:", e instanceof Error ? e.message : String(e));
|
|
446
|
+
return raw;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function launchUi(store) {
|
|
450
|
+
await ensureStore(store);
|
|
451
|
+
const pidDir = join2(homedir2(), ".memoir", "ui-servers");
|
|
452
|
+
await mkdir2(pidDir, { recursive: true });
|
|
453
|
+
const hash = createHash("sha256").update(store).digest("hex").slice(0, 8);
|
|
454
|
+
const pidfile = join2(pidDir, `${hash}.json`);
|
|
455
|
+
try {
|
|
456
|
+
const existing = JSON.parse(await readFile(pidfile, "utf8"));
|
|
457
|
+
if (existing?.pid && existing?.url) {
|
|
458
|
+
try {
|
|
459
|
+
process.kill(Number(existing.pid), 0);
|
|
460
|
+
return JSON.stringify({ ...existing, reused: true }, null, 2);
|
|
461
|
+
} catch (e) {
|
|
462
|
+
debugLog("launchUi: process dead, relaunching:", e instanceof Error ? e.message : String(e));
|
|
463
|
+
}
|
|
47
464
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
465
|
+
} catch (e) {
|
|
466
|
+
debugLog("launchUi: failed to read pidfile:", e instanceof Error ? e.message : String(e));
|
|
467
|
+
await rm2(pidfile, { force: true }).catch(() => void 0);
|
|
468
|
+
}
|
|
469
|
+
let lastError = "";
|
|
470
|
+
const uiSpecs = memoirSpawnSpecs(["ui", store]);
|
|
471
|
+
if (memoirResolved) {
|
|
472
|
+
const idx = uiSpecs.findIndex((s) => s.label === memoirResolved);
|
|
473
|
+
if (idx > 0) {
|
|
474
|
+
const [cached] = uiSpecs.splice(idx, 1);
|
|
475
|
+
uiSpecs.unshift(cached);
|
|
57
476
|
}
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
477
|
+
}
|
|
478
|
+
for (const spec of uiSpecs) {
|
|
479
|
+
const child = spawn(spec.command, spec.args, {
|
|
480
|
+
detached: true,
|
|
481
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
482
|
+
env: process.env
|
|
483
|
+
});
|
|
484
|
+
let output = "";
|
|
485
|
+
child.stdout?.on("data", (chunk) => {
|
|
486
|
+
output += String(chunk);
|
|
487
|
+
});
|
|
488
|
+
child.stderr?.on("data", (chunk) => {
|
|
489
|
+
output += String(chunk);
|
|
490
|
+
});
|
|
491
|
+
const spawnFailed = new Promise((resolve2) => {
|
|
492
|
+
child.once("error", (error2) => resolve2(String(error2.message || error2)));
|
|
493
|
+
child.once("spawn", () => resolve2(null));
|
|
494
|
+
});
|
|
495
|
+
const error = await spawnFailed;
|
|
496
|
+
if (error) {
|
|
497
|
+
lastError = `${spec.label}: ${error}`;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
child.unref();
|
|
501
|
+
const urlPattern = /https?:\/\/(?:localhost|127\.0\.0\.1):\d+\S*/;
|
|
502
|
+
const deadline = Date.now() + 5e3;
|
|
503
|
+
while (Date.now() < deadline) {
|
|
504
|
+
const match = output.match(urlPattern);
|
|
505
|
+
if (match) {
|
|
506
|
+
const url = match[0];
|
|
507
|
+
const data = { pid: child.pid, url, store, command: spec.label, started: (/* @__PURE__ */ new Date()).toISOString(), reused: false };
|
|
508
|
+
await writeFile(pidfile, JSON.stringify(data, null, 2));
|
|
509
|
+
return JSON.stringify(data, null, 2);
|
|
510
|
+
}
|
|
511
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
90
512
|
}
|
|
91
|
-
return `Memoir UI
|
|
513
|
+
return `Memoir UI started with ${spec.label} (pid ${child.pid ?? "unknown"}), but URL was not detected yet.
|
|
514
|
+
${output.trim()}`.trim();
|
|
515
|
+
}
|
|
516
|
+
return `Memoir UI failed to start: ${lastError || "no launcher succeeded"}`;
|
|
92
517
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return Array.isArray(path) ? path.filter(Boolean) : [path];
|
|
518
|
+
function coercePaths(path) {
|
|
519
|
+
if (!path) return [];
|
|
520
|
+
return Array.isArray(path) ? path.filter(Boolean) : [path];
|
|
97
521
|
}
|
|
98
522
|
function pushText(output, text) {
|
|
99
|
-
|
|
100
|
-
|
|
523
|
+
output.parts.length = 0;
|
|
524
|
+
output.parts.push({ type: "text", text });
|
|
101
525
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return text;
|
|
109
|
-
}
|
|
526
|
+
function tryPrettyJson(text) {
|
|
527
|
+
try {
|
|
528
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
529
|
+
} catch {
|
|
530
|
+
return text;
|
|
531
|
+
}
|
|
110
532
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* Tracks sessions that have received initial context injection.
|
|
116
|
-
* Uses a Map with timestamp so stale entries can be evicted.
|
|
117
|
-
* Cleaned on dispose. Sessions older than 1 hour are considered stale.
|
|
118
|
-
*/
|
|
119
|
-
const SESSION_CONTEXT_TTL_MS = 60 * 60 * 1000;
|
|
120
|
-
const sessionsWithContext = new Map();
|
|
121
|
-
/** Check if a session has received context (and prune stale entries opportunistically). */
|
|
533
|
+
var initContext = null;
|
|
534
|
+
var initContextFetched = false;
|
|
535
|
+
var SESSION_CONTEXT_TTL_MS = 60 * 60 * 1e3;
|
|
536
|
+
var sessionsWithContext = /* @__PURE__ */ new Map();
|
|
122
537
|
function hasSessionContext(sessionID) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return true;
|
|
538
|
+
const ts = sessionsWithContext.get(sessionID);
|
|
539
|
+
if (ts === void 0) return false;
|
|
540
|
+
if (Date.now() - ts > SESSION_CONTEXT_TTL_MS) {
|
|
541
|
+
sessionsWithContext.delete(sessionID);
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
return true;
|
|
131
545
|
}
|
|
132
|
-
/** Mark a session as having received context. */
|
|
133
546
|
function markSessionContext(sessionID) {
|
|
134
|
-
|
|
547
|
+
sessionsWithContext.set(sessionID, Date.now());
|
|
135
548
|
}
|
|
136
|
-
/** Prune all stale session entries. */
|
|
137
549
|
function pruneStaleSessions() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
550
|
+
const now = Date.now();
|
|
551
|
+
for (const [id, ts] of sessionsWithContext) {
|
|
552
|
+
if (now - ts > SESSION_CONTEXT_TTL_MS) sessionsWithContext.delete(id);
|
|
553
|
+
}
|
|
143
554
|
}
|
|
144
555
|
function registerCommands(config) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
556
|
+
config.command = config.command ?? {};
|
|
557
|
+
config.command["memoir:status"] = {
|
|
558
|
+
description: "Show Memoir status for the current OpenCode project",
|
|
559
|
+
template: "Show Memoir status for this OpenCode project."
|
|
560
|
+
};
|
|
561
|
+
config.command["memoir:ui"] = {
|
|
562
|
+
description: "Launch or reopen the Memoir web UI for this project store",
|
|
563
|
+
template: "Launch or reopen the Memoir UI for this project."
|
|
564
|
+
};
|
|
565
|
+
config.command["memoir:remember"] = {
|
|
566
|
+
description: "Save a durable fact, preference, rule, or decision to Memoir",
|
|
567
|
+
template: `Use Memoir to save this durable memory now.
|
|
568
|
+
|
|
569
|
+
USER REQUEST:
|
|
570
|
+
$ARGUMENTS
|
|
571
|
+
|
|
572
|
+
Extract the memory content, choose a semantic path if none is supplied, then call the memoir_remember tool. Never save secrets.`
|
|
573
|
+
};
|
|
574
|
+
config.command["memoir:recall"] = {
|
|
575
|
+
description: "Recall relevant facts from Memoir before answering",
|
|
576
|
+
template: `Recall relevant Memoir memories for this request.
|
|
577
|
+
|
|
578
|
+
USER REQUEST:
|
|
579
|
+
$ARGUMENTS
|
|
580
|
+
|
|
581
|
+
Use memoir_recall first. It checks default plus onboard namespaces unless a namespace is specified. Then call memoir_get with the matching namespace for exact values before answering.`
|
|
582
|
+
};
|
|
583
|
+
config.command["memoir:onboard"] = {
|
|
584
|
+
description: "Populate or refresh Memoir onboarding for this project",
|
|
585
|
+
template: `Populate or refresh Memoir onboarding for the CURRENT OpenCode project only.
|
|
586
|
+
|
|
587
|
+
USER REQUEST:
|
|
588
|
+
$ARGUMENTS
|
|
589
|
+
|
|
590
|
+
Workflow:
|
|
591
|
+
- Stay inside the current project/worktree. Do not inspect parent directories.
|
|
592
|
+
- First obtain a project file tree to understand structure.
|
|
593
|
+
- Start studying from project documentation.
|
|
594
|
+
- Continue only based on what the tree and documentation show.
|
|
595
|
+
|
|
596
|
+
Memory rules:
|
|
597
|
+
- Record only verified facts from files/docs/code or explicit user statements.
|
|
598
|
+
- Do not write inferred user thoughts, intentions, preferences, or opinions.
|
|
599
|
+
- Do not use preferences.* paths unless the user explicitly stated a preference.
|
|
600
|
+
- If a fact is your interpretation, do not save it; report it as uncertain instead.
|
|
601
|
+
|
|
602
|
+
Then call memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git. Do not install or invoke separate skills/scripts.`
|
|
603
|
+
};
|
|
604
|
+
config.command["memoir:unmerged"] = {
|
|
605
|
+
description: "List memoir branches with changes not yet merged into main",
|
|
606
|
+
template: "List memoir branches that have diverged from main."
|
|
607
|
+
};
|
|
170
608
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
609
|
+
var memoirStatus = tool({
|
|
610
|
+
description: "Show Memoir status for the current OpenCode project store.",
|
|
611
|
+
args: {},
|
|
612
|
+
execute: async () => statusJson(deriveStorePath())
|
|
175
613
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
614
|
+
var memoirRemember = tool({
|
|
615
|
+
description: "Explicitly save a durable memory to Memoir at one or more semantic taxonomy paths.",
|
|
616
|
+
args: {
|
|
617
|
+
content: tool.schema.string().describe("Memory content to save. Do not include secrets."),
|
|
618
|
+
path: tool.schema.string().optional().describe("Semantic taxonomy path, e.g. preferences.coding.style."),
|
|
619
|
+
namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default."),
|
|
620
|
+
replace: tool.schema.boolean().optional().describe("Replace existing value at the path.")
|
|
621
|
+
},
|
|
622
|
+
execute: async (args) => {
|
|
623
|
+
const content = args.content?.trim();
|
|
624
|
+
if (!content) return "Memoir memory was not saved: content is empty.";
|
|
625
|
+
if (isSecretSanitizationEnabled() && SECRET_PATTERN.test(content)) {
|
|
626
|
+
return "Memoir memory was not saved: the content looks like a secret or credential. Save a redacted rule instead.";
|
|
627
|
+
}
|
|
628
|
+
const paths = coercePaths(args.path);
|
|
629
|
+
if (paths.length === 0) {
|
|
630
|
+
return "Memoir memory was not saved: provide a semantic path, e.g. preferences.coding.style.";
|
|
631
|
+
}
|
|
632
|
+
const store = deriveStorePath();
|
|
633
|
+
try {
|
|
634
|
+
await ensureStore(store);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
return String(error.message || error);
|
|
637
|
+
}
|
|
638
|
+
const cliArgs = ["--json", "-s", store, "remember", content];
|
|
639
|
+
for (const p of paths) cliArgs.push("-p", p);
|
|
640
|
+
cliArgs.push("-n", args.namespace ?? "default");
|
|
641
|
+
if (args.replace) cliArgs.push("--replace");
|
|
642
|
+
return tryPrettyJson(await runMemoir(cliArgs, { cwd: store }));
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
var memoirRecall = tool({
|
|
646
|
+
description: "List Memoir memory keys across relevant namespaces for relevance picking. Never calls legacy memoir recall.",
|
|
647
|
+
args: {
|
|
648
|
+
query: tool.schema.string().optional().describe("User query or topic to recall for."),
|
|
649
|
+
namespace: tool.schema.string().optional().describe("Single Memoir namespace to inspect. If omitted, checks default + onboard namespaces."),
|
|
650
|
+
namespaces: tool.schema.array(tool.schema.string()).optional().describe("Namespaces to inspect. Defaults to default, project:onboard, codebase:onboard."),
|
|
651
|
+
includeMetrics: tool.schema.boolean().optional().describe("Include metrics.* memories in the listing.")
|
|
652
|
+
},
|
|
653
|
+
execute: async (args) => {
|
|
654
|
+
const store = deriveStorePath();
|
|
655
|
+
try {
|
|
656
|
+
await ensureStore(store);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
return String(error.message || error);
|
|
659
|
+
}
|
|
660
|
+
const namespaces = args.namespace ? [args.namespace] : args.namespaces && args.namespaces.length > 0 ? args.namespaces : DEFAULT_RECALL_NAMESPACES;
|
|
661
|
+
const sections = [];
|
|
662
|
+
for (const namespace of namespaces) {
|
|
663
|
+
const output = tryPrettyJson(await runMemoir(["--json", "-s", store, "summarize", "--depth", "3", "-n", namespace], { cwd: store }));
|
|
664
|
+
sections.push(`## namespace: ${namespace}
|
|
665
|
+
${output}`);
|
|
666
|
+
}
|
|
667
|
+
const note = args.includeMetrics ? "Metrics were included by request." : "Ignore metrics.* and taxonomy:v1:* entries unless explicitly needed. If default is empty or only metrics, inspect project:onboard/codebase:onboard before concluding there is no memory.";
|
|
668
|
+
const query = args.query ? `Query: ${args.query}
|
|
669
|
+
` : "";
|
|
670
|
+
return `${query}${note}
|
|
671
|
+
Pick at most 5-7 relevant exact keys across namespaces, then call memoir_get with the matching namespace if values are needed.
|
|
672
|
+
${sections.join("\n\n")}`;
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
var MEMOIR_GET_MAX_KEYS = 20;
|
|
676
|
+
var memoirGet = tool({
|
|
677
|
+
description: "Fetch exact Memoir memory keys after selecting them from memoir_recall output.",
|
|
678
|
+
args: {
|
|
679
|
+
keys: tool.schema.array(tool.schema.string()).describe("Exact memory keys to fetch."),
|
|
680
|
+
namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default.")
|
|
681
|
+
},
|
|
682
|
+
execute: async (args) => {
|
|
683
|
+
const keys = args.keys?.map((key) => key.trim()).filter(Boolean) ?? [];
|
|
684
|
+
if (keys.length === 0) return "No Memoir keys were provided.";
|
|
685
|
+
if (keys.length > MEMOIR_GET_MAX_KEYS) {
|
|
686
|
+
return `Error: Too many keys requested (max ${MEMOIR_GET_MAX_KEYS}, got ${keys.length}). Narrow your selection from memoir_recall output.`;
|
|
687
|
+
}
|
|
688
|
+
const store = deriveStorePath();
|
|
689
|
+
try {
|
|
690
|
+
await ensureStore(store);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
return String(error.message || error);
|
|
693
|
+
}
|
|
694
|
+
return tryPrettyJson(await runMemoir(["--json", "-s", store, "get", ...keys, "-n", args.namespace ?? "default"], { cwd: store }));
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
var MemoirOpenCode = async (_input, rawOptions) => {
|
|
698
|
+
const opts = rawOptions ?? {};
|
|
699
|
+
if (opts.store) setPluginStoreOverride(opts.store);
|
|
700
|
+
const storeRoot = deriveStorePath();
|
|
701
|
+
return {
|
|
702
|
+
name: "memoir",
|
|
703
|
+
tool: {
|
|
704
|
+
memoir_status: memoirStatus,
|
|
705
|
+
memoir_remember: memoirRemember,
|
|
706
|
+
memoir_recall: memoirRecall,
|
|
707
|
+
memoir_get: memoirGet
|
|
183
708
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const paths = coercePaths(args.path);
|
|
192
|
-
if (paths.length === 0) {
|
|
193
|
-
return 'Memoir memory was not saved: provide a semantic path, e.g. preferences.coding.style.';
|
|
709
|
+
config: async (opencodeConfig) => {
|
|
710
|
+
registerCommands(opencodeConfig);
|
|
711
|
+
},
|
|
712
|
+
"command.execute.before": async (input, output) => {
|
|
713
|
+
try {
|
|
714
|
+
if (input.command === "memoir:status") {
|
|
715
|
+
pushText(output, await statusJson(deriveStorePath()));
|
|
194
716
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await ensureStore(store);
|
|
717
|
+
if (input.command === "memoir:ui") {
|
|
718
|
+
pushText(output, await launchUi(deriveStorePath()));
|
|
198
719
|
}
|
|
199
|
-
|
|
200
|
-
|
|
720
|
+
if (input.command === "memoir:unmerged") {
|
|
721
|
+
const store = deriveStorePath();
|
|
722
|
+
await ensureStore(store);
|
|
723
|
+
const raw = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
|
|
724
|
+
const data = JSON.parse(raw);
|
|
725
|
+
const branches = data?.branches ?? [];
|
|
726
|
+
const unmerged = [];
|
|
727
|
+
for (const branch of branches) {
|
|
728
|
+
if (branch === "main") continue;
|
|
729
|
+
const diffOut = await runMemoir(["-s", store, "diff", branch, "main", "--stat"], { cwd: store }).catch(() => "");
|
|
730
|
+
if (diffOut.trim()) {
|
|
731
|
+
unmerged.push(branch);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
pushText(
|
|
735
|
+
output,
|
|
736
|
+
unmerged.length > 0 ? `Unmerged branches:
|
|
737
|
+
${unmerged.join("\n")}` : "All branches are merged into main."
|
|
738
|
+
);
|
|
201
739
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
cliArgs.push('-n', args.namespace ?? 'default');
|
|
206
|
-
if (args.replace)
|
|
207
|
-
cliArgs.push('--replace');
|
|
208
|
-
return tryPrettyJson(await runMemoir(cliArgs, { cwd: store }));
|
|
740
|
+
} catch (error) {
|
|
741
|
+
pushText(output, `Memoir command failed: ${String(error.message || error)}`);
|
|
742
|
+
}
|
|
209
743
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Inject MEMOIR_STORE into every shell command's environment so any memoir
|
|
746
|
+
* invocation automatically targets the right store without manual -s flags.
|
|
747
|
+
* Uses the cached value resolved at init time (avoiding execFileSync overhead
|
|
748
|
+
* on every shell command).
|
|
749
|
+
*/
|
|
750
|
+
"shell.env": async (_input2, output) => {
|
|
751
|
+
try {
|
|
752
|
+
output.env.MEMOIR_STORE = storeRoot;
|
|
753
|
+
} catch (e) {
|
|
754
|
+
debugLog("shell.env: failed:", e instanceof Error ? e.message : String(e));
|
|
755
|
+
}
|
|
218
756
|
},
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
757
|
+
/**
|
|
758
|
+
* Observe every tool execution for metrics and code-change tracking.
|
|
759
|
+
* Mirrors the observation phase of Claude Code's Stop hook.
|
|
760
|
+
* Never modifies the tool output.
|
|
761
|
+
*/
|
|
762
|
+
"tool.execute.after": async (input, output) => {
|
|
763
|
+
try {
|
|
764
|
+
const sid = input.sessionID ?? "default";
|
|
765
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_METRICS !== "1") {
|
|
766
|
+
const calls = 1;
|
|
767
|
+
const errors = output.metadata?.error || output.output?.startsWith("Error:") ? 1 : 0;
|
|
768
|
+
recordToolMetrics(sid, input.tool, { calls, errors });
|
|
769
|
+
}
|
|
770
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_CODE_SUMMARY !== "1" && EDIT_TOOLS.has(input.tool)) {
|
|
771
|
+
const filePath = typeof input.args?.filePath === "string" ? input.args.filePath : typeof input.args?.path === "string" ? input.args.path : "";
|
|
772
|
+
if (filePath) {
|
|
773
|
+
recordEdit(sid, { tool: input.tool, filePath, snippet: "", timestamp: Date.now() });
|
|
774
|
+
}
|
|
223
775
|
}
|
|
224
|
-
|
|
225
|
-
|
|
776
|
+
} catch (e) {
|
|
777
|
+
debugLog("tool.execute.after: failed:", e instanceof Error ? e.message : String(e));
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
/**
|
|
781
|
+
* Fires on every incoming user message.
|
|
782
|
+
*
|
|
783
|
+
* 1. Auto-match memoir branch to current git branch
|
|
784
|
+
* (cf. UserPromptSubmit's auto_match_memoir_branch in Claude Code plugin).
|
|
785
|
+
* 2. Flush pending edits from the previous assistant turn
|
|
786
|
+
* (cf. Stop hook code change audit, run after each turn).
|
|
787
|
+
* 3. Run the recall gate (cf. UserPromptSubmit).
|
|
788
|
+
*
|
|
789
|
+
* Steps 1–2 are skipped when MEMOIR_NO_CAPTURE=1.
|
|
790
|
+
*/
|
|
791
|
+
"chat.message": async (input, output) => {
|
|
792
|
+
try {
|
|
793
|
+
const sid = input.sessionID ?? "default";
|
|
794
|
+
const text = output.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
795
|
+
if (shouldTriggerRecall(text)) {
|
|
796
|
+
pendingRecall.add(sid);
|
|
226
797
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const namespace of namespaces) {
|
|
232
|
-
const output = tryPrettyJson(await runMemoir(['--json', '-s', store, 'summarize', '--depth', '3', '-n', namespace], { cwd: store }));
|
|
233
|
-
sections.push(`## namespace: ${namespace}\n${output}`);
|
|
798
|
+
const prevBranch = getCachedBranch(sid) === "unknown" ? void 0 : getCachedBranch(sid);
|
|
799
|
+
setCachedBranch(sid, await autoMatchMemoirBranch(storeRoot));
|
|
800
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1") {
|
|
801
|
+
await flushCapture(storeRoot, prevBranch, sid);
|
|
234
802
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const query = args.query ? `Query: ${args.query}\n` : '';
|
|
239
|
-
return `${query}${note}\nPick at most 5-7 relevant exact keys across namespaces, then call memoir_get with the matching namespace if values are needed.\n${sections.join('\n\n')}`;
|
|
803
|
+
} catch (e) {
|
|
804
|
+
debugLog("chat.message: failed:", e instanceof Error ? e.message : String(e));
|
|
805
|
+
}
|
|
240
806
|
},
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Fires on SDK events. On session.created, kick off a background fetch of
|
|
809
|
+
* the taxonomy overview so it's ready before the first LLM call.
|
|
810
|
+
* Mirrors Claude Code's SessionStart context injection.
|
|
811
|
+
*/
|
|
812
|
+
event: async ({ event: evt }) => {
|
|
813
|
+
if (evt.type === "session.created" && !initContextFetched) {
|
|
814
|
+
initContextFetched = true;
|
|
815
|
+
(async () => {
|
|
816
|
+
try {
|
|
817
|
+
const taxonomy = await runMemoir(
|
|
818
|
+
["--json", "-s", storeRoot, "summarize", "--depth", "3", "-n", "default"],
|
|
819
|
+
{ cwd: storeRoot }
|
|
820
|
+
);
|
|
821
|
+
if (taxonomy.startsWith("Memoir command failed")) {
|
|
822
|
+
throw new Error(taxonomy);
|
|
823
|
+
}
|
|
824
|
+
const pretty = tryPrettyJson(taxonomy);
|
|
825
|
+
initContext = `[memoir] Available taxonomy paths:
|
|
826
|
+
${pretty}`;
|
|
827
|
+
} catch (e) {
|
|
828
|
+
debugLog("event: taxonomy fetch failed, will retry on next session:", e instanceof Error ? e.message : String(e));
|
|
829
|
+
initContextFetched = false;
|
|
830
|
+
}
|
|
831
|
+
})();
|
|
832
|
+
}
|
|
249
833
|
},
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
834
|
+
/**
|
|
835
|
+
* Fires before every LLM call.
|
|
836
|
+
*
|
|
837
|
+
* 1. Injects the memoir taxonomy context for this session (once per session).
|
|
838
|
+
* 2. If a recall is pending for this session, injects a brief instruction
|
|
839
|
+
* telling the model to use memoir tools (one-shot per trigger).
|
|
840
|
+
*/
|
|
841
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
842
|
+
try {
|
|
843
|
+
if (input.sessionID && !hasSessionContext(input.sessionID)) {
|
|
844
|
+
markSessionContext(input.sessionID);
|
|
845
|
+
if (initContext) {
|
|
846
|
+
output.system.unshift(initContext);
|
|
847
|
+
}
|
|
256
848
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
849
|
+
if (input.sessionID && pendingRecall.has(input.sessionID)) {
|
|
850
|
+
pendingRecall.delete(input.sessionID);
|
|
851
|
+
output.system.push(
|
|
852
|
+
"\n[memoir] The user may have relevant context in Memoir. Run memoir_recall to list available memories across default and onboard namespaces, then memoir_get with the matching namespace to fetch exact values."
|
|
853
|
+
);
|
|
260
854
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return tryPrettyJson(await runMemoir(['--json', '-s', store, 'get', ...keys, '-n', args.namespace ?? 'default'], { cwd: store }));
|
|
855
|
+
} catch (e) {
|
|
856
|
+
debugLog("system.transform: failed:", e instanceof Error ? e.message : String(e));
|
|
857
|
+
}
|
|
265
858
|
},
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
registerCommands(opencodeConfig);
|
|
284
|
-
},
|
|
285
|
-
'command.execute.before': async (input, output) => {
|
|
286
|
-
try {
|
|
287
|
-
if (input.command === 'memoir:status') {
|
|
288
|
-
pushText(output, await statusJson(deriveStorePath()));
|
|
289
|
-
}
|
|
290
|
-
if (input.command === 'memoir:ui') {
|
|
291
|
-
pushText(output, await launchUi(deriveStorePath()));
|
|
292
|
-
}
|
|
293
|
-
if (input.command === 'memoir:unmerged') {
|
|
294
|
-
const store = deriveStorePath();
|
|
295
|
-
await ensureStore(store);
|
|
296
|
-
const raw = await runMemoir(['--json', '-s', store, 'branch'], { cwd: store });
|
|
297
|
-
const data = JSON.parse(raw);
|
|
298
|
-
const branches = data?.branches ?? [];
|
|
299
|
-
const unmerged = [];
|
|
300
|
-
for (const branch of branches) {
|
|
301
|
-
if (branch === 'main')
|
|
302
|
-
continue;
|
|
303
|
-
const diffOut = await runMemoir(['-s', store, 'diff', branch, 'main', '--stat'], { cwd: store }).catch(() => '');
|
|
304
|
-
if (diffOut.trim()) {
|
|
305
|
-
unmerged.push(branch);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
pushText(output, unmerged.length > 0
|
|
309
|
-
? `Unmerged branches:\n${unmerged.join('\n')}`
|
|
310
|
-
: 'All branches are merged into main.');
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
catch (error) {
|
|
314
|
-
pushText(output, `Memoir command failed: ${String(error.message || error)}`);
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
/**
|
|
318
|
-
* Inject MEMOIR_STORE into every shell command's environment so any memoir
|
|
319
|
-
* invocation automatically targets the right store without manual -s flags.
|
|
320
|
-
* Uses the cached value resolved at init time (avoiding execFileSync overhead
|
|
321
|
-
* on every shell command).
|
|
322
|
-
*/
|
|
323
|
-
'shell.env': async (_input, output) => {
|
|
324
|
-
try {
|
|
325
|
-
output.env.MEMOIR_STORE = storeRoot;
|
|
326
|
-
}
|
|
327
|
-
catch (e) {
|
|
328
|
-
debugLog('shell.env: failed:', e instanceof Error ? e.message : String(e));
|
|
329
|
-
}
|
|
330
|
-
},
|
|
331
|
-
/**
|
|
332
|
-
* Observe every tool execution for metrics and code-change tracking.
|
|
333
|
-
* Mirrors the observation phase of Claude Code's Stop hook.
|
|
334
|
-
* Never modifies the tool output.
|
|
335
|
-
*/
|
|
336
|
-
'tool.execute.after': async (input, output) => {
|
|
337
|
-
try {
|
|
338
|
-
// Per-session state key
|
|
339
|
-
const sid = input.sessionID ?? 'default';
|
|
340
|
-
// Accumulate per-tool metrics (cf. collect-metrics.sh).
|
|
341
|
-
// Skipped when MEMOIR_NO_CAPTURE or MEMOIR_NO_METRICS is set.
|
|
342
|
-
if (process.env.MEMOIR_NO_CAPTURE !== '1' && process.env.MEMOIR_NO_METRICS !== '1') {
|
|
343
|
-
const calls = 1;
|
|
344
|
-
const errors = (output.metadata?.error || output.output?.startsWith('Error:')) ? 1 : 0;
|
|
345
|
-
recordToolMetrics(sid, input.tool, { calls, errors });
|
|
346
|
-
}
|
|
347
|
-
// Track file edits (cf. collect-edits.sh).
|
|
348
|
-
// Skipped when MEMOIR_NO_CAPTURE or MEMOIR_NO_CODE_SUMMARY is set.
|
|
349
|
-
if (process.env.MEMOIR_NO_CAPTURE !== '1' && process.env.MEMOIR_NO_CODE_SUMMARY !== '1' && EDIT_TOOLS.has(input.tool)) {
|
|
350
|
-
const filePath = typeof input.args?.filePath === 'string' ? input.args.filePath
|
|
351
|
-
: typeof input.args?.path === 'string' ? input.args.path
|
|
352
|
-
: '';
|
|
353
|
-
if (filePath) {
|
|
354
|
-
recordEdit(sid, { tool: input.tool, filePath, snippet: '', timestamp: Date.now() });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
catch (e) {
|
|
359
|
-
debugLog('tool.execute.after: failed:', e instanceof Error ? e.message : String(e));
|
|
360
|
-
}
|
|
361
|
-
},
|
|
362
|
-
/**
|
|
363
|
-
* Fires on every incoming user message.
|
|
364
|
-
*
|
|
365
|
-
* 1. Auto-match memoir branch to current git branch
|
|
366
|
-
* (cf. UserPromptSubmit's auto_match_memoir_branch in Claude Code plugin).
|
|
367
|
-
* 2. Flush pending edits from the previous assistant turn
|
|
368
|
-
* (cf. Stop hook code change audit, run after each turn).
|
|
369
|
-
* 3. Run the recall gate (cf. UserPromptSubmit).
|
|
370
|
-
*
|
|
371
|
-
* Steps 1–2 are skipped when MEMOIR_NO_CAPTURE=1.
|
|
372
|
-
*/
|
|
373
|
-
'chat.message': async (input, output) => {
|
|
374
|
-
try {
|
|
375
|
-
const sid = input.sessionID ?? 'default';
|
|
376
|
-
// C4: run recall gate BEFORE any await — otherwise system.transform
|
|
377
|
-
// could fire before pendingRecall is set.
|
|
378
|
-
const text = output.parts
|
|
379
|
-
.filter((p) => p.type === 'text')
|
|
380
|
-
.map(p => p.text)
|
|
381
|
-
.join(' ');
|
|
382
|
-
if (shouldTriggerRecall(text)) {
|
|
383
|
-
pendingRecall.add(sid);
|
|
384
|
-
}
|
|
385
|
-
// Snapshot previous branch BEFORE switching — edits from the last turn
|
|
386
|
-
// belong to the OLD branch. (C2 fix: prevents misattribution when the
|
|
387
|
-
// user switches git branch between turns.)
|
|
388
|
-
const prevBranch = getCachedBranch(sid) === 'unknown' ? undefined : getCachedBranch(sid);
|
|
389
|
-
setCachedBranch(sid, await autoMatchMemoirBranch(storeRoot));
|
|
390
|
-
// Skip capture flush when MEMOIR_NO_CAPTURE is set.
|
|
391
|
-
// Flush under the PREVIOUS branch (the one edits were made on).
|
|
392
|
-
if (process.env.MEMOIR_NO_CAPTURE !== '1') {
|
|
393
|
-
await flushCapture(storeRoot, prevBranch, sid);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
catch (e) {
|
|
397
|
-
debugLog('chat.message: failed:', e instanceof Error ? e.message : String(e));
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
/**
|
|
401
|
-
* Fires on SDK events. On session.created, kick off a background fetch of
|
|
402
|
-
* the taxonomy overview so it's ready before the first LLM call.
|
|
403
|
-
* Mirrors Claude Code's SessionStart context injection.
|
|
404
|
-
*/
|
|
405
|
-
event: async ({ event: evt }) => {
|
|
406
|
-
if (evt.type === 'session.created' && !initContextFetched) {
|
|
407
|
-
initContextFetched = true;
|
|
408
|
-
(async () => {
|
|
409
|
-
try {
|
|
410
|
-
const taxonomy = await runMemoir(['--json', '-s', storeRoot, 'summarize', '--depth', '3', '-n', 'default'], { cwd: storeRoot });
|
|
411
|
-
if (taxonomy.startsWith('Memoir command failed')) {
|
|
412
|
-
throw new Error(taxonomy);
|
|
413
|
-
}
|
|
414
|
-
const pretty = tryPrettyJson(taxonomy);
|
|
415
|
-
initContext = `[memoir] Available taxonomy paths:\n${pretty}`;
|
|
416
|
-
}
|
|
417
|
-
catch (e) {
|
|
418
|
-
debugLog('event: taxonomy fetch failed, will retry on next session:', e instanceof Error ? e.message : String(e));
|
|
419
|
-
// Allow a later session to retry — the store may not have been ready yet.
|
|
420
|
-
initContextFetched = false;
|
|
421
|
-
}
|
|
422
|
-
})();
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
/**
|
|
426
|
-
* Fires before every LLM call.
|
|
427
|
-
*
|
|
428
|
-
* 1. Injects the memoir taxonomy context for this session (once per session).
|
|
429
|
-
* 2. If a recall is pending for this session, injects a brief instruction
|
|
430
|
-
* telling the model to use memoir tools (one-shot per trigger).
|
|
431
|
-
*/
|
|
432
|
-
'experimental.chat.system.transform': async (input, output) => {
|
|
433
|
-
try {
|
|
434
|
-
// Inject initial context on the first LLM call of each session
|
|
435
|
-
if (input.sessionID && !hasSessionContext(input.sessionID)) {
|
|
436
|
-
markSessionContext(input.sessionID);
|
|
437
|
-
if (initContext) {
|
|
438
|
-
output.system.unshift(initContext);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// Inject recall instruction (one-shot per trigger)
|
|
442
|
-
if (input.sessionID && pendingRecall.has(input.sessionID)) {
|
|
443
|
-
pendingRecall.delete(input.sessionID);
|
|
444
|
-
output.system.push('\n[memoir] The user may have relevant context in Memoir. Run memoir_recall to list available memories across default and onboard namespaces, then memoir_get with the matching namespace to fetch exact values.');
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch (e) {
|
|
448
|
-
debugLog('system.transform: failed:', e instanceof Error ? e.message : String(e));
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
/**
|
|
452
|
-
* Fires when the plugin is shut down. Flushes any remaining code changes
|
|
453
|
-
* and metrics (cf. SessionEnd heartbeat cleanup + final metrics flush).
|
|
454
|
-
*/
|
|
455
|
-
dispose: async () => {
|
|
456
|
-
// No sessionID → flushCapture flushes ALL sessions
|
|
457
|
-
await flushCapture(storeRoot);
|
|
458
|
-
pruneStaleSessions();
|
|
459
|
-
sessionsWithContext.clear();
|
|
460
|
-
},
|
|
461
|
-
});
|
|
859
|
+
/**
|
|
860
|
+
* Fires when the plugin is shut down. Flushes any remaining code changes
|
|
861
|
+
* and metrics (cf. SessionEnd heartbeat cleanup + final metrics flush).
|
|
862
|
+
*/
|
|
863
|
+
dispose: async () => {
|
|
864
|
+
await flushCapture(storeRoot);
|
|
865
|
+
pruneStaleSessions();
|
|
866
|
+
sessionsWithContext.clear();
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
};
|
|
870
|
+
var index_default = MemoirOpenCode;
|
|
871
|
+
export {
|
|
872
|
+
MEMOIR_GET_MAX_KEYS,
|
|
873
|
+
coercePaths,
|
|
874
|
+
index_default as default,
|
|
875
|
+
tryPrettyJson
|
|
462
876
|
};
|
|
463
|
-
|
|
464
|
-
//# sourceMappingURL=index.js.map
|
|
877
|
+
//# sourceMappingURL=index.js.map
|