open-memex 0.4.0-alpha.7 → 0.4.1

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/cli.js CHANGED
@@ -12,7 +12,7 @@ import { getPrStatus, formatPrStatus, applyPrStatus } from "./github.js";
12
12
  import { search, list, hitStateLabel } from "./retrieve/search.js";
13
13
  import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, } from "./store/markdown.js";
14
14
  import { loadConfig } from "./config.js";
15
- import { paths } from "./paths.js";
15
+ import { paths, projectRoot } from "./paths.js";
16
16
  import { redact } from "./redact.js";
17
17
  import { resolveMcpCommand } from "./init.js";
18
18
  import fs from "node:fs";
@@ -23,7 +23,10 @@ import { fileURLToPath } from "node:url";
23
23
  const COMMAND_HELP = {
24
24
  where: `Show which project scope the current directory resolves to, and where its data lives.
25
25
 
26
- Usage: open-memex where`,
26
+ Usage: open-memex where
27
+
28
+ Example:
29
+ open-memex where`,
27
30
  list: `List memories in a scope, newest first.
28
31
 
29
32
  Usage: open-memex list [--scope project|personal] [--type T] [--limit N]
@@ -60,13 +63,22 @@ Example:
60
63
  open-memex add "We deploy on Fridays" --scope project --tag process`,
61
64
  supersede: `Replace a memory with a newer version. The old one is kept as history.
62
65
 
63
- Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]`,
66
+ Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
67
+
68
+ Example:
69
+ open-memex supersede 01ABC "We deploy on Thursdays now"`,
64
70
  status: `Change a memory's lifecycle status.
65
71
 
66
- Usage: open-memex status <id> active|deprecated|retracted|archived`,
72
+ Usage: open-memex status <id> active|deprecated|retracted|archived
73
+
74
+ Example:
75
+ open-memex status 01ABC deprecated`,
67
76
  forget: `Delete a memory by id.
68
77
 
69
- Usage: open-memex forget <id>`,
78
+ Usage: open-memex forget <id>
79
+
80
+ Example:
81
+ open-memex forget 01ABC`,
70
82
  propose: `Copy personal memories into the project outbox as review drafts.
71
83
  The personal originals stay put. Nothing enters git at this step.
72
84
 
@@ -100,12 +112,70 @@ Usage: open-memex resolve [id-or-path]
100
112
 
101
113
  With no argument, lists conflicts. With an id or file path, shows the
102
114
  3-way merge (base / outbox / repo) so you can resolve it by hand.
103
- Conflicts are never auto-resolved.`,
115
+ Conflicts are never auto-resolved.
116
+
117
+ Examples:
118
+ open-memex resolve
119
+ open-memex resolve 01ABC`,
104
120
  "sync-status": `Show the project memory sync pipeline: when the index last synced
105
121
  and what triggered it, drafts waiting in the outbox (appdata), memories in the
106
122
  repo awaiting review or published, and repo files not yet committed.
107
123
 
