opencode-memoir 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture-hooks.d.ts.map +1 -1
- package/dist/capture.d.ts +1 -1
- package/dist/capture.d.ts.map +1 -1
- package/dist/commands.d.ts +2 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +661 -513
- package/dist/index.js.map +4 -4
- package/dist/memoir-ops.d.ts.map +1 -1
- package/dist/recall-gate.d.ts +6 -2
- package/dist/recall-gate.d.ts.map +1 -1
- package/dist/session-context.d.ts.map +1 -1
- package/dist/store.d.ts +11 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// src/store.ts
|
|
2
|
-
import { execFile, execFileSync } from "node:child_process";
|
|
3
|
-
import { access, mkdir, rm } from "node:fs/promises";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { homedir, tmpdir } from "node:os";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
|
|
8
1
|
// src/debug.ts
|
|
9
2
|
function debugLog(...args) {
|
|
10
3
|
if (process.env.MEMOIR_DEBUG !== "1") return;
|
|
@@ -13,6 +6,13 @@ function debugLog(...args) {
|
|
|
13
6
|
`);
|
|
14
7
|
}
|
|
15
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
16
|
// src/utils.ts
|
|
17
17
|
function coercePaths(path) {
|
|
18
18
|
if (!path) return [];
|
|
@@ -54,12 +54,39 @@ var GIT_TIMEOUT_MS = 3e3;
|
|
|
54
54
|
var MEMOIR_TIMEOUT_MS = 15e3;
|
|
55
55
|
var MEMOIR_MAX_BUFFER = 1024 * 1024;
|
|
56
56
|
var NO_GIT_CACHE_MAX = 500;
|
|
57
|
-
var
|
|
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
|
+
}
|
|
58
77
|
function _main_worktree_root(cwd) {
|
|
59
|
-
if (
|
|
78
|
+
if (_checkNoGit(cwd)) return "";
|
|
60
79
|
try {
|
|
61
|
-
const gitDir = execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
62
|
-
|
|
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();
|
|
63
90
|
const resolvePath = (path) => {
|
|
64
91
|
if (path.startsWith("/")) return path;
|
|
65
92
|
return join(cwd, path);
|
|
@@ -67,9 +94,17 @@ function _main_worktree_root(cwd) {
|
|
|
67
94
|
const gitDirAbs = resolvePath(gitDir);
|
|
68
95
|
const gitCommonDirAbs = resolvePath(gitCommonDir);
|
|
69
96
|
if (gitDirAbs === gitCommonDirAbs) {
|
|
70
|
-
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
97
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
98
|
+
cwd,
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
timeout: GIT_TIMEOUT_MS
|
|
101
|
+
}).trim();
|
|
71
102
|
}
|
|
72
|
-
const worktreeList = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
103
|
+
const worktreeList = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
104
|
+
cwd,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
timeout: GIT_TIMEOUT_MS
|
|
107
|
+
});
|
|
73
108
|
const firstLine = worktreeList.split("\n")[0];
|
|
74
109
|
if (firstLine.startsWith("worktree ")) {
|
|
75
110
|
const mainWorktree = firstLine.substring("worktree ".length).trim();
|
|
@@ -77,11 +112,16 @@ function _main_worktree_root(cwd) {
|
|
|
77
112
|
return mainWorktree;
|
|
78
113
|
}
|
|
79
114
|
}
|
|
80
|
-
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
115
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
116
|
+
cwd,
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
timeout: GIT_TIMEOUT_MS
|
|
119
|
+
}).trim();
|
|
81
120
|
} catch (e) {
|
|
82
121
|
debugLog("_main_worktree_root: not a git repo or git error:", errorMessage(e));
|
|
83
|
-
|
|
84
|
-
|
|
122
|
+
sweepNoGitCache();
|
|
123
|
+
_noGitCache.set(cwd, Date.now());
|
|
124
|
+
if (_noGitCache.size > NO_GIT_CACHE_MAX) evictOldestNoGit();
|
|
85
125
|
return "";
|
|
86
126
|
}
|
|
87
127
|
}
|
|
@@ -124,7 +164,9 @@ async function ensureStore(store) {
|
|
|
124
164
|
if (ensuredStores.size > ENSURED_STORES_MAX) ensuredStores.clear();
|
|
125
165
|
return;
|
|
126
166
|
} catch {
|
|
127
|
-
await mkdir(join(store, ".."), { recursive: true }).catch(
|
|
167
|
+
await mkdir(join(store, ".."), { recursive: true }).catch(
|
|
168
|
+
(e) => debugLog("ensureStore: mkdir failed:", errorMessage(e))
|
|
169
|
+
);
|
|
128
170
|
}
|
|
129
171
|
const creationPromise = (async () => {
|
|
130
172
|
const tmpDir = join(tmpdir(), `memoir-scratch-${Date.now()}`);
|
|
@@ -141,7 +183,9 @@ async function ensureStore(store) {
|
|
|
141
183
|
debugLog("ensureStore: creation failed:", errorMessage(e));
|
|
142
184
|
throw e;
|
|
143
185
|
} finally {
|
|
144
|
-
rm(tmpDir, { recursive: true, force: true }).catch(
|
|
186
|
+
rm(tmpDir, { recursive: true, force: true }).catch(
|
|
187
|
+
(e) => debugLog("tmp cleanup failed:", errorMessage(e))
|
|
188
|
+
);
|
|
145
189
|
}
|
|
146
190
|
})();
|
|
147
191
|
storeCreations.set(store, creationPromise);
|
|
@@ -191,8 +235,16 @@ async function runMemoir(args, options = {}) {
|
|
|
191
235
|
function memoirSpawnSpecs(args) {
|
|
192
236
|
return [
|
|
193
237
|
{ command: "memoir", args, label: "memoir" },
|
|
194
|
-
{
|
|
195
|
-
|
|
238
|
+
{
|
|
239
|
+
command: "uvx",
|
|
240
|
+
args: ["--from", `${MEMOIR_PACKAGE}==${MEMOIR_AI_PIN}`, "memoir", ...args],
|
|
241
|
+
label: "uvx"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
command: "uv",
|
|
245
|
+
args: ["tool", "run", "--from", `${MEMOIR_PACKAGE}==${MEMOIR_AI_PIN}`, "memoir", ...args],
|
|
246
|
+
label: "uv tool run"
|
|
247
|
+
}
|
|
196
248
|
];
|
|
197
249
|
}
|
|
198
250
|
async function getCurrentBranch(store) {
|
|
@@ -210,7 +262,10 @@ async function getCurrentBranch(store) {
|
|
|
210
262
|
async function codeGitBranch() {
|
|
211
263
|
try {
|
|
212
264
|
const execFn = _storeTestOverrides.execFileAsyncFn ?? execFileAsync;
|
|
213
|
-
const { stdout } = await execFn("git", ["branch", "--show-current"], {
|
|
265
|
+
const { stdout } = await execFn("git", ["branch", "--show-current"], {
|
|
266
|
+
encoding: "utf8",
|
|
267
|
+
timeout: GIT_TIMEOUT_MS
|
|
268
|
+
});
|
|
214
269
|
return stdout.trim();
|
|
215
270
|
} catch (e) {
|
|
216
271
|
debugLog("codeGitBranch: failed:", errorMessage(e));
|
|
@@ -240,7 +295,9 @@ async function autoMatchMemoirBranch(store) {
|
|
|
240
295
|
const current = await getCurrentBranch(store);
|
|
241
296
|
if (current === codeBranch) return current;
|
|
242
297
|
if (!await branchExistsInMemoir(store, codeBranch)) {
|
|
243
|
-
const result2 = await runFn(["-s", store, "branch", codeBranch, "--from", "main"], {
|
|
298
|
+
const result2 = await runFn(["-s", store, "branch", codeBranch, "--from", "main"], {
|
|
299
|
+
cwd: store
|
|
300
|
+
});
|
|
244
301
|
if (!result2.ok) {
|
|
245
302
|
debugLog("autoMatchMemoirBranch: create branch failed:", result2.error);
|
|
246
303
|
return getCurrentBranch(store);
|
|
@@ -256,7 +313,9 @@ async function autoMatchMemoirBranch(store) {
|
|
|
256
313
|
async function readMemoirValue(store, key, namespace = "default") {
|
|
257
314
|
try {
|
|
258
315
|
const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
|
|
259
|
-
const result = await runFn(["--json", "-s", store, "get", key, "-n", namespace], {
|
|
316
|
+
const result = await runFn(["--json", "-s", store, "get", key, "-n", namespace], {
|
|
317
|
+
cwd: store
|
|
318
|
+
});
|
|
260
319
|
if (!result.ok) return "";
|
|
261
320
|
const parsed = JSON.parse(result.stdout);
|
|
262
321
|
const items = parsed?.items ?? [];
|
|
@@ -269,128 +328,224 @@ async function readMemoirValue(store, key, namespace = "default") {
|
|
|
269
328
|
}
|
|
270
329
|
var _storeTestOverrides = {};
|
|
271
330
|
|
|
272
|
-
// src/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
331
|
+
// src/capture.ts
|
|
332
|
+
var _cliOverrides = null;
|
|
333
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set([
|
|
334
|
+
"Edit",
|
|
335
|
+
"Write",
|
|
336
|
+
"MultiEdit",
|
|
337
|
+
"NotebookEdit",
|
|
338
|
+
"apply_patch",
|
|
339
|
+
"ApplyPatch",
|
|
340
|
+
"MultiFileEdit"
|
|
341
|
+
]);
|
|
342
|
+
function evictOldest(map, maxSize) {
|
|
343
|
+
if (map.size <= maxSize) return;
|
|
344
|
+
const firstKey = map.keys().next().value;
|
|
345
|
+
if (firstKey !== void 0) map.delete(firstKey);
|
|
346
|
+
}
|
|
347
|
+
var METRICS_CODE_MAX = 1e3;
|
|
348
|
+
var pendingEditsBySession = /* @__PURE__ */ new Map();
|
|
349
|
+
var toolMetricsBySession = /* @__PURE__ */ new Map();
|
|
350
|
+
var cachedBranchBySession = /* @__PURE__ */ new Map();
|
|
351
|
+
function getCachedBranch(sessionID) {
|
|
352
|
+
return cachedBranchBySession.get(sessionID) ?? "unknown";
|
|
353
|
+
}
|
|
354
|
+
function setCachedBranch(sessionID, branch) {
|
|
355
|
+
cachedBranchBySession.set(sessionID, branch);
|
|
356
|
+
evictOldest(cachedBranchBySession, 200);
|
|
357
|
+
}
|
|
358
|
+
function recordEdit(sessionID, edit) {
|
|
359
|
+
let edits = pendingEditsBySession.get(sessionID);
|
|
360
|
+
if (!edits) {
|
|
361
|
+
edits = [];
|
|
362
|
+
pendingEditsBySession.set(sessionID, edits);
|
|
363
|
+
evictOldest(pendingEditsBySession, 500);
|
|
289
364
|
}
|
|
365
|
+
edits.push(edit);
|
|
290
366
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const data = JSON.parse(result.stdout);
|
|
298
|
-
const branch = await codeGitBranch();
|
|
299
|
-
data.opencode = { store, project_git_root: process.cwd(), project_git_branch: branch };
|
|
300
|
-
return JSON.stringify(data, null, 2);
|
|
301
|
-
} catch (e) {
|
|
302
|
-
debugLog("statusJson: failed to parse JSON:", errorMessage(e));
|
|
303
|
-
return result.stdout;
|
|
367
|
+
function recordToolMetrics(sessionID, tool2, metrics) {
|
|
368
|
+
let sessionMetrics = toolMetricsBySession.get(sessionID);
|
|
369
|
+
if (!sessionMetrics) {
|
|
370
|
+
sessionMetrics = /* @__PURE__ */ new Map();
|
|
371
|
+
toolMetricsBySession.set(sessionID, sessionMetrics);
|
|
372
|
+
evictOldest(toolMetricsBySession, 500);
|
|
304
373
|
}
|
|
374
|
+
const prev = sessionMetrics.get(tool2) ?? { calls: 0, errors: 0 };
|
|
375
|
+
sessionMetrics.set(tool2, {
|
|
376
|
+
calls: prev.calls + metrics.calls,
|
|
377
|
+
errors: prev.errors + metrics.errors
|
|
378
|
+
});
|
|
305
379
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
380
|
+
function parseTurnMetrics(text) {
|
|
381
|
+
const result = /* @__PURE__ */ new Map();
|
|
382
|
+
if (!text) return result;
|
|
383
|
+
const parts = text.split("|");
|
|
384
|
+
for (const part of parts) {
|
|
385
|
+
const trimmed = part.trim();
|
|
386
|
+
if (!trimmed) continue;
|
|
387
|
+
const [tool2, callsStr, errorsStr] = trimmed.split(":");
|
|
388
|
+
if (!tool2) continue;
|
|
389
|
+
result.set(tool2, {
|
|
390
|
+
calls: parseInt(callsStr, 10) || 0,
|
|
391
|
+
errors: parseInt(errorsStr, 10) || 0
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
function serializeTurnMetrics(map) {
|
|
397
|
+
return [...map.entries()].map(([tool2, m]) => `${tool2}:${m.calls}:${m.errors}`).join(" | ");
|
|
398
|
+
}
|
|
399
|
+
var flushQueues = /* @__PURE__ */ new Map();
|
|
400
|
+
async function acquireFlushLock(store) {
|
|
401
|
+
const prev = flushQueues.get(store) ?? Promise.resolve();
|
|
402
|
+
let release;
|
|
403
|
+
const next = new Promise((resolve2) => {
|
|
404
|
+
release = resolve2;
|
|
405
|
+
});
|
|
406
|
+
flushQueues.set(store, next);
|
|
407
|
+
await prev;
|
|
408
|
+
return () => {
|
|
409
|
+
release();
|
|
410
|
+
if (flushQueues.get(store) === next) flushQueues.delete(store);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
async function flushCapture(store, branch, sessionID) {
|
|
414
|
+
const targets = sessionID ? [sessionID] : [...pendingEditsBySession.keys()];
|
|
415
|
+
if (targets.length === 0) return;
|
|
312
416
|
try {
|
|
313
|
-
|
|
314
|
-
if (
|
|
417
|
+
store = store ?? deriveStorePath();
|
|
418
|
+
if (!store) return;
|
|
419
|
+
await ensureStore(store);
|
|
420
|
+
const releaseLock = await acquireFlushLock(store);
|
|
421
|
+
try {
|
|
422
|
+
const perSession = [];
|
|
423
|
+
const swappedSids = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const sid of targets) {
|
|
425
|
+
const idx = perSession.length;
|
|
426
|
+
const edits = pendingEditsBySession.get(sid) ?? [];
|
|
427
|
+
pendingEditsBySession.set(sid, []);
|
|
428
|
+
const metrics = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
429
|
+
toolMetricsBySession.set(sid, /* @__PURE__ */ new Map());
|
|
430
|
+
const sb = branch ?? cachedBranchBySession.get(sid);
|
|
431
|
+
perSession.push({ edits, metrics, sessionBranch: sb ?? "unknown" });
|
|
432
|
+
swappedSids.set(sid, idx);
|
|
433
|
+
}
|
|
434
|
+
if (!perSession.some((p) => p.edits.length > 0 || p.metrics.size > 0)) return;
|
|
315
435
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
436
|
+
for (const p of perSession) {
|
|
437
|
+
if (p.sessionBranch === "unknown" || !p.sessionBranch) {
|
|
438
|
+
p.sessionBranch = await (_cliOverrides?.getCurrentBranch ?? getCurrentBranch)(store);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const p of perSession) {
|
|
442
|
+
const { edits, metrics, sessionBranch: branchKey } = p;
|
|
443
|
+
if (edits.length === 0 && metrics.size === 0) continue;
|
|
444
|
+
const [prevCodeRaw, prevTurnRaw] = await Promise.all([
|
|
445
|
+
edits.length > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(
|
|
446
|
+
store,
|
|
447
|
+
`metrics.code.${branchKey}`
|
|
448
|
+
) : Promise.resolve(""),
|
|
449
|
+
metrics.size > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(
|
|
450
|
+
store,
|
|
451
|
+
`metrics.turn.${branchKey}`
|
|
452
|
+
) : Promise.resolve("")
|
|
453
|
+
]);
|
|
454
|
+
let codeWrite;
|
|
455
|
+
if (edits.length > 0) {
|
|
456
|
+
const files = [...new Set(edits.map((e) => e.filePath))];
|
|
457
|
+
const entry = {
|
|
458
|
+
timestamp: Date.now() / 1e3,
|
|
459
|
+
summary: `Changed ${edits.length} block(s) across ${files.length} file(s): ${files.join(", ")}`,
|
|
460
|
+
files
|
|
461
|
+
};
|
|
462
|
+
let acc = {
|
|
463
|
+
schema_version: 2,
|
|
464
|
+
entries: []
|
|
465
|
+
};
|
|
466
|
+
if (prevCodeRaw) {
|
|
467
|
+
try {
|
|
468
|
+
const parsed = JSON.parse(prevCodeRaw);
|
|
469
|
+
if (parsed?.entries && Array.isArray(parsed.entries)) {
|
|
470
|
+
acc = parsed;
|
|
471
|
+
if (acc.schema_version < 2) acc.schema_version = 2;
|
|
472
|
+
}
|
|
473
|
+
} catch (e) {
|
|
474
|
+
debugLog(
|
|
475
|
+
"flushCapture: failed to parse existing code metrics, starting fresh:",
|
|
476
|
+
errorMessage(e)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
acc.entries.push(entry);
|
|
481
|
+
if (acc.entries.length > METRICS_CODE_MAX)
|
|
482
|
+
acc.entries = acc.entries.slice(-METRICS_CODE_MAX);
|
|
483
|
+
codeWrite = (_cliOverrides?.runMemoir ?? runMemoir)(
|
|
484
|
+
[
|
|
485
|
+
"-s",
|
|
486
|
+
store,
|
|
487
|
+
"remember",
|
|
488
|
+
"--replace",
|
|
489
|
+
JSON.stringify(acc),
|
|
490
|
+
"-p",
|
|
491
|
+
`metrics.code.${branchKey}`
|
|
492
|
+
],
|
|
493
|
+
{ cwd: store }
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
let turnWrite;
|
|
497
|
+
if (metrics.size > 0) {
|
|
498
|
+
const existing = parseTurnMetrics(prevTurnRaw);
|
|
499
|
+
for (const [tool2, current] of metrics) {
|
|
500
|
+
const prev = existing.get(tool2) ?? { calls: 0, errors: 0 };
|
|
501
|
+
existing.set(tool2, {
|
|
502
|
+
calls: prev.calls + current.calls,
|
|
503
|
+
errors: prev.errors + current.errors
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
turnWrite = (_cliOverrides?.runMemoir ?? runMemoir)(
|
|
507
|
+
[
|
|
508
|
+
"-s",
|
|
509
|
+
store,
|
|
510
|
+
"remember",
|
|
511
|
+
"--replace",
|
|
512
|
+
serializeTurnMetrics(existing),
|
|
513
|
+
"-p",
|
|
514
|
+
`metrics.turn.${branchKey}`
|
|
515
|
+
],
|
|
516
|
+
{ cwd: store }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
const writes = [];
|
|
520
|
+
if (codeWrite) writes.push(codeWrite);
|
|
521
|
+
if (turnWrite) writes.push(turnWrite);
|
|
522
|
+
const results = await Promise.all(writes);
|
|
523
|
+
for (const result of results) {
|
|
524
|
+
if (!result.ok) {
|
|
525
|
+
throw new Error(`Memoir write failed: ${result.error}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
318
529
|
} catch (e) {
|
|
319
|
-
|
|
530
|
+
for (const [sid, idx] of swappedSids) {
|
|
531
|
+
const p = perSession[idx];
|
|
532
|
+
const currentEdits = pendingEditsBySession.get(sid) ?? [];
|
|
533
|
+
pendingEditsBySession.set(sid, [...p.edits, ...currentEdits]);
|
|
534
|
+
const merged = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
535
|
+
for (const [tool2, m] of p.metrics) {
|
|
536
|
+
const prev = merged.get(tool2) ?? { calls: 0, errors: 0 };
|
|
537
|
+
merged.set(tool2, { calls: prev.calls + m.calls, errors: prev.errors + m.errors });
|
|
538
|
+
}
|
|
539
|
+
toolMetricsBySession.set(sid, merged);
|
|
540
|
+
}
|
|
541
|
+
throw e;
|
|
320
542
|
}
|
|
543
|
+
} finally {
|
|
544
|
+
releaseLock();
|
|
321
545
|
}
|
|
322
546
|
} catch (e) {
|
|
323
|
-
debugLog("
|
|
324
|
-
await rm2(pidfile, { force: true }).catch((e2) => debugLog("launchUi: failed to remove stale pidfile", errorMessage(e2)));
|
|
325
|
-
}
|
|
326
|
-
let lastError = "";
|
|
327
|
-
let uiSpecs = memoirSpawnSpecs(["ui", store]);
|
|
328
|
-
if (memoirResolved) {
|
|
329
|
-
const winner = uiSpecs.find((s) => s.label === memoirResolved);
|
|
330
|
-
if (winner) uiSpecs = reorderResolver(uiSpecs, winner);
|
|
547
|
+
debugLog("flushCapture: failed:", errorMessage(e));
|
|
331
548
|
}
|
|
332
|
-
for (const spec of uiSpecs) {
|
|
333
|
-
const child = spawn(spec.command, spec.args, {
|
|
334
|
-
detached: true,
|
|
335
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
336
|
-
env: process.env
|
|
337
|
-
});
|
|
338
|
-
let output = "";
|
|
339
|
-
child.stdout?.on("data", (chunk) => {
|
|
340
|
-
output += String(chunk);
|
|
341
|
-
});
|
|
342
|
-
child.stderr?.on("data", (chunk) => {
|
|
343
|
-
output += String(chunk);
|
|
344
|
-
});
|
|
345
|
-
const spawnFailed = new Promise((resolve2) => {
|
|
346
|
-
child.once("error", (error2) => resolve2(errorMessage(error2)));
|
|
347
|
-
child.once("spawn", () => resolve2(null));
|
|
348
|
-
});
|
|
349
|
-
const error = await spawnFailed;
|
|
350
|
-
if (error) {
|
|
351
|
-
lastError = `${spec.label}: ${error}`;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
child.unref();
|
|
355
|
-
const urlPattern = /https?:\/\/(?:localhost|127\.0\.0\.1):\d+\S*/;
|
|
356
|
-
const deadline = Date.now() + UI_URL_DEADLINE_MS;
|
|
357
|
-
while (Date.now() < deadline) {
|
|
358
|
-
const match = output.match(urlPattern);
|
|
359
|
-
if (match) {
|
|
360
|
-
const url = match[0];
|
|
361
|
-
const data = { pid: child.pid, url, store, command: spec.label, started: (/* @__PURE__ */ new Date()).toISOString(), reused: false };
|
|
362
|
-
await writeFile(pidfile, JSON.stringify(data, null, 2));
|
|
363
|
-
return JSON.stringify(data, null, 2);
|
|
364
|
-
}
|
|
365
|
-
await new Promise((resolve2) => setTimeout(resolve2, UI_URL_POLL_MS));
|
|
366
|
-
}
|
|
367
|
-
return `Memoir UI started with ${spec.label} (pid ${child.pid ?? "unknown"}), but URL was not detected yet.
|
|
368
|
-
${output.trim()}`.trim();
|
|
369
|
-
}
|
|
370
|
-
return `Memoir UI failed to start: ${lastError || "no launcher succeeded"}`;
|
|
371
|
-
}
|
|
372
|
-
async function unmergedBranchesText(store) {
|
|
373
|
-
await ensureStore(store);
|
|
374
|
-
const branchResult = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
|
|
375
|
-
if (!branchResult.ok) {
|
|
376
|
-
return branchResult.error;
|
|
377
|
-
}
|
|
378
|
-
const data = JSON.parse(branchResult.stdout);
|
|
379
|
-
const branches = data?.branches ?? [];
|
|
380
|
-
const unmerged = [];
|
|
381
|
-
for (const branch of branches) {
|
|
382
|
-
if (branch === "main") continue;
|
|
383
|
-
const diffResult = await runMemoir(["-s", store, "diff", branch, "main", "--stat"], { cwd: store });
|
|
384
|
-
if (!diffResult.ok) {
|
|
385
|
-
debugLog("command.execute.before: memoir diff failed", diffResult.error);
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (diffResult.stdout.trim()) {
|
|
389
|
-
unmerged.push(branch);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return unmerged.length > 0 ? `Unmerged branches:
|
|
393
|
-
${unmerged.join("\n")}` : "All branches are merged into main.";
|
|
394
549
|
}
|
|
395
550
|
|
|
396
551
|
// src/recall-gate.ts
|
|
@@ -432,405 +587,291 @@ function shouldTriggerRecall(text) {
|
|
|
432
587
|
}
|
|
433
588
|
var DEFAULT_RECALL_NAMESPACES = ["default", "project:onboard", "codebase:onboard"];
|
|
434
589
|
|
|
435
|
-
// src/
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
var memoirRemember = tool({
|
|
442
|
-
description: "Explicitly save a durable memory to Memoir at one or more semantic taxonomy paths.",
|
|
443
|
-
args: {
|
|
444
|
-
content: tool.schema.string().describe("Memory content to save. Do not include secrets."),
|
|
445
|
-
path: tool.schema.string().optional().describe("Semantic taxonomy path, e.g. preferences.coding.style."),
|
|
446
|
-
namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default."),
|
|
447
|
-
replace: tool.schema.boolean().optional().describe("Replace existing value at the path.")
|
|
448
|
-
},
|
|
449
|
-
execute: async (args) => {
|
|
450
|
-
const content = args.content?.trim();
|
|
451
|
-
if (!content) return "Memoir memory was not saved: content is empty.";
|
|
452
|
-
if (isSecretSanitizationEnabled() && SECRET_PATTERN.test(content)) {
|
|
453
|
-
return "Memoir memory was not saved: the content looks like a secret or credential. Save a redacted rule instead.";
|
|
454
|
-
}
|
|
455
|
-
const paths = coercePaths(args.path);
|
|
456
|
-
if (paths.length === 0) {
|
|
457
|
-
return "Memoir memory was not saved: provide a semantic path, e.g. preferences.coding.style.";
|
|
458
|
-
}
|
|
459
|
-
const store = deriveStorePath();
|
|
460
|
-
const storeError = await ensureStoreOrError(store);
|
|
461
|
-
if (storeError) return storeError;
|
|
462
|
-
const cliArgs = ["--json", "-s", store, "remember", content];
|
|
463
|
-
for (const p of paths) cliArgs.push("-p", p);
|
|
464
|
-
cliArgs.push("-n", args.namespace ?? "default");
|
|
465
|
-
if (args.replace) cliArgs.push("--replace");
|
|
466
|
-
const rememberResult = await runMemoir(cliArgs, { cwd: store });
|
|
467
|
-
if (!rememberResult.ok) return rememberResult.error;
|
|
468
|
-
return tryPrettyJson(rememberResult.stdout);
|
|
590
|
+
// src/capture-hooks.ts
|
|
591
|
+
async function handleShellEnv(storeRoot, _input, output) {
|
|
592
|
+
try {
|
|
593
|
+
output.env.MEMOIR_STORE = storeRoot;
|
|
594
|
+
} catch (e) {
|
|
595
|
+
debugLog("shell.env: failed:", errorMessage(e));
|
|
469
596
|
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const store = deriveStorePath();
|
|
481
|
-
const storeError = await ensureStoreOrError(store);
|
|
482
|
-
if (storeError) return storeError;
|
|
483
|
-
const namespaces = args.namespace ? [args.namespace] : args.namespaces && args.namespaces.length > 0 ? args.namespaces : DEFAULT_RECALL_NAMESPACES;
|
|
484
|
-
const sections = [];
|
|
485
|
-
for (const namespace of namespaces) {
|
|
486
|
-
const summarizeResult = await runMemoir(["--json", "-s", store, "summarize", "--depth", "3", "-n", namespace], { cwd: store });
|
|
487
|
-
if (!summarizeResult.ok) return summarizeResult.error;
|
|
488
|
-
const output = tryPrettyJson(summarizeResult.stdout);
|
|
489
|
-
sections.push(`## namespace: ${namespace}
|
|
490
|
-
${output}`);
|
|
597
|
+
}
|
|
598
|
+
async function handleToolExecuteAfter(input, output) {
|
|
599
|
+
try {
|
|
600
|
+
const typedInput = input;
|
|
601
|
+
const typedOutput = output;
|
|
602
|
+
const sid = typedInput?.sessionID ?? "default";
|
|
603
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_METRICS !== "1") {
|
|
604
|
+
const calls = 1;
|
|
605
|
+
const errors = typedOutput?.metadata?.error || typedOutput?.output?.startsWith("Error:") ? 1 : 0;
|
|
606
|
+
recordToolMetrics(sid, typedInput?.tool ?? "", { calls, errors });
|
|
491
607
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
keys: tool.schema.array(tool.schema.string()).describe("Exact memory keys to fetch."),
|
|
504
|
-
namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default.")
|
|
505
|
-
},
|
|
506
|
-
execute: async (args) => {
|
|
507
|
-
const keys = args.keys?.map((key) => key.trim()).filter(Boolean) ?? [];
|
|
508
|
-
if (keys.length === 0) return "No Memoir keys were provided.";
|
|
509
|
-
if (keys.length > MEMOIR_GET_MAX_KEYS) {
|
|
510
|
-
return `Error: Too many keys requested (max ${MEMOIR_GET_MAX_KEYS}, got ${keys.length}). Narrow your selection from memoir_recall output.`;
|
|
608
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_CODE_SUMMARY !== "1" && EDIT_TOOLS.has(typedInput?.tool ?? "")) {
|
|
609
|
+
const args = typedInput?.args ?? {};
|
|
610
|
+
const filePath = typeof args.filePath === "string" ? args.filePath : typeof args.path === "string" ? args.path : "";
|
|
611
|
+
if (filePath) {
|
|
612
|
+
recordEdit(sid, {
|
|
613
|
+
tool: typedInput?.tool ?? "",
|
|
614
|
+
filePath,
|
|
615
|
+
snippet: "",
|
|
616
|
+
timestamp: Date.now()
|
|
617
|
+
});
|
|
618
|
+
}
|
|
511
619
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (storeError) return storeError;
|
|
515
|
-
const getResult = await runMemoir(["--json", "-s", store, "get", ...keys, "-n", args.namespace ?? "default"], { cwd: store });
|
|
516
|
-
if (!getResult.ok) return getResult.error;
|
|
517
|
-
return tryPrettyJson(getResult.stdout);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
debugLog("tool.execute.after: failed:", errorMessage(e));
|
|
518
622
|
}
|
|
519
|
-
});
|
|
520
|
-
var memoirTools = {
|
|
521
|
-
memoir_status: memoirStatus,
|
|
522
|
-
memoir_remember: memoirRemember,
|
|
523
|
-
memoir_recall: memoirRecall,
|
|
524
|
-
memoir_get: memoirGet
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
// src/commands.ts
|
|
528
|
-
function pushText(output, text) {
|
|
529
|
-
output.parts.length = 0;
|
|
530
|
-
output.parts.push({ type: "text", text });
|
|
531
623
|
}
|
|
532
|
-
function
|
|
533
|
-
config.command = config.command ?? {};
|
|
534
|
-
config.command["memoir:status"] = {
|
|
535
|
-
description: "Show Memoir status for the current OpenCode project",
|
|
536
|
-
template: "Show Memoir status for this OpenCode project."
|
|
537
|
-
};
|
|
538
|
-
config.command["memoir:ui"] = {
|
|
539
|
-
description: "Launch or reopen the Memoir web UI for this project store",
|
|
540
|
-
template: "Launch or reopen the Memoir UI for this project."
|
|
541
|
-
};
|
|
542
|
-
config.command["memoir:remember"] = {
|
|
543
|
-
description: "Save a durable fact, preference, rule, or decision to Memoir",
|
|
544
|
-
template: `Use Memoir to save this durable memory now.
|
|
545
|
-
|
|
546
|
-
USER REQUEST:
|
|
547
|
-
$ARGUMENTS
|
|
548
|
-
|
|
549
|
-
Extract the memory content, choose a semantic path if none is supplied, then call the memoir_remember tool. Never save secrets.`
|
|
550
|
-
};
|
|
551
|
-
config.command["memoir:recall"] = {
|
|
552
|
-
description: "Recall relevant facts from Memoir before answering",
|
|
553
|
-
template: `Recall relevant Memoir memories for this request.
|
|
554
|
-
|
|
555
|
-
USER REQUEST:
|
|
556
|
-
$ARGUMENTS
|
|
557
|
-
|
|
558
|
-
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.`
|
|
559
|
-
};
|
|
560
|
-
config.command["memoir:onboard"] = {
|
|
561
|
-
description: "Populate or refresh Memoir onboarding for this project",
|
|
562
|
-
template: `Populate or refresh Memoir onboarding for the CURRENT OpenCode project only.
|
|
563
|
-
|
|
564
|
-
USER REQUEST:
|
|
565
|
-
$ARGUMENTS
|
|
566
|
-
|
|
567
|
-
Workflow:
|
|
568
|
-
- Stay inside the current project/worktree. Do not inspect parent directories.
|
|
569
|
-
- First obtain a project file tree to understand structure.
|
|
570
|
-
- Start studying from project documentation.
|
|
571
|
-
- Continue only based on what the tree and documentation show.
|
|
572
|
-
|
|
573
|
-
Memory rules:
|
|
574
|
-
- Record only verified facts from files/docs/code or explicit user statements.
|
|
575
|
-
- Do not write inferred user thoughts, intentions, preferences, or opinions.
|
|
576
|
-
- Do not use preferences.* paths unless the user explicitly stated a preference.
|
|
577
|
-
- If a fact is your interpretation, do not save it; report it as uncertain instead.
|
|
578
|
-
|
|
579
|
-
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.`
|
|
580
|
-
};
|
|
581
|
-
config.command["memoir:unmerged"] = {
|
|
582
|
-
description: "List memoir branches with changes not yet merged into main",
|
|
583
|
-
template: "List memoir branches that have diverged from main."
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
async function handleCommandExecuteBefore(storeRoot, input, output) {
|
|
624
|
+
async function handleChatMessage(storeRoot, input, output) {
|
|
587
625
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
626
|
+
const typedInput = input;
|
|
627
|
+
const typedOutput = output;
|
|
628
|
+
const sid = typedInput?.sessionID ?? "default";
|
|
629
|
+
const text = (typedOutput?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
630
|
+
if (shouldTriggerRecall(text)) {
|
|
631
|
+
pendingRecall.add(sid);
|
|
593
632
|
}
|
|
594
|
-
|
|
595
|
-
|
|
633
|
+
const _cachedBranch = getCachedBranch(sid);
|
|
634
|
+
const prevBranch = _cachedBranch === "unknown" ? void 0 : _cachedBranch;
|
|
635
|
+
setCachedBranch(sid, await autoMatchMemoirBranch(storeRoot));
|
|
636
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1") {
|
|
637
|
+
await flushCapture(storeRoot, prevBranch, sid);
|
|
596
638
|
}
|
|
597
|
-
} catch (
|
|
598
|
-
|
|
639
|
+
} catch (e) {
|
|
640
|
+
debugLog("chat.message: failed:", errorMessage(e));
|
|
599
641
|
}
|
|
600
642
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
var _cliOverrides = null;
|
|
604
|
-
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch", "ApplyPatch", "MultiFileEdit"]);
|
|
605
|
-
function evictOldest(map, maxSize) {
|
|
606
|
-
if (map.size <= maxSize) return;
|
|
607
|
-
const firstKey = map.keys().next().value;
|
|
608
|
-
if (firstKey !== void 0) map.delete(firstKey);
|
|
609
|
-
}
|
|
610
|
-
var METRICS_CODE_MAX = 1e3;
|
|
611
|
-
var pendingEditsBySession = /* @__PURE__ */ new Map();
|
|
612
|
-
var toolMetricsBySession = /* @__PURE__ */ new Map();
|
|
613
|
-
var cachedBranchBySession = /* @__PURE__ */ new Map();
|
|
614
|
-
function getCachedBranch(sessionID) {
|
|
615
|
-
return cachedBranchBySession.get(sessionID) ?? "unknown";
|
|
616
|
-
}
|
|
617
|
-
function setCachedBranch(sessionID, branch) {
|
|
618
|
-
cachedBranchBySession.set(sessionID, branch);
|
|
619
|
-
evictOldest(cachedBranchBySession, 200);
|
|
643
|
+
async function flushAllCapture(storeRoot) {
|
|
644
|
+
await flushCapture(storeRoot);
|
|
620
645
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
646
|
+
|
|
647
|
+
// src/memoir-ops.ts
|
|
648
|
+
import { spawn } from "node:child_process";
|
|
649
|
+
import { createHash } from "node:crypto";
|
|
650
|
+
import { mkdir as mkdir2, readFile, rm as rm2, writeFile } from "node:fs/promises";
|
|
651
|
+
import { homedir as homedir2 } from "node:os";
|
|
652
|
+
import { join as join2 } from "node:path";
|
|
653
|
+
var UI_URL_DEADLINE_MS = 5e3;
|
|
654
|
+
var UI_URL_POLL_MS = 100;
|
|
655
|
+
async function ensureStoreOrError(store) {
|
|
656
|
+
try {
|
|
657
|
+
await ensureStore(store);
|
|
658
|
+
return null;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
return errorMessage(error);
|
|
627
661
|
}
|
|
628
|
-
edits.push(edit);
|
|
629
662
|
}
|
|
630
|
-
function
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
function parseTurnMetrics(text) {
|
|
644
|
-
const result = /* @__PURE__ */ new Map();
|
|
645
|
-
if (!text) return result;
|
|
646
|
-
const parts = text.split("|");
|
|
647
|
-
for (const part of parts) {
|
|
648
|
-
const trimmed = part.trim();
|
|
649
|
-
if (!trimmed) continue;
|
|
650
|
-
const [tool2, callsStr, errorsStr] = trimmed.split(":");
|
|
651
|
-
if (!tool2) continue;
|
|
652
|
-
result.set(tool2, {
|
|
653
|
-
calls: parseInt(callsStr, 10) || 0,
|
|
654
|
-
errors: parseInt(errorsStr, 10) || 0
|
|
655
|
-
});
|
|
663
|
+
async function statusJson(store) {
|
|
664
|
+
const storeErr = await ensureStoreOrError(store);
|
|
665
|
+
if (storeErr) return `Memoir command failed: ${storeErr}`;
|
|
666
|
+
const result = await runMemoir(["--json", "-s", store, "status"], { cwd: store });
|
|
667
|
+
if (!result.ok) return result.error;
|
|
668
|
+
try {
|
|
669
|
+
const data = JSON.parse(result.stdout);
|
|
670
|
+
const branch = await codeGitBranch();
|
|
671
|
+
data.opencode = { store, project_git_root: process.cwd(), project_git_branch: branch };
|
|
672
|
+
return JSON.stringify(data, null, 2);
|
|
673
|
+
} catch (e) {
|
|
674
|
+
debugLog("statusJson: failed to parse JSON:", errorMessage(e));
|
|
675
|
+
return result.stdout;
|
|
656
676
|
}
|
|
657
|
-
return result;
|
|
658
|
-
}
|
|
659
|
-
function serializeTurnMetrics(map) {
|
|
660
|
-
return [...map.entries()].map(([tool2, m]) => `${tool2}:${m.calls}:${m.errors}`).join(" | ");
|
|
661
677
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
});
|
|
669
|
-
flushQueues.set(store, next);
|
|
670
|
-
await prev;
|
|
671
|
-
return () => {
|
|
672
|
-
release();
|
|
673
|
-
if (flushQueues.get(store) === next) flushQueues.delete(store);
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
async function flushCapture(store, branch, sessionID) {
|
|
677
|
-
const targets = sessionID ? [sessionID] : [...pendingEditsBySession.keys()];
|
|
678
|
-
if (targets.length === 0) return;
|
|
678
|
+
async function launchUi(store) {
|
|
679
|
+
await ensureStore(store);
|
|
680
|
+
const pidDir = join2(homedir2(), ".memoir", "ui-servers");
|
|
681
|
+
await mkdir2(pidDir, { recursive: true });
|
|
682
|
+
const hash = createHash("sha256").update(store).digest("hex").slice(0, 8);
|
|
683
|
+
const pidfile = join2(pidDir, `${hash}.json`);
|
|
679
684
|
try {
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
await ensureStore(store);
|
|
683
|
-
const releaseLock = await acquireFlushLock(store);
|
|
684
|
-
try {
|
|
685
|
-
const perSession = [];
|
|
686
|
-
const swappedSids = /* @__PURE__ */ new Map();
|
|
687
|
-
for (const sid of targets) {
|
|
688
|
-
const idx = perSession.length;
|
|
689
|
-
const edits = pendingEditsBySession.get(sid) ?? [];
|
|
690
|
-
pendingEditsBySession.set(sid, []);
|
|
691
|
-
const metrics = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
692
|
-
toolMetricsBySession.set(sid, /* @__PURE__ */ new Map());
|
|
693
|
-
const sb = branch ?? cachedBranchBySession.get(sid);
|
|
694
|
-
perSession.push({ edits, metrics, sessionBranch: sb ?? "unknown" });
|
|
695
|
-
swappedSids.set(sid, idx);
|
|
696
|
-
}
|
|
697
|
-
if (!perSession.some((p) => p.edits.length > 0 || p.metrics.size > 0)) return;
|
|
685
|
+
const existing = JSON.parse(await readFile(pidfile, "utf8"));
|
|
686
|
+
if (existing?.pid && existing?.url) {
|
|
698
687
|
try {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
p.sessionBranch = await (_cliOverrides?.getCurrentBranch ?? getCurrentBranch)(store);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
for (const p of perSession) {
|
|
705
|
-
const { edits, metrics, sessionBranch: branchKey } = p;
|
|
706
|
-
if (edits.length === 0 && metrics.size === 0) continue;
|
|
707
|
-
const [prevCodeRaw, prevTurnRaw] = await Promise.all([
|
|
708
|
-
edits.length > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(store, `metrics.code.${branchKey}`) : Promise.resolve(""),
|
|
709
|
-
metrics.size > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(store, `metrics.turn.${branchKey}`) : Promise.resolve("")
|
|
710
|
-
]);
|
|
711
|
-
let codeWrite;
|
|
712
|
-
if (edits.length > 0) {
|
|
713
|
-
const files = [...new Set(edits.map((e) => e.filePath))];
|
|
714
|
-
const entry = {
|
|
715
|
-
timestamp: Date.now() / 1e3,
|
|
716
|
-
summary: `Changed ${edits.length} block(s) across ${files.length} file(s): ${files.join(", ")}`,
|
|
717
|
-
files
|
|
718
|
-
};
|
|
719
|
-
let acc = {
|
|
720
|
-
schema_version: 2,
|
|
721
|
-
entries: []
|
|
722
|
-
};
|
|
723
|
-
if (prevCodeRaw) {
|
|
724
|
-
try {
|
|
725
|
-
const parsed = JSON.parse(prevCodeRaw);
|
|
726
|
-
if (parsed?.entries && Array.isArray(parsed.entries)) {
|
|
727
|
-
acc = parsed;
|
|
728
|
-
if (acc.schema_version < 2) acc.schema_version = 2;
|
|
729
|
-
}
|
|
730
|
-
} catch (e) {
|
|
731
|
-
debugLog("flushCapture: failed to parse existing code metrics, starting fresh:", errorMessage(e));
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
acc.entries.push(entry);
|
|
735
|
-
if (acc.entries.length > METRICS_CODE_MAX) acc.entries = acc.entries.slice(-METRICS_CODE_MAX);
|
|
736
|
-
codeWrite = (_cliOverrides?.runMemoir ?? runMemoir)(["-s", store, "remember", "--replace", JSON.stringify(acc), "-p", `metrics.code.${branchKey}`], { cwd: store });
|
|
737
|
-
}
|
|
738
|
-
let turnWrite;
|
|
739
|
-
if (metrics.size > 0) {
|
|
740
|
-
const existing = parseTurnMetrics(prevTurnRaw);
|
|
741
|
-
for (const [tool2, current] of metrics) {
|
|
742
|
-
const prev = existing.get(tool2) ?? { calls: 0, errors: 0 };
|
|
743
|
-
existing.set(tool2, { calls: prev.calls + current.calls, errors: prev.errors + current.errors });
|
|
744
|
-
}
|
|
745
|
-
turnWrite = (_cliOverrides?.runMemoir ?? runMemoir)(["-s", store, "remember", "--replace", serializeTurnMetrics(existing), "-p", `metrics.turn.${branchKey}`], { cwd: store });
|
|
746
|
-
}
|
|
747
|
-
const writes = [];
|
|
748
|
-
if (codeWrite) writes.push(codeWrite);
|
|
749
|
-
if (turnWrite) writes.push(turnWrite);
|
|
750
|
-
const results = await Promise.all(writes);
|
|
751
|
-
for (const result of results) {
|
|
752
|
-
if (!result.ok) {
|
|
753
|
-
throw new Error(`Memoir write failed: ${result.error}`);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
688
|
+
process.kill(Number(existing.pid), 0);
|
|
689
|
+
return JSON.stringify({ ...existing, reused: true }, null, 2);
|
|
757
690
|
} catch (e) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
691
|
+
debugLog("launchUi: process dead, relaunching:", errorMessage(e));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {
|
|
695
|
+
debugLog("launchUi: failed to read pidfile:", errorMessage(e));
|
|
696
|
+
await rm2(pidfile, { force: true }).catch(
|
|
697
|
+
(e2) => debugLog("launchUi: failed to remove stale pidfile", errorMessage(e2))
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
let lastError = "";
|
|
701
|
+
let uiSpecs = memoirSpawnSpecs(["ui", store]);
|
|
702
|
+
if (memoirResolved) {
|
|
703
|
+
const winner = uiSpecs.find((s) => s.label === memoirResolved);
|
|
704
|
+
if (winner) uiSpecs = reorderResolver(uiSpecs, winner);
|
|
705
|
+
}
|
|
706
|
+
for (const spec of uiSpecs) {
|
|
707
|
+
const child = spawn(spec.command, spec.args, {
|
|
708
|
+
detached: true,
|
|
709
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
710
|
+
// Scrub process.env — only forward whitelisted vars to avoid leaking secrets
|
|
711
|
+
env: Object.fromEntries(
|
|
712
|
+
Object.entries({
|
|
713
|
+
PATH: process.env.PATH,
|
|
714
|
+
MEMOIR_STORE: process.env.MEMOIR_STORE,
|
|
715
|
+
MEMOIR_DEBUG: process.env.MEMOIR_DEBUG
|
|
716
|
+
}).filter(([, v]) => v != null)
|
|
717
|
+
)
|
|
718
|
+
});
|
|
719
|
+
let output = "";
|
|
720
|
+
child.stdout?.on("data", (chunk) => {
|
|
721
|
+
output += String(chunk);
|
|
722
|
+
});
|
|
723
|
+
child.stderr?.on("data", (chunk) => {
|
|
724
|
+
output += String(chunk);
|
|
725
|
+
});
|
|
726
|
+
const spawnFailed = new Promise((resolve2) => {
|
|
727
|
+
child.once("error", (error2) => resolve2(errorMessage(error2)));
|
|
728
|
+
child.once("spawn", () => resolve2(null));
|
|
729
|
+
});
|
|
730
|
+
const error = await spawnFailed;
|
|
731
|
+
if (error) {
|
|
732
|
+
lastError = `${spec.label}: ${error}`;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
child.unref();
|
|
736
|
+
const urlPattern = /https?:\/\/(?:localhost|127\.0\.0\.1):\d+\S*/;
|
|
737
|
+
const deadline = Date.now() + UI_URL_DEADLINE_MS;
|
|
738
|
+
while (Date.now() < deadline) {
|
|
739
|
+
const match = output.match(urlPattern);
|
|
740
|
+
if (match) {
|
|
741
|
+
const url = match[0];
|
|
742
|
+
const data = {
|
|
743
|
+
pid: child.pid,
|
|
744
|
+
url,
|
|
745
|
+
store,
|
|
746
|
+
command: spec.label,
|
|
747
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
748
|
+
reused: false
|
|
749
|
+
};
|
|
750
|
+
await writeFile(pidfile, JSON.stringify(data, null, 2));
|
|
751
|
+
return JSON.stringify(data, null, 2);
|
|
770
752
|
}
|
|
771
|
-
|
|
772
|
-
releaseLock();
|
|
753
|
+
await new Promise((resolve2) => setTimeout(resolve2, UI_URL_POLL_MS));
|
|
773
754
|
}
|
|
774
|
-
|
|
775
|
-
|
|
755
|
+
return `Memoir UI started with ${spec.label} (pid ${child.pid ?? "unknown"}), but URL was not detected yet.
|
|
756
|
+
${output.trim()}`.trim();
|
|
776
757
|
}
|
|
758
|
+
return `Memoir UI failed to start: ${lastError || "no launcher succeeded"}`;
|
|
777
759
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
} catch (e) {
|
|
784
|
-
debugLog("shell.env: failed:", errorMessage(e));
|
|
760
|
+
async function unmergedBranchesText(store) {
|
|
761
|
+
await ensureStore(store);
|
|
762
|
+
const branchResult = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
|
|
763
|
+
if (!branchResult.ok) {
|
|
764
|
+
return branchResult.error;
|
|
785
765
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
766
|
+
const data = JSON.parse(branchResult.stdout);
|
|
767
|
+
const branches = data?.branches ?? [];
|
|
768
|
+
const unmerged = [];
|
|
769
|
+
for (const branch of branches) {
|
|
770
|
+
if (branch === "main") continue;
|
|
771
|
+
const diffResult = await runMemoir(["-s", store, "diff", branch, "main", "--stat"], {
|
|
772
|
+
cwd: store
|
|
773
|
+
});
|
|
774
|
+
if (!diffResult.ok) {
|
|
775
|
+
debugLog("command.execute.before: memoir diff failed", diffResult.error);
|
|
776
|
+
continue;
|
|
796
777
|
}
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
const filePath = typeof args.filePath === "string" ? args.filePath : typeof args.path === "string" ? args.path : "";
|
|
800
|
-
if (filePath) {
|
|
801
|
-
recordEdit(sid, { tool: typedInput?.tool ?? "", filePath, snippet: "", timestamp: Date.now() });
|
|
802
|
-
}
|
|
778
|
+
if (diffResult.stdout.trim()) {
|
|
779
|
+
unmerged.push(branch);
|
|
803
780
|
}
|
|
804
|
-
} catch (e) {
|
|
805
|
-
debugLog("tool.execute.after: failed:", errorMessage(e));
|
|
806
781
|
}
|
|
782
|
+
return unmerged.length > 0 ? `Unmerged branches:
|
|
783
|
+
${unmerged.join("\n")}` : "All branches are merged into main.";
|
|
807
784
|
}
|
|
808
|
-
|
|
785
|
+
|
|
786
|
+
// src/commands.ts
|
|
787
|
+
function pushText(output, text) {
|
|
788
|
+
output.parts.length = 0;
|
|
789
|
+
output.parts.push({ type: "text", text });
|
|
790
|
+
}
|
|
791
|
+
function registerCommands(config) {
|
|
792
|
+
config.command = config.command ?? {};
|
|
793
|
+
config.command["memoir:status"] = {
|
|
794
|
+
description: "Show Memoir status for the current OpenCode project",
|
|
795
|
+
template: "Show Memoir status for this OpenCode project."
|
|
796
|
+
};
|
|
797
|
+
config.command["memoir:ui"] = {
|
|
798
|
+
description: "Launch or reopen the Memoir web UI for this project store",
|
|
799
|
+
template: "Launch or reopen the Memoir UI for this project."
|
|
800
|
+
};
|
|
801
|
+
config.command["memoir:remember"] = {
|
|
802
|
+
description: "Save a durable fact, preference, rule, or decision to Memoir",
|
|
803
|
+
template: `Use Memoir to save this durable memory now.
|
|
804
|
+
|
|
805
|
+
USER REQUEST:
|
|
806
|
+
$ARGUMENTS
|
|
807
|
+
|
|
808
|
+
Extract the memory content, choose a semantic path if none is supplied, then call the memoir_remember tool. Never save secrets.`
|
|
809
|
+
};
|
|
810
|
+
config.command["memoir:recall"] = {
|
|
811
|
+
description: "Recall relevant facts from Memoir before answering",
|
|
812
|
+
template: `Recall relevant Memoir memories for this request.
|
|
813
|
+
|
|
814
|
+
USER REQUEST:
|
|
815
|
+
$ARGUMENTS
|
|
816
|
+
|
|
817
|
+
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.`
|
|
818
|
+
};
|
|
819
|
+
config.command["memoir:onboard"] = {
|
|
820
|
+
description: "Populate or refresh Memoir onboarding for this project",
|
|
821
|
+
template: `Populate or refresh Memoir onboarding for the CURRENT OpenCode project only.
|
|
822
|
+
|
|
823
|
+
USER REQUEST:
|
|
824
|
+
$ARGUMENTS
|
|
825
|
+
|
|
826
|
+
Workflow:
|
|
827
|
+
- Stay inside the current project/worktree. Do not inspect parent directories.
|
|
828
|
+
- First obtain a project file tree to understand structure.
|
|
829
|
+
- Start studying from project documentation.
|
|
830
|
+
- Continue only based on what the tree and documentation show.
|
|
831
|
+
|
|
832
|
+
Memory rules:
|
|
833
|
+
- Record only verified facts from files/docs/code or explicit user statements.
|
|
834
|
+
- Do not write inferred user thoughts, intentions, preferences, or opinions.
|
|
835
|
+
- Do not use preferences.* paths unless the user explicitly stated a preference.
|
|
836
|
+
- If a fact is your interpretation, do not save it; report it as uncertain instead.
|
|
837
|
+
|
|
838
|
+
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.`
|
|
839
|
+
};
|
|
840
|
+
config.command["memoir:unmerged"] = {
|
|
841
|
+
description: "List memoir branches with changes not yet merged into main",
|
|
842
|
+
template: "List memoir branches that have diverged from main."
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
async function handleCommandExecuteBefore(storeRoot, input, output) {
|
|
809
846
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const sid = typedInput?.sessionID ?? "default";
|
|
813
|
-
const text = (typedOutput?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
814
|
-
if (shouldTriggerRecall(text)) {
|
|
815
|
-
pendingRecall.add(sid);
|
|
847
|
+
if (input.command === "memoir:status") {
|
|
848
|
+
pushText(output, await statusJson(storeRoot));
|
|
816
849
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
setCachedBranch(sid, await autoMatchMemoirBranch(storeRoot));
|
|
820
|
-
if (process.env.MEMOIR_NO_CAPTURE !== "1") {
|
|
821
|
-
await flushCapture(storeRoot, prevBranch, sid);
|
|
850
|
+
if (input.command === "memoir:ui") {
|
|
851
|
+
pushText(output, await launchUi(storeRoot));
|
|
822
852
|
}
|
|
823
|
-
|
|
824
|
-
|
|
853
|
+
if (input.command === "memoir:remember") {
|
|
854
|
+
const args = input.arguments;
|
|
855
|
+
if (args && isSecretSanitizationEnabled() && SECRET_PATTERN.test(args)) {
|
|
856
|
+
pushText(
|
|
857
|
+
output,
|
|
858
|
+
"Memoir: cannot remember content that matches secret patterns. Please remove sensitive data and try again."
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (input.command === "memoir:unmerged") {
|
|
863
|
+
pushText(output, await unmergedBranchesText(storeRoot));
|
|
864
|
+
}
|
|
865
|
+
} catch (error) {
|
|
866
|
+
pushText(output, `Memoir command failed: ${errorMessage(error)}`);
|
|
825
867
|
}
|
|
826
868
|
}
|
|
827
|
-
async function flushAllCapture(storeRoot) {
|
|
828
|
-
await flushCapture(storeRoot);
|
|
829
|
-
}
|
|
830
869
|
|
|
831
870
|
// src/session-context.ts
|
|
832
871
|
var initContext = null;
|
|
833
872
|
var initContextFetched = false;
|
|
873
|
+
var MAX_TAXONOMY_RETRIES = 3;
|
|
874
|
+
var taxonomyFetchRetries = 0;
|
|
834
875
|
var SESSION_CONTEXT_TTL_MS = 60 * 60 * 1e3;
|
|
835
876
|
var sessionsWithContext = /* @__PURE__ */ new Map();
|
|
836
877
|
function hasSessionContext(sessionID) {
|
|
@@ -854,27 +895,30 @@ function pruneStaleSessions() {
|
|
|
854
895
|
async function handleEvent(storeRoot, input) {
|
|
855
896
|
if (input.event.type === "session.created" && !initContextFetched) {
|
|
856
897
|
initContextFetched = true;
|
|
857
|
-
(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (!taxonomyResult.ok) {
|
|
864
|
-
throw new Error(taxonomyResult.error);
|
|
865
|
-
}
|
|
866
|
-
const pretty = tryPrettyJson(taxonomyResult.stdout);
|
|
867
|
-
initContext = `[memoir] Available taxonomy paths:
|
|
868
|
-
${pretty}`;
|
|
869
|
-
} catch (e) {
|
|
870
|
-
debugLog("event: taxonomy fetch failed, will retry on next session:", errorMessage(e));
|
|
871
|
-
initContextFetched = false;
|
|
898
|
+
fetchTaxonomy(storeRoot).catch((e) => {
|
|
899
|
+
debugLog("event: taxonomy fetch failed, will retry on next session:", errorMessage(e));
|
|
900
|
+
taxonomyFetchRetries++;
|
|
901
|
+
if (taxonomyFetchRetries >= MAX_TAXONOMY_RETRIES) {
|
|
902
|
+
debugLog("memoir: taxonomy fetch failed after max retries, giving up");
|
|
903
|
+
return;
|
|
872
904
|
}
|
|
873
|
-
})().catch(() => {
|
|
874
905
|
initContextFetched = false;
|
|
875
906
|
});
|
|
876
907
|
}
|
|
877
908
|
}
|
|
909
|
+
async function fetchTaxonomy(storeRoot) {
|
|
910
|
+
const taxonomyResult = await runMemoir(
|
|
911
|
+
["--json", "-s", storeRoot, "summarize", "--depth", "3", "-n", "default"],
|
|
912
|
+
{ cwd: storeRoot }
|
|
913
|
+
);
|
|
914
|
+
if (!taxonomyResult.ok) {
|
|
915
|
+
throw new Error(taxonomyResult.error);
|
|
916
|
+
}
|
|
917
|
+
const pretty = tryPrettyJson(taxonomyResult.stdout);
|
|
918
|
+
initContext = `[memoir] Available taxonomy paths:
|
|
919
|
+
${pretty}`;
|
|
920
|
+
taxonomyFetchRetries = 0;
|
|
921
|
+
}
|
|
878
922
|
async function handleSystemTransform(input, output) {
|
|
879
923
|
try {
|
|
880
924
|
const typedInput = input;
|
|
@@ -900,6 +944,107 @@ function clearSessionContext() {
|
|
|
900
944
|
sessionsWithContext.clear();
|
|
901
945
|
}
|
|
902
946
|
|
|
947
|
+
// src/tools.ts
|
|
948
|
+
import { tool } from "@opencode-ai/plugin";
|
|
949
|
+
var memoirStatus = tool({
|
|
950
|
+
description: "Show Memoir status for the current OpenCode project store.",
|
|
951
|
+
args: {},
|
|
952
|
+
execute: async () => statusJson(deriveStorePath())
|
|
953
|
+
});
|
|
954
|
+
var memoirRemember = tool({
|
|
955
|
+
description: "Explicitly save a durable memory to Memoir at one or more semantic taxonomy paths.",
|
|
956
|
+
args: {
|
|
957
|
+
content: tool.schema.string().describe("Memory content to save. Do not include secrets."),
|
|
958
|
+
path: tool.schema.string().optional().describe("Semantic taxonomy path, e.g. preferences.coding.style."),
|
|
959
|
+
namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default."),
|
|
960
|
+
replace: tool.schema.boolean().optional().describe("Replace existing value at the path.")
|
|
961
|
+
},
|
|
962
|
+
execute: async (args) => {
|
|
963
|
+
const content = args.content?.trim();
|
|
964
|
+
if (!content) return "Memoir memory was not saved: content is empty.";
|
|
965
|
+
if (isSecretSanitizationEnabled() && SECRET_PATTERN.test(content)) {
|
|
966
|
+
return "Memoir memory was not saved: the content looks like a secret or credential. Save a redacted rule instead.";
|
|
967
|
+
}
|
|
968
|
+
const paths = coercePaths(args.path);
|
|
969
|
+
if (paths.length === 0) {
|
|
970
|
+
return "Memoir memory was not saved: provide a semantic path, e.g. preferences.coding.style.";
|
|
971
|
+
}
|
|
972
|
+
const store = deriveStorePath();
|
|
973
|
+
const storeError = await ensureStoreOrError(store);
|
|
974
|
+
if (storeError) return storeError;
|
|
975
|
+
const cliArgs = ["--json", "-s", store, "remember", content];
|
|
976
|
+
for (const p of paths) cliArgs.push("-p", p);
|
|
977
|
+
cliArgs.push("-n", args.namespace ?? "default");
|
|
978
|
+
if (args.replace) cliArgs.push("--replace");
|
|
979
|
+
const rememberResult = await runMemoir(cliArgs, { cwd: store });
|
|
980
|
+
if (!rememberResult.ok) return rememberResult.error;
|
|
981
|
+
return tryPrettyJson(rememberResult.stdout);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
var memoirRecall = tool({
|
|
985
|
+
description: "List Memoir memory keys across relevant namespaces for relevance picking. Never calls legacy memoir recall.",
|
|
986
|
+
args: {
|
|
987
|
+
query: tool.schema.string().optional().describe("User query or topic to recall for."),
|
|
988
|
+
namespace: tool.schema.string().optional().describe(
|
|
989
|
+
"Single Memoir namespace to inspect. If omitted, checks default + onboard namespaces."
|
|
990
|
+
),
|
|
991
|
+
namespaces: tool.schema.array(tool.schema.string()).optional().describe("Namespaces to inspect. Defaults to default, project:onboard, codebase:onboard."),
|
|
992
|
+
includeMetrics: tool.schema.boolean().optional().describe("Include metrics.* memories in the listing.")
|
|
993
|
+
},
|
|
994
|
+
execute: async (args) => {
|
|
995
|
+
const store = deriveStorePath();
|
|
996
|
+
const storeError = await ensureStoreOrError(store);
|
|
997
|
+
if (storeError) return storeError;
|
|
998
|
+
const namespaces = args.namespace ? [args.namespace] : args.namespaces && args.namespaces.length > 0 ? args.namespaces : DEFAULT_RECALL_NAMESPACES;
|
|
999
|
+
const sections = [];
|
|
1000
|
+
for (const namespace of namespaces) {
|
|
1001
|
+
const summarizeResult = await runMemoir(
|
|
1002
|
+
["--json", "-s", store, "summarize", "--depth", "3", "-n", namespace],
|
|
1003
|
+
{ cwd: store }
|
|
1004
|
+
);
|
|
1005
|
+
if (!summarizeResult.ok) return summarizeResult.error;
|
|
1006
|
+
const output = tryPrettyJson(summarizeResult.stdout);
|
|
1007
|
+
sections.push(`## namespace: ${namespace}
|
|
1008
|
+
${output}`);
|
|
1009
|
+
}
|
|
1010
|
+
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.";
|
|
1011
|
+
const query = args.query ? `Query: ${args.query}
|
|
1012
|
+
` : "";
|
|
1013
|
+
return `${query}${note}
|
|
1014
|
+
Pick at most 5-7 relevant exact keys across namespaces, then call memoir_get with the matching namespace if values are needed.
|
|
1015
|
+
${sections.join("\n\n")}`;
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
var memoirGet = tool({
|
|
1019
|
+
description: "Fetch exact Memoir memory keys after selecting them from memoir_recall output.",
|
|
1020
|
+
args: {
|
|
1021
|
+
keys: tool.schema.array(tool.schema.string()).describe("Exact memory keys to fetch."),
|
|
1022
|
+
namespace: tool.schema.string().optional().describe("Memoir namespace. Defaults to default.")
|
|
1023
|
+
},
|
|
1024
|
+
execute: async (args) => {
|
|
1025
|
+
const keys = args.keys?.map((key) => key.trim()).filter(Boolean) ?? [];
|
|
1026
|
+
if (keys.length === 0) return "No Memoir keys were provided.";
|
|
1027
|
+
if (keys.length > MEMOIR_GET_MAX_KEYS) {
|
|
1028
|
+
return `Error: Too many keys requested (max ${MEMOIR_GET_MAX_KEYS}, got ${keys.length}). Narrow your selection from memoir_recall output.`;
|
|
1029
|
+
}
|
|
1030
|
+
const store = deriveStorePath();
|
|
1031
|
+
const storeError = await ensureStoreOrError(store);
|
|
1032
|
+
if (storeError) return storeError;
|
|
1033
|
+
const getResult = await runMemoir(
|
|
1034
|
+
["--json", "-s", store, "get", ...keys, "-n", args.namespace ?? "default"],
|
|
1035
|
+
{ cwd: store }
|
|
1036
|
+
);
|
|
1037
|
+
if (!getResult.ok) return getResult.error;
|
|
1038
|
+
return tryPrettyJson(getResult.stdout);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
var memoirTools = {
|
|
1042
|
+
memoir_status: memoirStatus,
|
|
1043
|
+
memoir_remember: memoirRemember,
|
|
1044
|
+
memoir_recall: memoirRecall,
|
|
1045
|
+
memoir_get: memoirGet
|
|
1046
|
+
};
|
|
1047
|
+
|
|
903
1048
|
// src/index.ts
|
|
904
1049
|
var MemoirOpenCode = async (_input, rawOptions) => {
|
|
905
1050
|
const opts = rawOptions ?? {};
|
|
@@ -972,6 +1117,9 @@ var MemoirOpenCode = async (_input, rawOptions) => {
|
|
|
972
1117
|
await flushAllCapture(storeRoot);
|
|
973
1118
|
clearSessionContext();
|
|
974
1119
|
} catch (e) {
|
|
1120
|
+
const msg = `memoir: dispose error: ${errorMessage(e)}`;
|
|
1121
|
+
debugLog(msg);
|
|
1122
|
+
console.error(msg);
|
|
975
1123
|
}
|
|
976
1124
|
}
|
|
977
1125
|
};
|