opencode-memoir 1.1.8 → 2.0.0

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/dist/index.js CHANGED
@@ -6,862 +6,132 @@ function debugLog(...args) {
6
6
  `);
7
7
  }
8
8
 
9
- // src/store.ts
10
- import { execFile, execFileSync } from "node:child_process";
11
- import { access, mkdir, rm } from "node:fs/promises";
12
- import { homedir, tmpdir } from "node:os";
13
- import { join, resolve } from "node:path";
14
- import { promisify } from "node:util";
15
-
16
- // src/utils.ts
17
- function coercePaths(path) {
18
- if (!path) return [];
19
- return Array.isArray(path) ? path.filter(Boolean) : [path];
9
+ // src/memory-saver.ts
10
+ var sessionMsgCount = /* @__PURE__ */ new Map();
11
+ var REMINDER_INTERVAL = Math.max(0, parseInt(process.env.MEMOIR_REMINDER_INTERVAL ?? "5", 10));
12
+ function incrementMsgCount(sessionID) {
13
+ const count = (sessionMsgCount.get(sessionID) ?? 0) + 1;
14
+ sessionMsgCount.set(sessionID, count);
15
+ return count;
20
16
  }
21
- function tryPrettyJson(text) {
22
- try {
23
- return JSON.stringify(JSON.parse(text), null, 2);
24
- } catch {
25
- return text;
26
- }
17
+ function shouldRemind(count) {
18
+ return REMINDER_INTERVAL > 0 && count > 1 && count % REMINDER_INTERVAL === 0;
27
19
  }
28
- var MEMOIR_GET_MAX_KEYS = 20;
29
- var SECRET_PATTERN = new RegExp(
30
- [
31
- // Generic secret assignments
32
- `(?:password|passwd|secret|api[_-]?key|token|auth[_-]?token|access[_-]?key|private[_-]?key)\\s*[=:]\\s*['"]?[^\\s'"]{8,}`,
33
- // AWS secret keys
34
- `(?:aws[_-]?secret[_-]?access[_-]?key|aws_secret)\\s*[=:]\\s*['"]?[A-Za-z0-9/+=]{40}`,
35
- // Connection strings with embedded credentials
36
- "(?:postgres|mysql|mongodb|redis|amqp)://[^\\s:]+:[^\\s@]+@[^\\s]+",
37
- // Generic connection strings with passwords
38
- "://[^\\s:]+:[^\\s@]+@",
39
- // PEM private keys
40
- "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----",
41
- // Generic high-entropy secret assignments
42
- `(?:secret|credential|key|password)\\s*[=:]\\s*['"][A-Za-z0-9+/=_-]{20,}['"]`
43
- ].join("|")
44
- );
45
- function errorMessage(e) {
46
- return e instanceof Error ? e.message : String(e);
20
+ function pruneAll() {
21
+ sessionMsgCount.clear();
47
22
  }
48
23
 
49
24
  // src/store.ts
25
+ import { execFile, execFileSync } from "node:child_process";
26
+ import { homedir } from "node:os";
27
+ import { join, resolve } from "node:path";
28
+ import { promisify } from "node:util";
50
29
  var execFileAsync = promisify(execFile);
51
- var MEMOIR_PACKAGE = "memoir-ai";
52
- var MEMOIR_AI_PIN = "0.2.2";
53
- var GIT_TIMEOUT_MS = 3e3;
54
- var MEMOIR_TIMEOUT_MS = 15e3;
55
- var MEMOIR_MAX_BUFFER = 1024 * 1024;
56
- var NO_GIT_CACHE_MAX = 500;
57
- var NO_GIT_CACHE_TTL = 5 * 60 * 1e3;
58
- function evictOldestNoGit() {
59
- const firstKey = _noGitCache.keys().next().value;
60
- if (firstKey !== void 0) _noGitCache.delete(firstKey);
61
- }
62
- function sweepNoGitCache() {
63
- const now = Date.now();
64
- for (const [key, ts] of _noGitCache) {
65
- if (now - ts >= NO_GIT_CACHE_TTL) _noGitCache.delete(key);
66
- }
67
- }
68
- var _noGitCache = /* @__PURE__ */ new Map();
69
- function _checkNoGit(cwd) {
70
- const ts = _noGitCache.get(cwd);
71
- if (ts === void 0) return false;
72
- const age = Date.now() - ts;
73
- if (age < NO_GIT_CACHE_TTL) return true;
74
- _noGitCache.delete(cwd);
75
- return false;
76
- }
77
- function _main_worktree_root(cwd) {
78
- if (_checkNoGit(cwd)) return "";
79
- try {
80
- const gitDir = execFileSync("git", ["rev-parse", "--git-dir"], {
81
- cwd,
82
- encoding: "utf8",
83
- timeout: GIT_TIMEOUT_MS
84
- }).trim();
85
- const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
86
- cwd,
87
- encoding: "utf8",
88
- timeout: GIT_TIMEOUT_MS
89
- }).trim();
90
- const resolvePath = (path) => {
91
- if (path.startsWith("/")) return path;
92
- return join(cwd, path);
93
- };
94
- const gitDirAbs = resolvePath(gitDir);
95
- const gitCommonDirAbs = resolvePath(gitCommonDir);
96
- if (gitDirAbs === gitCommonDirAbs) {
97
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
98
- cwd,
99
- encoding: "utf8",
100
- timeout: GIT_TIMEOUT_MS
101
- }).trim();
102
- }
103
- const worktreeList = execFileSync("git", ["worktree", "list", "--porcelain"], {
104
- cwd,
105
- encoding: "utf8",
106
- timeout: GIT_TIMEOUT_MS
107
- });
108
- const firstLine = worktreeList.split("\n")[0];
109
- if (firstLine.startsWith("worktree ")) {
110
- const mainWorktree = firstLine.substring("worktree ".length).trim();
111
- if (mainWorktree && mainWorktree !== "(bare)") {
112
- return mainWorktree;
113
- }
114
- }
115
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
116
- cwd,
117
- encoding: "utf8",
118
- timeout: GIT_TIMEOUT_MS
119
- }).trim();
120
- } catch (e) {
121
- debugLog("_main_worktree_root: not a git repo or git error:", errorMessage(e));
122
- sweepNoGitCache();
123
- _noGitCache.set(cwd, Date.now());
124
- if (_noGitCache.size > NO_GIT_CACHE_MAX) evictOldestNoGit();
125
- return "";
126
- }
30
+ function errorMessage(e) {
31
+ return e instanceof Error ? e.message : String(e);
127
32
  }
128
- var _storePathCache = null;
129
33
  function deriveStorePath(cwd = process.cwd()) {
130
34
  if (pluginStoreOverride) return pluginStoreOverride;
131
- const configured = process.env.MEMOIR_STORE;
132
- if (configured) return configured;
133
- if (_storePathCache && _storePathCache.cwd === cwd) return _storePathCache.path;
35
+ const envStore = process.env.MEMOIR_STORE;
36
+ if (envStore) return envStore;
134
37
  let projectDir;
135
38
  try {
136
- const gitRoot = _main_worktree_root(cwd);
137
- if (gitRoot) {
138
- projectDir = gitRoot;
139
- } else {
140
- projectDir = resolve(cwd);
141
- }
142
- } catch (e) {
143
- debugLog("deriveStorePath: unexpected error:", errorMessage(e));
144
- projectDir = cwd;
39
+ const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
40
+ cwd,
41
+ encoding: "utf8",
42
+ timeout: 3e3
43
+ }).trim();
44
+ projectDir = gitRoot;
45
+ } catch {
46
+ projectDir = resolve(cwd);
145
47
  }