108
- Usage: open-memex sync-status`,
124
+ Usage: open-memex sync-status
125
+
126
+ Example:
127
+ open-memex sync-status`,
128
+ pull: `Pull shared project memories from the git remote: fetch + fast-forward
129
+ only. Never auto-merges — a diverged branch fails with a clear message and is
130
+ left for you to resolve by hand. On success the local index re-syncs.
131
+
132
+ Usage: open-memex pull
133
+
134
+ Example:
135
+ open-memex pull`,
136
+ push: `Push the current branch (with its submitted memories) to the git
137
+ remote. Explicit only — open-memex never pushes on its own.
138
+
139
+ Usage: open-memex push
140
+
141
+ Example:
142
+ open-memex push`,
143
+ export: `Export memories to a portable .tar.gz bundle (markdown source of
144
+ truth + manifest.json) for moving to another machine or another app.
145
+ Excludes visibility:private memories by default; --all includes everything.
146
+
147
+ Usage: open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
148
+
149
+ Flags:
150
+ --scope project (default), personal, or both
151
+ --type filter by memory type
152
+ --tag filter by tag
153
+ --all, -a include private memories (full migration)
154
+ -o output file (default: ./open-memex-export-<timestamp>.tar.gz)
155
+
156
+ Examples:
157
+ open-memex export -o backup.tar.gz
158
+ open-memex export --scope both --all -o full-migration.tar.gz`,
159
+ import: `Import a bundle created by \`open-memex export\`. Personal memories
160
+ go to the personal dir; project memories are re-keyed to the current project
161
+ and land in the outbox as drafts. Existing identical memories are skipped;
162
+ conflicting ids are reported, never overwritten.
163
+
164
+ Usage: open-memex import <bundle.tar.gz> [--dry-run]
165
+
166
+ Examples:
167
+ open-memex import backup.tar.gz --dry-run
168
+ open-memex import backup.tar.gz`,
169
+ "distill-agents": `Propose an AGENTS.md snippet distilled from project
170
+ memories (decisions, constraints, lessons, gotchas, howtos). Prints markdown
171
+ to stdout, or writes it with -o. Review and merge by hand — open-memex never
172
+ rewrites your AGENTS.md on its own.
173
+
174
+ Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
175
+
176
+ Examples:
177
+ open-memex distill-agents
178
+ open-memex distill-agents --type decision,gotcha -o agents-snippet.md`,
109
179
  submit: `Move outbox drafts into the repo for review: copies the drafts into
110
180
  the repo memory dir as proposed (a local-approved copy keeps its approval),
111
181
  commits locally on the CURRENT branch, and moves the outbox originals out.
@@ -129,13 +199,23 @@ overwritten. Report-only by default.
129
199
  Usage: open-memex pr-status [--apply]
130
200
 
131
201
  Flags:
132
- --apply write the transitions locally (still never pushes)`,
202
+ --apply write the transitions locally (still never pushes)
203
+
204
+ Examples:
205
+ open-memex pr-status
206
+ open-memex pr-status --apply`,
133
207
  reindex: `Rebuild the SQLite index from the markdown files.
134
208
 
135
- Usage: open-memex reindex`,
209
+ Usage: open-memex reindex
210
+
211
+ Example:
212
+ open-memex reindex`,
136
213
  scopes: `List the known scopes (personal + project).
137
214
 
138
- Usage: open-memex scopes`,
215
+ Usage: open-memex scopes
216
+
217
+ Example:
218
+ open-memex scopes`,
139
219
  migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
140
220
 
141
221
  Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict newer|overwrite|skip]
@@ -143,17 +223,29 @@ Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict
143
223
 
144
224
  Flags:
145
225
  --from / --to scope keys (default: current project → personal)
146
- --dry-run preview without moving anything
226
+ --dry-run preview without moving anything (--to-v2 previews the legacy
227
+ files in place, including per-file conversion plans)
147
228
  --on-conflict newer (default), overwrite, or skip
148
229
  --to-v2 convert a legacy my-o-memory data dir to the v2 layout
149
230
 
150
- Always preview with --dry-run first; nothing moves without confirmation.`,
231
+ Always preview with --dry-run first; nothing moves without confirmation.
232
+ On Windows, if another program holds the legacy folder open, the backup
233
+ rename fails with an actionable message instead of a stack trace — close
234
+ the program and re-run.
235
+
236
+ Examples:
237
+ open-memex migrate --dry-run
238
+ open-memex migrate --from personal --to project --dry-run`,
151
239
  mcp: `Start the stdio MCP server (the same server editors connect to).
152
240
 
153
241
  Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
154
242
 
155
243
  Flags:
156
- --print-config print the MCP client config instead of starting the server`,
244
+ --print-config print the MCP client config instead of starting the server
245
+
246
+ Examples:
247
+ open-memex mcp
248
+ open-memex mcp --print-config vscode`,
157
249
  init: `One-command project setup: writes the MCP config for your editor and the
158
250
  agent memory instructions. Existing files are merged, never clobbered.
159
251
 
@@ -164,21 +256,32 @@ Flags:
164
256
  --client editor to configure (default: auto-detect)
165
257
  --instructions personal (default, ~/.copilot/copilot-instructions.md) or project
166
258
  --force overwrite existing config
167
- --yes accept all defaults, never prompt`,
259
+ --yes accept all defaults, never prompt
260
+
261
+ Examples:
262
+ open-memex init
263
+ open-memex init --client cursor --yes`,
168
264
  config: `Show config, or set a key.
169
265
 
170
266
  Usage: open-memex config [set <key> <value>]
171
267
 
172
- Example:
268
+ Examples:
269
+ open-memex config
173
270
  open-memex config set sync.autoPull false`,
174
271
  capture: `Preview what the keyword-capture watcher would extract from text.
