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