146
48
  const slug = projectDir.replace(/[/.]/g, "-");
147
- const result = join(homedir(), ".memoir", slug);
148
- _storePathCache = { cwd, path: result };
149
- return result;
49
+ return join(homedir(), ".memoir", slug);
150
50
  }
151
51
  var pluginStoreOverride;
152
52
  function setPluginStoreOverride(store) {
153
53
  pluginStoreOverride = store;
154
54
  }
155
- var ENSURED_STORES_MAX = 200;
156
- var ensuredStores = /* @__PURE__ */ new Set();
157
- var storeCreations = /* @__PURE__ */ new Map();
158
- async function ensureStore(store) {
159
- if (ensuredStores.has(store)) return;
160
- const inFlight = storeCreations.get(store);
161
- if (inFlight) {
162
- await inFlight;
163
- return;
164
- }
165
- try {
166
- await access(join(store, ".git"));
167
- ensuredStores.add(store);
168
- if (ensuredStores.size > ENSURED_STORES_MAX) {
169
- const first = ensuredStores.keys().next().value;
170
- if (first !== void 0) ensuredStores.delete(first);
171
- }
172
- return;
173
- } catch {
174
- await mkdir(join(store, ".."), { recursive: true }).catch(
175
- (e) => debugLog("ensureStore: mkdir failed:", errorMessage(e))
176
- );
177
- }
178
- const creationPromise = (async () => {
179
- const tmpDir = join(tmpdir(), `memoir-scratch-${Date.now()}`);
180
- try {
181
- await mkdir(tmpDir, { recursive: true });
182
- await execFileAsync("git", ["init", "-q", tmpDir], { timeout: 5e3 });
183
- const result = await runMemoir(["new", store, "--taxonomy-builtin"], { cwd: tmpDir });
184
- if (!result.ok) {
185
- throw new Error(result.error);
186
- }
187
- ensuredStores.add(store);
188
- if (ensuredStores.size > ENSURED_STORES_MAX) {
189
- const first = ensuredStores.keys().next().value;
190
- if (first !== void 0) ensuredStores.delete(first);
191
- }
192
- } catch (e) {
193
- debugLog("ensureStore: creation failed:", errorMessage(e));
194
- throw e;
195
- } finally {
196
- rm(tmpDir, { recursive: true, force: true }).catch(
197
- (e) => debugLog("tmp cleanup failed:", errorMessage(e))
198
- );
199
- }
200
- })();
201
- storeCreations.set(store, creationPromise);
202
- try {
203
- await creationPromise;
204
- } finally {
205
- storeCreations.delete(store);
206
- }
207
- }
208
- function reorderResolver(resolver, winner) {
209
- const idx = resolver.indexOf(winner);
210
- if (idx <= 0) return [...resolver];
211
- const next = [...resolver];
212
- next.splice(idx, 1);
213
- next.unshift(winner);
214
- return next;
215
- }
216
- var memoirResolved = null;
217
- async function runMemoir(args, options = {}) {
218
- let specs = memoirSpawnSpecs(args);
219
- if (memoirResolved) {
220
- const winner = specs.find((s) => s.label === memoirResolved);
221
- if (winner) specs = reorderResolver(specs, winner);
222
- }
223
- let lastError = "";
224
- let lastResolver;
225
- for (const spec of specs) {
226
- try {
227
- const { stdout } = await execFileAsync(spec.command, spec.args, {
228
- cwd: options.cwd ?? process.cwd(),
229
- env: process.env,
230
- maxBuffer: MEMOIR_MAX_BUFFER,
231
- timeout: MEMOIR_TIMEOUT_MS
232
- // prevent hang if memoir CLI stalls
233
- });
234
- memoirResolved = spec.label;
235
- return { ok: true, stdout: stdout.trim() };
236
- } catch (e) {
237
- lastError = errorMessage(e);
238
- lastResolver = spec.label;
239
- debugLog("runMemoir: fallback", spec.label, "failed:", lastError);
240
- if (memoirResolved === spec.label) memoirResolved = null;
241
- }
242
- }
243
- return { ok: false, error: `Memoir command failed: ${lastError}`, resolver: lastResolver };
244
- }
245
- function memoirSpawnSpecs(args) {
246
- return [
247
- { command: "memoir", args, label: "memoir" },
248
- {
249
- command: "uvx",
250
- args: ["--from", `${MEMOIR_PACKAGE}==${MEMOIR_AI_PIN}`, "memoir", ...args],
251
- label: "uvx"
252
- },
253
- {
254
- command: "uv",
255
- args: ["tool", "run", "--from", `${MEMOIR_PACKAGE}==${MEMOIR_AI_PIN}`, "memoir", ...args],
256
- label: "uv tool run"
257
- }
258
- ];
259
- }
260
- async function getCurrentBranch(store) {
55
+ function currentGitBranch(cwd = process.cwd()) {
261
56
  try {
262
- const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
263
- const result = await runFn(["--json", "-s", store, "status"], { cwd: store });
264
- if (!result.ok) return "unknown";
265
- const data = JSON.parse(result.stdout);
266
- return data.branch || "unknown";
267
- } catch (e) {
268
- debugLog("getCurrentBranch: failed:", errorMessage(e));
269
- return "unknown";
270
- }
271
- }
272
- async function codeGitBranch() {
273
- try {
274
- const execFn = _storeTestOverrides.execFileAsyncFn ?? execFileAsync;
275
- const { stdout } = await execFn("git", ["branch", "--show-current"], {
57
+ return execFileSync("git", ["branch", "--show-current"], {
58
+ cwd,
276
59
  encoding: "utf8",
277
- timeout: GIT_TIMEOUT_MS
278
- });
279
- return stdout.trim();
280
- } catch (e) {
281
- debugLog("codeGitBranch: failed:", errorMessage(e));
282
- return "";
283
- }
284
- }
285
- async function branchExistsInMemoir(store, name) {
286
- if (!name) return false;
287
- try {
288
- const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
289
- const result = await runFn(["--json", "-s", store, "branch"], { cwd: store });
290
- if (!result.ok) return false;
291
- const data = JSON.parse(result.stdout);
292
- const branches = data?.branches ?? [];
293
- return branches.includes(name);
294
- } catch (e) {
295
- debugLog("branchExistsInMemoir: failed:", errorMessage(e));
296
- return false;
297
- }
298
- }
299
- async function autoMatchMemoirBranch(store) {
300
- const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
301
- const codeBranch = await codeGitBranch();
302
- if (!codeBranch) {
303
- return getCurrentBranch(store);
304
- }
305
- const current = await getCurrentBranch(store);
306
- if (current === codeBranch) return current;
307
- if (!await branchExistsInMemoir(store, codeBranch)) {
308
- const result2 = await runFn(["-s", store, "branch", codeBranch, "--from", "main"], {
309
- cwd: store
310
- });
311
- if (!result2.ok) {
312
- debugLog("autoMatchMemoirBranch: create branch failed:", result2.error);
313
- return getCurrentBranch(store);
314
- }
315
- }
316
- const result = await runFn(["-s", store, "checkout", codeBranch], { cwd: store });
317
- if (!result.ok) {
318
- debugLog("autoMatchMemoirBranch: checkout failed:", result.error);
319
- return getCurrentBranch(store);
320
- }
321
- return codeBranch;
322
- }
323
- async function readMemoirValue(store, key, namespace = "default") {
324
- try {
325
- const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
326
- const result = await runFn(["--json", "-s", store, "get", key, "-n", namespace], {
327
- cwd: store
328
- });
329
- if (!result.ok) return "";
330
- const parsed = JSON.parse(result.stdout);
331
- const items = parsed?.items ?? [];
332
- const value = items[0]?.value?.content;
333
- return typeof value === "string" ? value : "";
334
- } catch (e) {
335
- debugLog("readMemoirValue: failed:", errorMessage(e));
60
+ timeout: 3e3
61
+ }).trim();
62
+ } catch {
336
63
  return "";
337
64
  }
338
65
  }
339
- var _storeTestOverrides = {};
340
-
341
- // src/capture.ts
342
- var _cliOverrides = null;
343
- var EDIT_TOOLS = /* @__PURE__ */ new Set([
344
- "Edit",
345
- "Write",
346
- "MultiEdit",
347
- "NotebookEdit",
348
- "apply_patch",
349
- "ApplyPatch",
350
- "MultiFileEdit"
351
- ]);
352
- function evictOldest(map, maxSize) {
353
- if (map.size <= maxSize) return;
354
- const firstKey = map.keys().next().value;
355
- if (firstKey !== void 0) map.delete(firstKey);
356
- }
357
- var METRICS_CODE_MAX = 1e3;
358
- var pendingEditsBySession = /* @__PURE__ */ new Map();
359
- var toolMetricsBySession = /* @__PURE__ */ new Map();
360
- var cachedBranchBySession = /* @__PURE__ */ new Map();
66
+ var branchCache = /* @__PURE__ */ new Map();
361
67
  function getCachedBranch(sessionID) {
362
- return cachedBranchBySession.get(sessionID) ?? "unknown";
68
+ return branchCache.get(sessionID) ?? "";
363
69
  }
364
70
  function setCachedBranch(sessionID, branch) {
365
- cachedBranchBySession.set(sessionID, branch);
366
- evictOldest(cachedBranchBySession, 200);
367
- }
368
- function recordEdit(sessionID, edit) {
369
- let edits = pendingEditsBySession.get(sessionID);
370
- if (!edits) {
371
- edits = [];
372
- pendingEditsBySession.set(sessionID, edits);
373
- evictOldest(pendingEditsBySession, 500);
374
- }
375
- edits.push(edit);
376
- }
377
- function recordToolMetrics(sessionID, tool2, metrics) {
378
- let sessionMetrics = toolMetricsBySession.get(sessionID);
379
- if (!sessionMetrics) {
380
- sessionMetrics = /* @__PURE__ */ new Map();
381
- toolMetricsBySession.set(sessionID, sessionMetrics);
382
- evictOldest(toolMetricsBySession, 500);
383
- }
384
- const prev = sessionMetrics.get(tool2) ?? { calls: 0, errors: 0 };
385
- sessionMetrics.set(tool2, {
386
- calls: prev.calls + metrics.calls,
387
- errors: prev.errors + metrics.errors
388
- });
389
- }
390
- function parseTurnMetrics(text) {
391
- const result = /* @__PURE__ */ new Map();
392
- if (!text) return result;
393
- const parts = text.split("|");
394
- for (const part of parts) {
395
- const trimmed = part.trim();
396
- if (!trimmed) continue;
397
- const [tool2, callsStr, errorsStr] = trimmed.split(":");
398
- if (!tool2) continue;
399
- result.set(tool2, {
400
- calls: parseInt(callsStr, 10) || 0,
401
- errors: parseInt(errorsStr, 10) || 0
402
- });
403
- }
404
- return result;
71
+ branchCache.set(sessionID, branch);
405
72
  }
406
- function serializeTurnMetrics(map) {
407
- return [...map.entries()].map(([tool2, m]) => `${tool2}:${m.calls}:${m.errors}`).join(" | ");
408
- }
409
- var flushQueues = /* @__PURE__ */ new Map();
410
- async function acquireFlushLock(store) {
411
- const prev = flushQueues.get(store) ?? Promise.resolve();
412
- let release;
413
- const next = new Promise((resolve2) => {
414
- release = resolve2;
415
- });
416
- flushQueues.set(store, next);
417
- await prev;
418
- return () => {
419
- release();
420
- if (flushQueues.get(store) === next) flushQueues.delete(store);
421
- };
422
- }
423
- async function flushCapture(store, branch, sessionID) {
424
- const targets = sessionID ? [sessionID] : [...pendingEditsBySession.keys()];
425
- if (targets.length === 0) return;
73
+ async function callMemoir(args, store) {
426
74
  try {
427
- store = store ?? deriveStorePath();
428
- if (!store) return;
429
- await ensureStore(store);
430
- const releaseLock = await acquireFlushLock(store);
431
- try {
432
- const perSession = [];
433
- const swappedSids = /* @__PURE__ */ new Map();
434
- for (const sid of targets) {
435
- const idx = perSession.length;
436
- const edits = pendingEditsBySession.get(sid) ?? [];
437
- pendingEditsBySession.set(sid, []);
438
- const metrics = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
439
- toolMetricsBySession.set(sid, /* @__PURE__ */ new Map());
440
- const sb = branch ?? cachedBranchBySession.get(sid);
441
- perSession.push({ edits, metrics, sessionBranch: sb ?? "unknown" });
442
- swappedSids.set(sid, idx);
443
- }
444
- if (!perSession.some((p) => p.edits.length > 0 || p.metrics.size > 0)) return;
445
- try {
446
- for (const p of perSession) {
447
- if (p.sessionBranch === "unknown" || !p.sessionBranch) {
448
- p.sessionBranch = await (_cliOverrides?.getCurrentBranch ?? getCurrentBranch)(store);
449
- }
450
- }
451
- for (const p of perSession) {
452
- const { edits, metrics, sessionBranch: branchKey } = p;
453
- if (edits.length === 0 && metrics.size === 0) continue;
454
- const [prevCodeRaw, prevTurnRaw] = await Promise.all([
455
- edits.length > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(
456
- store,
457
- `metrics.code.${branchKey}`
458
- ) : Promise.resolve(""),
459
- metrics.size > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(
460
- store,
461
- `metrics.turn.${branchKey}`
462
- ) : Promise.resolve("")
463
- ]);
464
- let codeWrite;
465
- if (edits.length > 0) {
466
- const files = [...new Set(edits.map((e) => e.filePath))];
467
- const entry = {
468
- timestamp: Date.now() / 1e3,
469
- summary: `Changed ${edits.length} block(s) across ${files.length} file(s): ${files.join(", ")}`,
470
- files
471
- };
472
- let acc = {
473
- schema_version: 2,
474
- entries: []
475
- };
476
- if (prevCodeRaw) {
477
- try {
478
- const parsed = JSON.parse(prevCodeRaw);
479
- if (parsed?.entries && Array.isArray(parsed.entries)) {
480
- acc = parsed;
481
- if (acc.schema_version < 2) acc.schema_version = 2;
482
- }
483
- } catch (e) {
484
- debugLog(
485
- "flushCapture: failed to parse existing code metrics, starting fresh:",
486
- errorMessage(e)
487
- );
488
- }
489
- }
490
- acc.entries.push(entry);
491
- if (acc.entries.length > METRICS_CODE_MAX)
492
- acc.entries = acc.entries.slice(-METRICS_CODE_MAX);
493
- codeWrite = (_cliOverrides?.runMemoir ?? runMemoir)(
494
- [
495
- "-s",
496
- store,
497
- "remember",
498
- "--replace",
499
- JSON.stringify(acc),
500
- "-p",
501
- `metrics.code.${branchKey}`
502
- ],
503
- { cwd: store }
504
- );
505
- }
506
- let turnWrite;
507
- if (metrics.size > 0) {
508
- const existing = parseTurnMetrics(prevTurnRaw);
509
- for (const [tool2, current] of metrics) {
510
- const prev = existing.get(tool2) ?? { calls: 0, errors: 0 };
511
- existing.set(tool2, {
512
- calls: prev.calls + current.calls,
513
- errors: prev.errors + current.errors
514
- });
515
- }
516
- turnWrite = (_cliOverrides?.runMemoir ?? runMemoir)(
517
- [
518
- "-s",
519
- store,
520
- "remember",
521
- "--replace",
522
- serializeTurnMetrics(existing),
523
- "-p",
524
- `metrics.turn.${branchKey}`
525
- ],
526
- { cwd: store }
527
- );
528
- }
529
- const writes = [];
530
- if (codeWrite) writes.push(codeWrite);
531
- if (turnWrite) writes.push(turnWrite);
532
- const results = await Promise.all(writes);
533
- for (const result of results) {
534
- if (!result.ok) {
535
- throw new Error(`Memoir write failed: ${result.error}`);
536
- }
537
- }
538
- }
539
- } catch (e) {
540
- for (const [sid, idx] of swappedSids) {
541
- const p = perSession[idx];
542
- const currentEdits = pendingEditsBySession.get(sid) ?? [];
543
- pendingEditsBySession.set(sid, [...p.edits, ...currentEdits]);
544
- const merged = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
545
- for (const [tool2, m] of p.metrics) {
546
- const prev = merged.get(tool2) ?? { calls: 0, errors: 0 };
547
- merged.set(tool2, { calls: prev.calls + m.calls, errors: prev.errors + m.errors });
548
- }
549
- toolMetricsBySession.set(sid, merged);
550
- }
551
- throw e;
552
- }
553
- } finally {
554
- releaseLock();
555
- }
556
- } catch (e) {
557
- debugLog("flushCapture: failed:", errorMessage(e));
558
- }
559
- }
560
-
561
- // src/recall-gate.ts
562
- function isSecretSanitizationEnabled() {
563
- return process.env.MEMOIR_SANITIZE_SECRETS !== "0";
564
- }
565
- var pendingRecall = /* @__PURE__ */ new Set();
566
- 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;
567
- var EXPLICIT_RECALL_PATTERN = /\b(memoir:recall|memoir:remember|memoir-recall|memoir-remember)\b|(\/recall|\/remember)\b/i;
568
- var RECALL_TRIGGER_PATTERNS = [
569
- // Action verbs and domain nouns
570
- /\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,
571
- // Question starts (how/why/what/where/when/should/can/could/would/is it/are we/do I/does it)
572
- /^(how|why|what|where|when|should|can|could|would|is it|are we|do I|does it)\b.*\?/im,
573
- // Code blocks (triple backticks)
574
- /```/,
575
- // Code definitions
576
- /\b(def|function|class|import|export)\s+/,
577
- // Memoir/recall keywords
578
- /\b(memoir|recall|remember|memory)\b/i,
579
- // File extensions
580
- /\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,
581
- // File paths (slash-containing tokens)
582
- /\w+\/\w+/
583
- ];
584
- function isAcknowledgement(text) {
585
- const trimmed = text.trim().toLowerCase();
586
- const words = trimmed.split(/\s+/);
587
- return words.length <= 5 && ACK_PATTERN.test(trimmed);
588
- }
589
- function shouldTriggerRecall(text) {
590
- const trimmed = text.trim();
591
- if (!trimmed) return false;
592
- if (EXPLICIT_RECALL_PATTERN.test(trimmed)) return true;
593
- if (trimmed.length < 10) return false;
594
- if (isAcknowledgement(trimmed)) return false;
595
- if (trimmed.length >= 40 && RECALL_TRIGGER_PATTERNS.some((p) => p.test(trimmed))) return true;
596
- return false;
597
- }
598
- var DEFAULT_RECALL_NAMESPACES = ["default", "project:onboard", "codebase:onboard"];
599
-
600
- // src/capture-hooks.ts
601
- var branchMatchCache = /* @__PURE__ */ new Map();
602
- var BRANCH_MATCH_TTL_MS = 5e3;
603
- async function handleShellEnv(storeRoot, _input, output) {
604
- try {
605
- output.env.MEMOIR_STORE = storeRoot;
606
- } catch (e) {
607
- debugLog("shell.env: failed:", errorMessage(e));
608
- }
609
- }
610
- async function handleToolExecuteAfter(input, output) {
611
- try {
612
- const typedInput = input;
613
- const typedOutput = output;
614
- const sid = typedInput?.sessionID ?? "default";
615
- if (process.env.MEMOIR_NO_CAPTURE === "1") return;
616
- if (process.env.MEMOIR_NO_METRICS !== "1") {
617
- const calls = 1;
618
- const errors = typedOutput?.metadata?.error || typedOutput?.output?.startsWith("Error:") ? 1 : 0;
619
- recordToolMetrics(sid, typedInput?.tool ?? "", { calls, errors });
620
- }
621
- if (process.env.MEMOIR_NO_CODE_SUMMARY !== "1" && EDIT_TOOLS.has(typedInput?.tool ?? "")) {
622
- const args = typedInput?.args ?? {};
623
- const filePath = typeof args.filePath === "string" ? args.filePath : typeof args.path === "string" ? args.path : "";
624
- if (filePath) {
625
- recordEdit(sid, {
626
- tool: typedInput?.tool ?? "",
627
- filePath,
628
- snippet: "",
629
- timestamp: Date.now()
630
- });
75
+ const { stdout } = await execFileAsync(
76
+ "uvx",
77
+ ["--from", "memoir-ai", "memoir", "-s", store, ...args],
78
+ {
79
+ encoding: "utf8",
80
+ timeout: 15e3,
81
+ maxBuffer: 1024 * 1024
631
82
  }
632
- }
633
- } catch (e) {
634
- debugLog("tool.execute.after: failed:", errorMessage(e));
635
- }
636
- }
637
- async function handleChatMessage(storeRoot, input, output) {
638
- try {
639
- const typedInput = input;
640
- const typedOutput = output;
641
- const sid = typedInput?.sessionID ?? "default";
642
- const text = (typedOutput?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(" ");
643
- if (shouldTriggerRecall(text)) {
644
- pendingRecall.add(sid);
645
- }
646
- const _cachedBranch = getCachedBranch(sid);
647
- const prevBranch = _cachedBranch === "unknown" ? void 0 : _cachedBranch;
648
- const now = Date.now();
649
- const cached = branchMatchCache.get(sid);
650
- if (cached && now - cached.ts < BRANCH_MATCH_TTL_MS) {
651
- setCachedBranch(sid, cached.branch);
652
- } else {
653
- const branch = await autoMatchMemoirBranch(storeRoot);
654
- branchMatchCache.set(sid, { branch, ts: now });
655
- setCachedBranch(sid, branch);
656
- }
657
- if (process.env.MEMOIR_NO_CAPTURE !== "1") {
658
- await flushCapture(storeRoot, prevBranch, sid);
659
- }
83
+ );
84
+ return stdout.trim();
660
85
  } catch (e) {
661
- debugLog("chat.message: failed:", errorMessage(e));
662
- }
663
- }
664
- async function flushAllCapture(storeRoot) {
665
- await flushCapture(storeRoot);
666
- }
667
-
668
- // src/memoir-ops.ts
669
- import { spawn } from "node:child_process";
670
- import { createHash } from "node:crypto";
671
- import { mkdir as mkdir2, readFile, rm as rm2, writeFile } from "node:fs/promises";
672
- import { homedir as homedir2 } from "node:os";
673
- import { join as join2 } from "node:path";
674
- var UI_URL_DEADLINE_MS = 5e3;
675
- var UI_URL_POLL_MS = 100;
676
- var UI_OUTPUT_MAX_BYTES = 256 * 1024;
677
- async function ensureStoreOrError(store) {
678
- try {
679
- await ensureStore(store);
86
+ debugLog("callMemoir failed:", errorMessage(e));
680
87
  return null;
681
- } catch (error) {
682
- return errorMessage(error);
683
- }
684
- }
685
- async function statusJson(store) {
686
- const storeErr = await ensureStoreOrError(store);
687
- if (storeErr) return `Memoir command failed: ${storeErr}`;
688
- const result = await runMemoir(["--json", "-s", store, "status"], { cwd: store });
689
- if (!result.ok) return result.error;
690
- try {
691
- const data = JSON.parse(result.stdout);
692
- const branch = await codeGitBranch();
693
- data.opencode = { store, project_git_root: process.cwd(), project_git_branch: branch };
694
- return JSON.stringify(data, null, 2);
695
- } catch (e) {
696
- debugLog("statusJson: failed to parse JSON:", errorMessage(e));
697
- return result.stdout;
698
88
  }
699
89
  }
700
- async function launchUi(store) {
701
- await ensureStore(store);
702
- const pidDir = join2(homedir2(), ".memoir", "ui-servers");
703
- await mkdir2(pidDir, { recursive: true });
704
- const hash = createHash("sha256").update(store).digest("hex").slice(0, 8);
705
- const pidfile = join2(pidDir, `${hash}.json`);
90
+ async function autoMatchMemoirBranch(store, sessionID) {
91
+ const codeBranch = currentGitBranch();
92
+ if (!codeBranch) return;
93
+ const cached = getCachedBranch(sessionID);
94
+ if (cached === codeBranch) return;
95
+ const branchResult = await callMemoir(["--json", "branch"], store);
96
+ if (branchResult === null) return;
706
97
  try {
707
- const existing = JSON.parse(await readFile(pidfile, "utf8"));
708
- if (existing?.pid && existing?.url) {
709
- try {
710
- process.kill(Number(existing.pid), 0);
711
- return JSON.stringify({ ...existing, reused: true }, null, 2);
712
- } catch (e) {
713
- debugLog("launchUi: process dead, relaunching:", errorMessage(e));
714
- }
98
+ const data = JSON.parse(branchResult);
99
+ const branches = data?.branches ?? [];
100
+ if (!branches.includes(codeBranch)) {
101
+ await callMemoir(["branch", codeBranch, "--from", "main"], store);
715
102
  }
103
+ await callMemoir(["checkout", codeBranch], store);
104
+ setCachedBranch(sessionID, codeBranch);
716
105
  } catch (e) {
717
- debugLog("launchUi: failed to read pidfile:", errorMessage(e));
718
- await rm2(pidfile, { force: true }).catch(
719
- (e2) => debugLog("launchUi: failed to remove stale pidfile", errorMessage(e2))
720
- );
106
+ debugLog("autoMatchMemoirBranch: parsing failed:", errorMessage(e));
721
107
  }
722
- let lastError = "";
723
- let uiSpecs = memoirSpawnSpecs(["ui", store]);
724
- if (memoirResolved) {
725
- const winner = uiSpecs.find((s) => s.label === memoirResolved);
726
- if (winner) uiSpecs = reorderResolver(uiSpecs, winner);
727
- }
728
- for (const spec of uiSpecs) {
729
- const child = spawn(spec.command, spec.args, {
730
- detached: true,
731
- stdio: ["ignore", "pipe", "pipe"],
732
- // Scrub process.env — only forward whitelisted vars to avoid leaking secrets
733
- env: Object.fromEntries(
734
- Object.entries({
735
- HOME: process.env.HOME,
736
- USER: process.env.USER,
737
- PATH: process.env.PATH,
738
- MEMOIR_STORE: process.env.MEMOIR_STORE,
739
- MEMOIR_DEBUG: process.env.MEMOIR_DEBUG
740
- }).filter(([, v]) => v != null)
741
- )
742
- });
743
- let output = "";
744
- child.stdout?.on("data", (chunk) => {
745
- if (output.length < UI_OUTPUT_MAX_BYTES) output += String(chunk);
746
- });
747
- child.stderr?.on("data", (chunk) => {
748
- if (output.length < UI_OUTPUT_MAX_BYTES) output += String(chunk);
749
- });
750
- const spawnFailed = new Promise((resolve2) => {
751
- child.once("error", (error2) => resolve2(errorMessage(error2)));
752
- child.once("spawn", () => resolve2(null));
753
- });
754
- const error = await spawnFailed;
755
- if (error) {
756
- lastError = `${spec.label}: ${error}`;
757
- continue;
758
- }
759
- child.unref();
760
- let childExited = false;
761
- child.once("exit", () => {
762
- childExited = true;
763
- });
764
- const urlPattern = /https?:\/\/(?:localhost|127\.0\.0\.1):\d+\S*/;
765
- const deadline = Date.now() + UI_URL_DEADLINE_MS;
766
- while (Date.now() < deadline && !childExited) {
767
- const match = output.match(urlPattern);
768
- if (match) {
769
- const url = match[0];
770
- const data = {
771
- pid: child.pid,
772
- url,
773
- store,
774
- command: spec.label,
775
- started: (/* @__PURE__ */ new Date()).toISOString(),
776
- reused: false
777
- };
778
- await writeFile(pidfile, JSON.stringify(data, null, 2));
779
- return JSON.stringify(data, null, 2);
780
- }
781
- await new Promise((resolve2) => setTimeout(resolve2, UI_URL_POLL_MS));
782
- }
783
- if (childExited) {
784
- lastError = `${spec.label}: process exited before URL was detected. Output: ${output.slice(0, 500)}`.trim();
785
- continue;
786
- }
787
- const msg = `Memoir UI started with ${spec.label} (pid ${child.pid ?? "unknown"}), but URL was not detected yet.
788
- ${output.trim()}`;
789
- return msg.trim();
790
- }
791
- return `Memoir UI failed to start: ${lastError || "no launcher succeeded"}`;
792
108
  }
793
- async function unmergedBranchesText(store) {
794
- await ensureStore(store);
795
- const branchResult = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
796
- if (!branchResult.ok) {
797
- return branchResult.error;
798
- }
799
- const data = JSON.parse(branchResult.stdout);
800
- const branches = data?.branches ?? [];
801
- const unmerged = [];
802
- const diffResults = await Promise.all(
803
- branches.filter((b) => b !== "main").map(async (branch) => {
804
- const diffResult = await runMemoir(["-s", store, "diff", branch, "main", "--stat"], {
805
- cwd: store
806
- });
807
- return { branch, diffResult };
808
- })
809
- );
810
- for (const { branch, diffResult } of diffResults) {
811
- if (!diffResult.ok) {
812
- debugLog("command.execute.before: memoir diff failed", diffResult.error);
813
- continue;
814
- }
815
- if (diffResult.stdout.trim()) {
816
- unmerged.push(branch);
817
- }
818
- }
819
- return unmerged.length > 0 ? `Unmerged branches:
820
- ${unmerged.join("\n")}` : "All branches are merged into main.";
109
+ function pruneBranchCache() {
110
+ branchCache.clear();
821
111
  }
822
112
 
823
- // src/commands.ts
824
- function pushText(output, text) {
825
- output.parts.length = 0;
826
- output.parts.push({ type: "text", text });
113
+ // src/index.ts
114
+ var sessionsWithStartupHint = /* @__PURE__ */ new Set();
115
+ function errorMessage2(e) {
116
+ return e instanceof Error ? e.message : String(e);
827
117
  }
828
- function registerCommands(config) {
829
- config.command = config.command ?? {};
830
- config.command["memoir:status"] = {
831
- description: "Show Memoir status for the current OpenCode project",
832
- template: "Show Memoir status for this OpenCode project."
833
- };
834
- config.command["memoir:ui"] = {
835
- description: "Launch or reopen the Memoir web UI for this project store",
836
- template: "Launch or reopen the Memoir UI for this project."
837
- };
838
- config.command["memoir:remember"] = {
839
- description: "Save a durable fact, preference, rule, or decision to Memoir",
840
- template: `Use Memoir to save this durable memory now.
841
-
842
- USER REQUEST:
843
- $ARGUMENTS
844
-
845
- Extract the memory content, choose a semantic path if none is supplied, then call the memoir_remember tool. Never save secrets.`
846
- };
847
- config.command["memoir:recall"] = {
848
- description: "Recall relevant facts from Memoir before answering",
849
- template: `Recall relevant Memoir memories for this request.
850
-
851
- USER REQUEST:
852
- $ARGUMENTS
853
-
854
- 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.`
855
- };
856
- config.command["memoir:onboard"] = {
857
- description: "Populate or refresh Memoir onboarding for this project",
858
- template: `Populate or refresh Memoir onboarding for the CURRENT OpenCode project only.
859
-
860
- USER REQUEST:
861
- $ARGUMENTS
118
+ var MemoirOpenCode = async (_input, rawOptions) => {
119
+ const opts = rawOptions ?? {};
120
+ if (opts.store) setPluginStoreOverride(opts.store);
121
+ const storePath = deriveStorePath();
122
+ const mcpCommand = ["uvx", "--from", "memoir-ai[mcp]", "memoir-mcp"];
123
+ if (storePath) {
124
+ mcpCommand.push("--store", storePath);
125
+ }
126
+ const hooks = {
127
+ name: "memoir",
128
+ config: async (config) => {
129
+ config.command = config.command ?? {};
130
+ config.command["memoir:onboard"] = {
131
+ description: "Populate or refresh Memoir onboarding for this project",
132
+ template: `Populate or refresh Memoir onboarding for the current project.
862
133
 
863
134
  Workflow:
864
- - Stay inside the current project/worktree. Do not inspect parent directories.
865
135
  - First obtain a project file tree to understand structure.
866
136
  - Start studying from project documentation.
867
137
  - Continue only based on what the tree and documentation show.
@@ -872,307 +142,77 @@ Memory rules:
872
142
  - Do not use preferences.* paths unless the user explicitly stated a preference.
873
143
  - If a fact is your interpretation, do not save it; report it as uncertain instead.
874
144
 
875
- 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.`
876
- };
877
- config.command["memoir:unmerged"] = {
878
- description: "List memoir branches with changes not yet merged into main",
879
- template: "List memoir branches that have diverged from main."
880
- };
881
- }
882
- async function handleCommandExecuteBefore(storeRoot, input, output) {
883
- try {
884
- if (input.command === "memoir:status") {
885
- pushText(output, await statusJson(storeRoot));
886
- } else if (input.command === "memoir:ui") {
887
- pushText(output, await launchUi(storeRoot));
888
- } else if (input.command === "memoir:remember") {
889
- const args = input.arguments;
890
- if (args && isSecretSanitizationEnabled() && SECRET_PATTERN.test(args)) {
891
- pushText(
892
- output,
893
- "Memoir: cannot remember content that matches secret patterns. Please remove sensitive data and try again."
894
- );
895
- }
896
- } else if (input.command === "memoir:unmerged") {
897
- pushText(output, await unmergedBranchesText(storeRoot));
898
- }
899
- } catch (error) {
900
- pushText(output, `Memoir command failed: ${errorMessage(error)}`);
901
- }
902
- }
903
-
904
- // src/session-context.ts
905
- var initContext = null;
906
- var initContextFetched = false;
907
- var MAX_TAXONOMY_RETRIES = 3;
908
- var taxonomyFetchRetries = 0;
909
- var SESSION_CONTEXT_TTL_MS = 60 * 60 * 1e3;
910
- var sessionsWithContext = /* @__PURE__ */ new Map();
911
- function hasSessionContext(sessionID) {
912
- const ts = sessionsWithContext.get(sessionID);
913
- if (ts === void 0) return false;
914
- if (Date.now() - ts > SESSION_CONTEXT_TTL_MS) {
915
- sessionsWithContext.delete(sessionID);
916
- return false;
917
- }
918
- return true;
919
- }
920
- function markSessionContext(sessionID) {
921
- sessionsWithContext.set(sessionID, Date.now());
922
- }
923
- function pruneStaleSessions() {
924
- const now = Date.now();
925
- for (const [id, ts] of sessionsWithContext) {
926
- if (now - ts > SESSION_CONTEXT_TTL_MS) sessionsWithContext.delete(id);
927
- }
928
- }
929
- async function handleEvent(storeRoot, input) {
930
- if (input.event.type === "session.created" && !initContextFetched) {
931
- initContextFetched = true;
932
- fetchTaxonomy(storeRoot).catch((e) => {
933
- debugLog("event: taxonomy fetch failed, will retry on next session:", errorMessage(e));
934
- taxonomyFetchRetries++;
935
- if (taxonomyFetchRetries >= MAX_TAXONOMY_RETRIES) {
936
- debugLog("memoir: taxonomy fetch failed after max retries, giving up");
937
- return;
938
- }
939
- initContextFetched = false;
940
- });
941
- }
942
- }
943
- async function fetchTaxonomy(storeRoot) {
944
- const taxonomyResult = await runMemoir(
945
- ["--json", "-s", storeRoot, "summarize", "--depth", "3", "-n", "default"],
946
- { cwd: storeRoot }
947
- );
948
- if (!taxonomyResult.ok) {
949
- throw new Error(taxonomyResult.error);
950
- }
951
- const pretty = tryPrettyJson(taxonomyResult.stdout);
952
- initContext = `[memoir] Available taxonomy paths:
953
- ${pretty}`;
954
- taxonomyFetchRetries = 0;
955
- }
956
- async function handleSystemTransform(input, output) {
957
- try {
958
- const typedInput = input;
959
- const typedOutput = output;
960
- if (typedInput?.sessionID && !hasSessionContext(typedInput.sessionID)) {
961
- markSessionContext(typedInput.sessionID);
962
- if (initContext) {
963
- typedOutput?.system?.unshift(initContext);
964
- }
965
- }
966
- if (typedInput?.sessionID && pendingRecall.has(typedInput.sessionID)) {
967
- pendingRecall.delete(typedInput.sessionID);
968
- typedOutput?.system?.push(
969
- "\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."
970
- );
971
- }
972
- } catch (e) {
973
- debugLog("system.transform: failed:", errorMessage(e));
974
- }
975
- }
976
- function clearSessionContext() {
977
- pruneStaleSessions();
978
- sessionsWithContext.clear();
979
- }
980
-
981
- // src/tools.ts
982
- import { tool } from "@opencode-ai/plugin";
983
- var memoirStatus = tool({
984
- description: "Show Memoir status for the current OpenCode project store.",
985
- args: {},
986
- execute: async () => statusJson(deriveStorePath())
987
- });
988
- var memoirRemember = tool({
989
- description: "Explicitly save a durable memory to Memoir at one or more semantic taxonomy paths.",
990
- args: {
991
- content: tool.schema.string().describe("Memory content to save. Do not include secrets."),
992
- path: tool.schema.string().describe("Semantic taxonomy path, e.g. preferences.coding.style."),
993
- namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default."),
994
- replace: tool.schema.boolean().optional().describe("Replace existing value at the path.")
995
- },
996
- execute: async (args) => {
997
- const content = args.content?.trim();
998
- if (!content) return "Memoir memory was not saved: content is empty.";
999
- if (isSecretSanitizationEnabled() && SECRET_PATTERN.test(content)) {
1000
- return "Memoir memory was not saved: the content looks like a secret or credential. Save a redacted rule instead.";
1001
- }
1002
- const paths = coercePaths(args.path);
1003
- if (paths.length === 0) {
1004
- return "Memoir memory was not saved: provide a semantic path, e.g. preferences.coding.style.";
1005
- }
1006
- const store = deriveStorePath();
1007
- const storeError = await ensureStoreOrError(store);
1008
- if (storeError) return storeError;
1009
- const cliArgs = ["--json", "-s", store, "remember", content];
1010
- for (const p of paths) cliArgs.push("-p", p);
1011
- cliArgs.push("-n", args.namespace ?? "default");
1012
- if (args.replace) cliArgs.push("--replace");
1013
- const rememberResult = await runMemoir(cliArgs, { cwd: store });
1014
- if (!rememberResult.ok) return rememberResult.error;
1015
- return tryPrettyJson(rememberResult.stdout);
1016
- }
1017
- });
1018
- var memoirRecall = tool({
1019
- description: "List Memoir memory keys across relevant namespaces for relevance picking. Never calls legacy memoir recall.",
1020
- args: {
1021
- query: tool.schema.string().optional().describe("User query or topic to recall for."),
1022
- namespace: tool.schema.string().optional().describe(
1023
- "Single Memoir namespace to inspect. If omitted, checks default + onboard namespaces."
1024
- ),
1025
- namespaces: tool.schema.array(tool.schema.string()).optional().describe("Namespaces to inspect. Defaults to default, project:onboard, codebase:onboard."),
1026
- includeMetrics: tool.schema.boolean().optional().describe("Include metrics.* memories in the listing.")
1027
- },
1028
- execute: async (args) => {
1029
- const store = deriveStorePath();
1030
- const storeError = await ensureStoreOrError(store);
1031
- if (storeError) return storeError;
1032
- const namespaces = args.namespace ? [args.namespace] : args.namespaces && args.namespaces.length > 0 ? args.namespaces : DEFAULT_RECALL_NAMESPACES;
1033
- const results = await Promise.all(
1034
- namespaces.map(async (namespace) => {
1035
- const summarizeResult = await runMemoir(
1036
- ["--json", "-s", store, "summarize", "--depth", "3", "-n", namespace],
1037
- { cwd: store }
1038
- );
1039
- if (!summarizeResult.ok) return summarizeResult.error;
1040
- const output = tryPrettyJson(summarizeResult.stdout);
1041
- return `## namespace: ${namespace}
1042
- ${output}`;
1043
- })
1044
- );
1045
- const sections = [];
1046
- const errors = [];
1047
- for (const r of results) {
1048
- if (typeof r === "string") {
1049
- if (r.startsWith("Memoir command failed")) {
1050
- errors.push(r);
1051
- } else {
1052
- sections.push(r);
1053
- }
1054
- }
1055
- }
1056
- 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.";
1057
- const query = args.query ? `Query: ${args.query}
1058
- ` : "";
1059
- const errorBlock = errors.length > 0 ? `
1060
-
1061
- Note: some namespaces failed:
1062
- ${errors.map((e) => ` - ${e}`).join("\n")}` : "";
1063
- return `${query}${note}
1064
- Pick at most 5-7 relevant exact keys across namespaces, then call memoir_get with the matching namespace if values are needed.
1065
- ${sections.join("\n\n")}${errorBlock}`;
1066
- }
1067
- });
1068
- var memoirGet = tool({
1069
- description: "Fetch exact Memoir memory keys after selecting them from memoir_recall output.",
1070
- args: {
1071
- keys: tool.schema.array(tool.schema.string()).describe("Exact memory keys to fetch."),
1072
- namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default.")
1073
- },
1074
- execute: async (args) => {
1075
- const keys = args.keys?.map((key) => key.trim()).filter(Boolean) ?? [];
1076
- if (keys.length === 0) return "No Memoir keys were provided.";
1077
- if (keys.length > MEMOIR_GET_MAX_KEYS) {
1078
- return `Error: Too many keys requested (max ${MEMOIR_GET_MAX_KEYS}, got ${keys.length}). Narrow your selection from memoir_recall output.`;
1079
- }
1080
- const store = deriveStorePath();
1081
- const storeError = await ensureStoreOrError(store);
1082
- if (storeError) return storeError;
1083
- const getResult = await runMemoir(
1084
- ["--json", "-s", store, "get", ...keys, "-n", args.namespace ?? "default"],
1085
- { cwd: store }
1086
- );
1087
- if (!getResult.ok) return getResult.error;
1088
- return tryPrettyJson(getResult.stdout);
1089
- }
1090
- });
1091
- var memoirTools = {
1092
- memoir_status: memoirStatus,
1093
- memoir_remember: memoirRemember,
1094
- memoir_recall: memoirRecall,
1095
- memoir_get: memoirGet
1096
- };
1097
-
1098
- // src/index.ts
1099
- var MemoirOpenCode = async (_input, rawOptions) => {
1100
- const opts = rawOptions ?? {};
1101
- if (opts.store) setPluginStoreOverride(opts.store);
1102
- const storeRoot = deriveStorePath();
1103
- return {
1104
- name: "memoir",
1105
- tool: { ...memoirTools },
1106
- config: async (opencodeConfig) => {
1107
- registerCommands(opencodeConfig);
145
+ Then call memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git.`
146
+ };
147
+ const mcp = config.mcp ?? {};
148
+ mcp.memoir = {
149
+ type: "local",
150
+ command: mcpCommand,
151
+ environment: storePath ? { MEMOIR_STORE: storePath } : void 0
152
+ };
153
+ config.mcp = mcp;
1108
154
  },
1109
- "command.execute.before": async (input, output) => {
1110
- await handleCommandExecuteBefore(storeRoot, input, output);
1111
- },
1112
- /**
1113
- * Inject MEMOIR_STORE into every shell command's environment so any memoir
1114
- * invocation automatically targets the right store without manual -s flags.
1115
- * Uses the cached value resolved at init time (avoiding execFileSync overhead
1116
- * on every shell command).
1117
- */
1118
155
  "shell.env": async (_input2, output) => {
1119
- await handleShellEnv(storeRoot, _input2, output);
1120
- },
1121
- /**
1122
- * Observe every tool execution for metrics and code-change tracking.
1123
- * Mirrors the observation phase of Claude Code's Stop hook.
1124
- * Never modifies the tool output.
1125
- */
1126
- "tool.execute.after": async (input, output) => {
1127
- await handleToolExecuteAfter(input, output);
1128
- },
1129
- /**
1130
- * Fires on every incoming user message.
1131
- *
1132
- * 1. Auto-match memoir branch to current git branch
1133
- * (cf. UserPromptSubmit's auto_match_memoir_branch in Claude Code plugin).
1134
- * 2. Flush pending edits from the previous assistant turn
1135
- * (cf. Stop hook code change audit, run after each turn).
1136
- * 3. Run the recall gate (cf. UserPromptSubmit).
1137
- *
1138
- * Steps 1–2 are skipped when MEMOIR_NO_CAPTURE=1.
1139
- */
1140
- "chat.message": async (input, output) => {
1141
- await handleChatMessage(storeRoot, input, output);
156
+ try {
157
+ if (storePath) {
158
+ output.env.MEMOIR_STORE = storePath;
159
+ }
160
+ } catch (e) {
161
+ debugLog("shell.env: failed:", errorMessage2(e));
162
+ }
1142
163
  },
1143
- /**
1144
- * Fires on SDK events. On session.created, kick off a background fetch of
1145
- * the taxonomy overview so it's ready before the first LLM call.
1146
- * Mirrors Claude Code's SessionStart context injection.
1147
- */
1148
- event: async (input) => {
1149
- await handleEvent(storeRoot, input);
164
+ "chat.message": async (input, _output) => {
165
+ try {
166
+ const sid = input.sessionID ?? "default";
167
+ incrementMsgCount(sid);
168
+ await autoMatchMemoirBranch(storePath, sid);
169
+ } catch (e) {
170
+ debugLog("chat.message: failed:", errorMessage2(e));
171
+ }
1150
172
  },
1151
- /**
1152
- * Fires before every LLM call.
1153
- *
1154
- * 1. Injects the memoir taxonomy context for this session (once per session).
1155
- * 2. If a recall is pending for this session, injects a brief instruction
1156
- * telling the model to use memoir tools (one-shot per trigger).
1157
- */
1158
173
  "experimental.chat.system.transform": async (input, output) => {
1159
- await handleSystemTransform(input, output);
174
+ try {
175
+ const sid = input.sessionID ?? "default";
176
+ if (input.sessionID && !sessionsWithStartupHint.has(sid)) {
177
+ sessionsWithStartupHint.add(sid);
178
+ output.system?.unshift(
179
+ '[memoir] You have Memoir MCP tools (memoir_recall, memoir_remember, memoir_get, memoir_summarize, memoir_status, memoir_branches, memoir_checkout).\nSAVE (memoir_remember) after: task completion, user stating preferences/constraints, discovering project facts. RECALL (memoir_recall \u2192 memoir_get) at session start for prior context, when user asks about prior work.\nNamespaces: "default" (user context), "codebase:onboard" (project facts in git repos). Taxonomy paths: preferences.*, project.*, codebase.*, decisions.*, learning.*.'
180
+ );
181
+ }
182
+ const count = sessionMsgCount.get(sid) ?? 0;
183
+ if (shouldRemind(count)) {
184
+ output.system?.push(
185
+ "\n[memoir] Reminder: Use memoir_remember to save context, memoir_recall to retrieve prior context."
186
+ );
187
+ }
188
+ } catch (e) {
189
+ debugLog("system.transform: failed:", errorMessage2(e));
190
+ }
1160
191
  },
1161
- /**
1162
- * Fires when the plugin is shut down. Flushes any remaining code changes
1163
- * and metrics (cf. SessionEnd heartbeat cleanup + final metrics flush).
1164
- */
1165
192
  dispose: async () => {
1166
193
  try {
1167
- await flushAllCapture(storeRoot);
1168
- clearSessionContext();
194
+ if (process.env.MEMOIR_AUTO_SAVE !== "0") {
195
+ for (const [sid, count] of sessionMsgCount) {
196
+ await callMemoir(
197
+ [
198
+ "remember",
199
+ `session.${sid}`,
200
+ "--message",
201
+ `Session ended \u2014 ${count} user messages exchanged`
202
+ ],
203
+ storePath
204
+ );
205
+ }
206
+ }
1169
207
  } catch (e) {
1170
- const msg = `memoir: dispose error: ${errorMessage(e)}`;
1171
- debugLog(msg);
1172
- console.error(msg);
208
+ debugLog("dispose: save failed:", errorMessage2(e));
1173
209
  }
210
+ sessionsWithStartupHint.clear();
211
+ pruneBranchCache();
212
+ pruneAll();
1174
213
  }
1175
214
  };
215
+ return hooks;
1176
216
  };
1177
217
  var plugin = {
1178
218
  id: "opencode-memoir",