175
272
 
176
- Usage: open-memex capture --dry-run "text"`,
273
+ Usage: open-memex capture --dry-run "text"
274
+
275
+ Example:
276
+ open-memex capture --dry-run "remember: we deploy on Fridays"`,
177
277
  doctor: `Environment health check: Node version, config source, scope resolution,
178
278
  storage writability, then boots a real MCP server and runs initialize +
179
279
  tools/list against it — all eleven tools must show up.
180
280
 
181
- Usage: open-memex doctor`,
281
+ Usage: open-memex doctor
282
+
283
+ Example:
284
+ open-memex doctor`,
182
285
  };
183
286
  function usage(exitCode = 1) {
184
287
  console.log(`open-memex CLI
@@ -195,6 +298,11 @@ Usage:
195
298
  open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
196
299
  open-memex resolve [id-or-path]
197
300
  open-memex sync-status
301
+ open-memex pull
302
+ open-memex push
303
+ open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
304
+ open-memex import <bundle.tar.gz> [--dry-run]
305
+ open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
198
306
  open-memex submit <id...> [--branch <name>] [--base <branch>]
199
307
  open-memex pr-status [--apply]
200
308
  open-memex reindex
@@ -209,6 +317,9 @@ Usage:
209
317
  open-memex capture --dry-run "text"
210
318
  open-memex doctor
211
319
 
320
+ Every command has its own help with description and examples:
321
+ open-memex <command> --help (or -h)
322
+
212
323
  One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) writes
213
324
  the MCP config for your editor (\`.vscode/mcp.json\`, \`.cursor/mcp.json\`,
214
325
  \`opencode.jsonc\`, or Visual Studio's solution-level \`.mcp.json\`) — no copy-paste
@@ -239,19 +350,51 @@ type: instruction→role split. Always preview with --dry-run first.
239
350
  Run \`open-memex <command> --help\` for details on a single command.`);
240
351
  process.exit(exitCode);
241
352
  }
353
+ /**
354
+ * Build a saveConfig patch for a (possibly dotted) config key, preserving
355
+ * sibling keys already present in the nested object.
356
+ */
357
+ function setConfigPath(key, value) {
358
+ const parts = key.split(".");
359
+ if (parts.length === 1)
360
+ return { [key]: value };
361
+ const cfg = loadConfig();
362
+ const top = parts[0];
363
+ const cur = cfg[top] && typeof cfg[top] === "object"
364
+ ? { ...cfg[top] }
365
+ : {};
366
+ let node = cur;
367
+ for (let i = 1; i < parts.length - 1; i++) {
368
+ const seg = parts[i];
369
+ const nxt = node[seg] && typeof node[seg] === "object"
370
+ ? { ...node[seg] }
371
+ : {};
372
+ node[seg] = nxt;
373
+ node = nxt;
374
+ }
375
+ node[parts[parts.length - 1]] = value;
376
+ return { [top]: cur };
377
+ }
242
378
  function parseFlags(argv) {
243
379
  const out = {};
244
380
  for (let i = 0; i < argv.length; i++) {
245
381
  const a = argv[i];
246
382
  if (a.startsWith("--")) {
247
- const key = a.slice(2);
383
+ const raw = a.slice(2);
384
+ const eq = raw.indexOf("=");
385
+ if (eq >= 0) {
386
+ // D44: accept --key=value as well as --key value (issue #7 — the
387
+ // = form was silently misparsed before, dropping the flag).
388
+ out[raw.slice(0, eq)] = raw.slice(eq + 1);
389
+ continue;
390
+ }
248
391
  const val = argv[i + 1];
249
392
  if (val !== undefined && !val.startsWith("--")) {
250
- out[key] = val;
393
+ out[raw] = val;
251
394
  i++;
252
395
  }
253
396
  else {
254
- out[key] = "true";
397
+ out[raw] = "true";
255
398
  }
256
399
  }
257
400
  }
@@ -354,7 +497,15 @@ async function main() {
354
497
  if (cmd === "migrate" && rest.includes("--to-v2")) {
355
498
  const flags = parseFlags(rest);
356
499
  const dryRun = flags["dry-run"] === "true";
357
- const stats = migrateV2({ dryRun });
500
+ let stats;
501
+ try {
502
+ stats = migrateV2({ dryRun });
503
+ }
504
+ catch (err) {
505
+ // D44: actionable message, not a raw syscall stack (issue #7).
506
+ console.error(`Error: ${err.message}`);
507
+ process.exit(1);
508
+ }
358
509
  console.log(`${dryRun ? "DRY RUN: " : ""}scanned ${stats.scanned} files: ` +
359
510
  `${stats.converted} to convert, ${stats.skippedV2} already v2`);
360
511
  for (const p of stats.plans) {
@@ -364,7 +515,9 @@ async function main() {
364
515
  console.log(` - ${c}`);
365
516
  }
366
517
  if (stats.legacyBackup) {
367
- console.log(`\nlegacy my-o-memory data dir merged; backup kept at:\n ${stats.legacyBackup}`);
518
+ console.log(dryRun
519
+ ? `\nDRY RUN: legacy my-o-memory data dir found — it would be merged and backed up at:\n ${stats.legacyBackup}`
520
+ : `\nlegacy my-o-memory data dir merged; backup kept at:\n ${stats.legacyBackup}`);
368
521
  }
369
522
  if (!dryRun && stats.converted > 0) {
370
523
  console.log(`\nindex schema will rebuild automatically on next run; ` +
@@ -418,7 +571,9 @@ async function main() {
418
571
  }
419
572
  try {
420
573
  const saved = validate(value);
421
- const file = saveConfig({ [key]: saved });
574
+ // Dotted keys (e.g. sync.autoPull) write into the nested config
575
+ // object, preserving sibling keys already on disk.
576
+ const file = saveConfig(setConfigPath(key, saved));
422
577
  console.log(`set ${key} = ${JSON.stringify(saved)} (${file})`);
423
578
  }
424
579
  catch (err) {
@@ -781,6 +936,133 @@ async function main() {
781
936
  }
782
937
  return;
783
938
  }
939
+ // §9 / D12: explicit pull — fetch + fast-forward only, never auto-merge.
940
+ if (cmd === "pull") {
941
+ const { GitProvider } = await import("./providers/git.js");
942
+ const root = projectRoot();
943
+ try {
944
+ const r = new GitProvider().pull(root);
945
+ const stats = syncScope(project.key, "pull");
946
+ if (r.fastForwarded) {
947
+ console.log(`pulled ${r.branch} from ${r.remote}: ${r.before.slice(0, 8)} → ${r.after.slice(0, 8)} (fast-forward)`);
948
+ }
949
+ else {
950
+ console.log(`already up to date: ${r.branch} @ ${r.after.slice(0, 8)}`);
951
+ }
952
+ console.log(`index: +${stats.added} ~${stats.updated} -${stats.removed} (scanned ${stats.scanned})`);
953
+ }
954
+ catch (e) {
955
+ console.error(`pull failed: ${e.message}`);
956
+ process.exit(2);
957
+ }
958
+ return;
959
+ }
960
+ // Explicit push — open-memex never pushes on its own (D36).
961
+ if (cmd === "push") {
962
+ const { GitProvider } = await import("./providers/git.js");
963
+ const root = projectRoot();
964
+ try {
965
+ const r = new GitProvider().push(root);
966
+ syncScope(project.key, "push");
967
+ console.log(`pushed ${r.branch} to ${r.remote} @ ${r.head.slice(0, 8)}`);
968
+ }
969
+ catch (e) {
970
+ console.error(`push failed: ${e.message}`);
971
+ process.exit(2);
972
+ }
973
+ return;
974
+ }
975
+ // §9 / D40: portable export bundle (markdown + manifest).
976
+ if (cmd === "export") {
977
+ const { exportMemories } = await import("./export.js");
978
+ const flags = parseFlags(rest);
979
+ const all = flags["all"] === "true" || flags["a"] === "true" || rest.includes("--all") || rest.includes("-a");
980
+ const scopeFlag = flags["scope"] ?? "project";
981
+ // parseFlags only handles `--` flags; `-o <file>` is picked up here.
982
+ const oIdx = rest.findIndex((a) => a === "-o");
983
+ const outFile = flags["o"] ?? flags["output"] ?? (oIdx >= 0 ? rest[oIdx + 1] : undefined);
984
+ const scopeKeys = scopeFlag === "both"
985
+ ? [project.key, PERSONAL_SCOPE.key]
986
+ : scopeFlag === "personal"
987
+ ? [PERSONAL_SCOPE.key]
988
+ : [project.key];
989
+ try {
990
+ const r = exportMemories({
991
+ scopeKeys,
992
+ type: flags["type"],
993
+ tag: flags["tag"],
994
+ includePrivate: all,
995
+ outFile,
996
+ });
997
+ console.log(`exported ${r.exported} memories → ${r.file}`);
998
+ if (!r.includePrivate && r.skippedPrivate > 0) {
999
+ console.log(`skipped ${r.skippedPrivate} private memories (use --all to include them)`);
1000
+ }
1001
+ }
1002
+ catch (e) {
1003
+ console.error(`export failed: ${e.message}`);
1004
+ process.exit(2);
1005
+ }
1006
+ return;
1007
+ }
1008
+ if (cmd === "import") {
1009
+ const { importBundle } = await import("./export.js");
1010
+ const flags = parseFlags(rest);
1011
+ const bundle = positionalArgs(rest)[0];
1012
+ if (!bundle) {
1013
+ console.error(`usage: open-memex import <bundle.tar.gz> [--dry-run]`);
1014
+ process.exit(2);
1015
+ }
1016
+ const dryRun = flags["dry-run"] === "true";
1017
+ try {
1018
+ const r = importBundle(bundle, { projectScopeKey: project.key, dryRun });
1019
+ if (!dryRun) {
1020
+ syncScope(project.key, "cli");
1021
+ syncScope(PERSONAL_SCOPE.key, "cli");
1022
+ }
1023
+ console.log(`${dryRun ? "DRY RUN: " : ""}imported ${r.imported}, skipped ${r.skippedIdentical} identical`);
1024
+ for (const c of r.skippedConflict) {
1025
+ console.log(` conflict (kept existing): ${c.id} from ${c.file}`);
1026
+ }
1027
+ }
1028
+ catch (e) {
1029
+ console.error(`import failed: ${e.message}`);
1030
+ process.exit(2);
1031
+ }
1032
+ return;
1033
+ }
1034
+ // Phase 3: distill project memories into a proposed AGENTS.md snippet.
1035
+ if (cmd === "distill-agents") {
1036
+ const { distillAgentsMarkdown } = await import("./distill-agents.js");
1037
+ const flags = parseFlags(rest);
1038
+ const scopeFlag = flags["scope"] ?? "project";
1039
+ const scopeKeys = scopeFlag === "personal" ? [PERSONAL_SCOPE.key] : [project.key];
1040
+ const types = flags["type"]
1041
+ ? flags["type"].split(",").map((t) => t.trim()).filter(Boolean)
1042
+ : undefined;
1043
+ const limit = flags["limit"] ? parseInt(flags["limit"], 10) : undefined;
1044
+ const oIdx = rest.findIndex((a) => a === "-o");
1045
+ const outFile = oIdx >= 0 ? rest[oIdx + 1] : undefined;
1046
+ try {
1047
+ const md = distillAgentsMarkdown({ scopeKeys, types, limit });
1048
+ if (!md) {
1049
+ console.log("no distillable memories found (decisions, constraints, lessons, gotchas, howtos)");
1050
+ return;
1051
+ }
1052
+ if (outFile) {
1053
+ fs.writeFileSync(outFile, md, "utf8");
1054
+ console.log(`wrote proposed AGENTS.md snippet → ${outFile} (review and merge by hand)`);
1055
+ }
1056
+ else {
1057
+ console.log(md);
1058
+ }
1059
+ }
1060
+ catch (e) {
1061
+ console.error(`distill-agents failed: ${e.message}`);
1062
+ process.exit(2);
1063
+ }
1064
+ return;
1065
+ }
784
1066
  if (cmd === "pr-status") {
785
1067
  const flags = parseFlags(rest);
786
1068
  try {
@@ -852,6 +1134,10 @@ async function main() {
852
1134
  entries.push({ key: name, files, marker });
853
1135
  }
854
1136
  entries.sort((a, b) => b.files - a.files);
1137
+ if (entries.length === 0) {
1138
+ console.log("(no scopes with memories yet — add one with `open-memex add`)");
1139
+ return;
1140
+ }
855
1141
  for (const e of entries) {
856
1142
  console.log(` ${e.files.toString().padStart(4)} ${e.key}${e.marker}`);
857
1143
  }
package/dist/config.js CHANGED
@@ -34,6 +34,7 @@ export const DEFAULT_CONFIG = {
34
34
  redactPatterns: [],
35
35
  logLevel: "info",
36
36
  memoryDir: ".ai/open-memex",
37
+ sync: { autoPull: false },
37
38
  };
38
39
  function stripJsonComments(raw) {
39
40
  // Line comments
@@ -79,6 +80,7 @@ export const SETTABLE_KEYS = {
79
80
  injectOnFirstTurn: toBool,
80
81
  keywordCaptureEnabled: toBool,
81
82
  memoryDir: toRelativeDir,
83
+ "sync.autoPull": toBool,
82
84
  logLevel: (v) => {
83
85
  if (v !== "info" && v !== "debug")
84
86
  throw new Error('must be "info" or "debug"');
@@ -0,0 +1,62 @@
1
+ /**
2
+ * distill-to-AGENTS.md assist (Phase 3).
3
+ *
4
+ * Turns distilled project memories (decisions, lessons, gotchas, howtos,
5
+ * constraints) into a proposed AGENTS.md snippet. ASSIST only: it prints
6
+ * (or writes with -o) a markdown section for the human to review and merge
7
+ * by hand — open-memex never rewrites your AGENTS.md on its own.
8
+ */
9
+ import { db } from "./store/db.js";
10
+ const DEFAULT_TYPES = ["decision", "constraint", "lesson", "gotcha", "howto"];
11
+ export function distillAgentsMarkdown(opts) {
12
+ const types = opts.types?.length ? opts.types : DEFAULT_TYPES;
13
+ const limit = Math.max(1, Math.min(opts.limit ?? 30, 200));
14
+ const sql = `
15
+ SELECT id, type, tags, content, updated_at FROM memories
16
+ WHERE scope_key IN (${opts.scopeKeys.map(() => "?").join(",")})
17
+ AND type IN (${types.map(() => "?").join(",")})
18
+ AND status = 'active'
19
+ ORDER BY updated_at DESC
20
+ LIMIT ?`;
21
+ const rows = db()
22
+ .prepare(sql)
23
+ .all(...opts.scopeKeys, ...types, limit);
24
+ if (rows.length === 0)
25
+ return "";
26
+ const date = new Date().toISOString().slice(0, 10);
27
+ const lines = [
28
+ `## Learned (distilled from open-memex, ${date})`,
29
+ ``,
30
+ `<!-- Proposed by \`open-memex distill-agents\`. Review each line, keep`,
31
+ `what's true, delete the rest, then merge into this file by hand. -->`,
32
+ ``,
33
+ ];
34
+ // Group by type, preserving recency within each group.
35
+ const byType = new Map();
36
+ for (const r of rows) {
37
+ const g = byType.get(r.type) ?? [];
38
+ g.push(r);
39
+ byType.set(r.type, g);
40
+ }
41
+ for (const t of types) {
42
+ const g = byType.get(t);
43
+ if (!g?.length)
44
+ continue;
45
+ lines.push(`### ${t}`);
46
+ lines.push(``);
47
+ for (const r of g) {
48
+ const first = r.content.split("\n").map((l) => l.trim()).find((l) => l) ?? "";
49
+ const snippet = first.length > 160 ? first.slice(0, 160) + "…" : first;
50
+ lines.push(`- ${snippet} \`[${r.id.slice(0, 8)}]\``);
51
+ }
52
+ lines.push(``);
53
+ }
54
+ // D43 — §3.5 memory-hygiene footer (double insurance for opencode users,
55
+ // who never see the MCP handshake / init instructions): teach the agent
56
+ // reading this AGENTS.md to propose distilled captures at checkpoints.
57
+ lines.push(`### Memory hygiene (open-memex)`);
58
+ lines.push(``);
59
+ lines.push(`- At checkpoints (session start, end of a work chunk, after the user commits),`, ` distill the session: propose 1–3 short memories capturing the useful`, ` conclusion — what was learned or decided, how an issue was resolved, what`, ` to avoid, where the authoritative doc lives — not the raw transcript.`, ` Save nothing without user approval.`, `- If the knowledge already lives in project docs, save a \`reference\` memory`, ` pointing at the doc instead of copying it.`);
60
+ lines.push(``);
61
+ return lines.join("\n");
62
+ }