open-memex 0.4.0-alpha.7 → 0.4.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/AGENTS.md +5 -0
- package/README.md +191 -21
- package/README.zh-CN.md +164 -17
- package/dist/cli.js +282 -17
- package/dist/config.js +2 -0
- package/dist/distill-agents.js +62 -0
- package/dist/export.js +157 -0
- package/dist/mcp.js +14 -0
- package/dist/providers/git.js +142 -0
- package/docs/CURATOR.md +59 -0
- package/docs/TEST-PLAN.md +72 -0
- package/docs/V2-DESIGN.md +11 -2
- package/package.json +1 -1
- package/scripts/test-full.ts +345 -0
- package/src/cli.ts +295 -17
- package/src/config.ts +10 -0
- package/src/distill-agents.ts +85 -0
- package/src/export.ts +206 -0
- package/src/mcp.ts +16 -0
- package/src/providers/git.ts +191 -0
- package/src/store/sync.ts +1 -1
package/src/cli.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type Frontmatter,
|
|
19
19
|
} from "./store/markdown.ts";
|
|
20
20
|
import { loadConfig } from "./config.ts";
|
|
21
|
-
import { paths } from "./paths.ts";
|
|
21
|
+
import { paths, projectRoot } from "./paths.ts";
|
|
22
22
|
import { redact } from "./redact.ts";
|
|
23
23
|
import { resolveMcpCommand } from "./init.ts";
|
|
24
24
|
import fs from "node:fs";
|
|
@@ -30,7 +30,10 @@ import { fileURLToPath } from "node:url";
|
|
|
30
30
|
const COMMAND_HELP: Record<string, string> = {
|
|
31
31
|
where: `Show which project scope the current directory resolves to, and where its data lives.
|
|
32
32
|
|
|
33
|
-
Usage: open-memex where
|
|
33
|
+
Usage: open-memex where
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
open-memex where`,
|
|
34
37
|
|
|
35
38
|
list: `List memories in a scope, newest first.
|
|
36
39
|
|
|
@@ -71,15 +74,24 @@ Example:
|
|
|
71
74
|
|
|
72
75
|
supersede: `Replace a memory with a newer version. The old one is kept as history.
|
|
73
76
|
|
|
74
|
-
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
77
|
+
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
open-memex supersede 01ABC "We deploy on Thursdays now"`,
|
|
75
81
|
|
|
76
82
|
status: `Change a memory's lifecycle status.
|
|
77
83
|
|
|
78
|
-
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
84
|
+
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
open-memex status 01ABC deprecated`,
|
|
79
88
|
|
|
80
89
|
forget: `Delete a memory by id.
|
|
81
90
|
|
|
82
|
-
Usage: open-memex forget <id
|
|
91
|
+
Usage: open-memex forget <id>
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
open-memex forget 01ABC`,
|
|
83
95
|
|
|
84
96
|
propose: `Copy personal memories into the project outbox as review drafts.
|
|
85
97
|
The personal originals stay put. Nothing enters git at this step.
|
|
@@ -116,13 +128,76 @@ Usage: open-memex resolve [id-or-path]
|
|
|
116
128
|
|
|
117
129
|
With no argument, lists conflicts. With an id or file path, shows the
|
|
118
130
|
3-way merge (base / outbox / repo) so you can resolve it by hand.
|
|
119
|
-
Conflicts are never auto-resolved
|
|
131
|
+
Conflicts are never auto-resolved.
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
open-memex resolve
|
|
135
|
+
open-memex resolve 01ABC`,
|
|
120
136
|
|
|
121
137
|
"sync-status": `Show the project memory sync pipeline: when the index last synced
|
|
122
138
|
and what triggered it, drafts waiting in the outbox (appdata), memories in the
|
|
123
139
|
repo awaiting review or published, and repo files not yet committed.
|
|
124
140
|
|
|
125
|
-
Usage: open-memex sync-status
|
|
141
|
+
Usage: open-memex sync-status
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
open-memex sync-status`,
|
|
145
|
+
|
|
146
|
+
pull: `Pull shared project memories from the git remote: fetch + fast-forward
|
|
147
|
+
only. Never auto-merges — a diverged branch fails with a clear message and is
|
|
148
|
+
left for you to resolve by hand. On success the local index re-syncs.
|
|
149
|
+
|
|
150
|
+
Usage: open-memex pull
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
open-memex pull`,
|
|
154
|
+
|
|
155
|
+
push: `Push the current branch (with its submitted memories) to the git
|
|
156
|
+
remote. Explicit only — open-memex never pushes on its own.
|
|
157
|
+
|
|
158
|
+
Usage: open-memex push
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
open-memex push`,
|
|
162
|
+
|
|
163
|
+
export: `Export memories to a portable .tar.gz bundle (markdown source of
|
|
164
|
+
truth + manifest.json) for moving to another machine or another app.
|
|
165
|
+
Excludes visibility:private memories by default; --all includes everything.
|
|
166
|
+
|
|
167
|
+
Usage: open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
|
|
168
|
+
|
|
169
|
+
Flags:
|
|
170
|
+
--scope project (default), personal, or both
|
|
171
|
+
--type filter by memory type
|
|
172
|
+
--tag filter by tag
|
|
173
|
+
--all, -a include private memories (full migration)
|
|
174
|
+
-o output file (default: ./open-memex-export-<timestamp>.tar.gz)
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
open-memex export -o backup.tar.gz
|
|
178
|
+
open-memex export --scope both --all -o full-migration.tar.gz`,
|
|
179
|
+
|
|
180
|
+
import: `Import a bundle created by \`open-memex export\`. Personal memories
|
|
181
|
+
go to the personal dir; project memories are re-keyed to the current project
|
|
182
|
+
and land in the outbox as drafts. Existing identical memories are skipped;
|
|
183
|
+
conflicting ids are reported, never overwritten.
|
|
184
|
+
|
|
185
|
+
Usage: open-memex import <bundle.tar.gz> [--dry-run]
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
open-memex import backup.tar.gz --dry-run
|
|
189
|
+
open-memex import backup.tar.gz`,
|
|
190
|
+
|
|
191
|
+
"distill-agents": `Propose an AGENTS.md snippet distilled from project
|
|
192
|
+
memories (decisions, constraints, lessons, gotchas, howtos). Prints markdown
|
|
193
|
+
to stdout, or writes it with -o. Review and merge by hand — open-memex never
|
|
194
|
+
rewrites your AGENTS.md on its own.
|
|
195
|
+
|
|
196
|
+
Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
open-memex distill-agents
|
|
200
|
+
open-memex distill-agents --type decision,gotcha -o agents-snippet.md`,
|
|
126
201
|
|
|
127
202
|
submit: `Move outbox drafts into the repo for review: copies the drafts into
|
|
128
203
|
the repo memory dir as proposed (a local-approved copy keeps its approval),
|
|
@@ -148,15 +223,25 @@ overwritten. Report-only by default.
|
|
|
148
223
|
Usage: open-memex pr-status [--apply]
|
|
149
224
|
|
|
150
225
|
Flags:
|
|
151
|
-
--apply write the transitions locally (still never pushes)
|
|
226
|
+
--apply write the transitions locally (still never pushes)
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
open-memex pr-status
|
|
230
|
+
open-memex pr-status --apply`,
|
|
152
231
|
|
|
153
232
|
reindex: `Rebuild the SQLite index from the markdown files.
|
|
154
233
|
|
|
155
|
-
Usage: open-memex reindex
|
|
234
|
+
Usage: open-memex reindex
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
open-memex reindex`,
|
|
156
238
|
|
|
157
239
|
scopes: `List the known scopes (personal + project).
|
|
158
240
|
|
|
159
|
-
Usage: open-memex scopes
|
|
241
|
+
Usage: open-memex scopes
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
open-memex scopes`,
|
|
160
245
|
|
|
161
246
|
migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
|
|
162
247
|
|
|
@@ -169,14 +254,22 @@ Flags:
|
|
|
169
254
|
--on-conflict newer (default), overwrite, or skip
|
|
170
255
|
--to-v2 convert a legacy my-o-memory data dir to the v2 layout
|
|
171
256
|
|
|
172
|
-
Always preview with --dry-run first; nothing moves without confirmation
|
|
257
|
+
Always preview with --dry-run first; nothing moves without confirmation.
|
|
258
|
+
|
|
259
|
+
Examples:
|
|
260
|
+
open-memex migrate --dry-run
|
|
261
|
+
open-memex migrate --from personal --to project --dry-run`,
|
|
173
262
|
|
|
174
263
|
mcp: `Start the stdio MCP server (the same server editors connect to).
|
|
175
264
|
|
|
176
265
|
Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
177
266
|
|
|
178
267
|
Flags:
|
|
179
|
-
--print-config print the MCP client config instead of starting the server
|
|
268
|
+
--print-config print the MCP client config instead of starting the server
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
open-memex mcp
|
|
272
|
+
open-memex mcp --print-config vscode`,
|
|
180
273
|
|
|
181
274
|
init: `One-command project setup: writes the MCP config for your editor and the
|
|
182
275
|
agent memory instructions. Existing files are merged, never clobbered.
|
|
@@ -188,24 +281,35 @@ Flags:
|
|
|
188
281
|
--client editor to configure (default: auto-detect)
|
|
189
282
|
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
190
283
|
--force overwrite existing config
|
|
191
|
-
--yes accept all defaults, never prompt
|
|
284
|
+
--yes accept all defaults, never prompt
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
open-memex init
|
|
288
|
+
open-memex init --client cursor --yes`,
|
|
192
289
|
|
|
193
290
|
config: `Show config, or set a key.
|
|
194
291
|
|
|
195
292
|
Usage: open-memex config [set <key> <value>]
|
|
196
293
|
|
|
197
|
-
|
|
294
|
+
Examples:
|
|
295
|
+
open-memex config
|
|
198
296
|
open-memex config set sync.autoPull false`,
|
|
199
297
|
|
|
200
298
|
capture: `Preview what the keyword-capture watcher would extract from text.
|
|
201
299
|
|
|
202
|
-
Usage: open-memex capture --dry-run "text"
|
|
300
|
+
Usage: open-memex capture --dry-run "text"
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
open-memex capture --dry-run "remember: we deploy on Fridays"`,
|
|
203
304
|
|
|
204
305
|
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
205
306
|
storage writability, then boots a real MCP server and runs initialize +
|
|
206
307
|
tools/list against it — all eleven tools must show up.
|
|
207
308
|
|
|
208
|
-
Usage: open-memex doctor
|
|
309
|
+
Usage: open-memex doctor
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
open-memex doctor`,
|
|
209
313
|
};
|
|
210
314
|
|
|
211
315
|
function usage(exitCode = 1): never {
|
|
@@ -223,6 +327,11 @@ Usage:
|
|
|
223
327
|
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
224
328
|
open-memex resolve [id-or-path]
|
|
225
329
|
open-memex sync-status
|
|
330
|
+
open-memex pull
|
|
331
|
+
open-memex push
|
|
332
|
+
open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
|
|
333
|
+
open-memex import <bundle.tar.gz> [--dry-run]
|
|
334
|
+
open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
226
335
|
open-memex submit <id...> [--branch <name>] [--base <branch>]
|
|
227
336
|
open-memex pr-status [--apply]
|
|
228
337
|
open-memex reindex
|
|
@@ -237,6 +346,9 @@ Usage:
|
|
|
237
346
|
open-memex capture --dry-run "text"
|
|
238
347
|
open-memex doctor
|
|
239
348
|
|
|
349
|
+
Every command has its own help with description and examples:
|
|
350
|
+
open-memex <command> --help (or -h)
|
|
351
|
+
|
|
240
352
|
One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) writes
|
|
241
353
|
the MCP config for your editor (\`.vscode/mcp.json\`, \`.cursor/mcp.json\`,
|
|
242
354
|
\`opencode.jsonc\`, or Visual Studio's solution-level \`.mcp.json\`) — no copy-paste
|
|
@@ -268,6 +380,33 @@ Run \`open-memex <command> --help\` for details on a single command.`);
|
|
|
268
380
|
process.exit(exitCode);
|
|
269
381
|
}
|
|
270
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Build a saveConfig patch for a (possibly dotted) config key, preserving
|
|
385
|
+
* sibling keys already present in the nested object.
|
|
386
|
+
*/
|
|
387
|
+
function setConfigPath(key: string, value: unknown): Record<string, unknown> {
|
|
388
|
+
const parts = key.split(".");
|
|
389
|
+
if (parts.length === 1) return { [key]: value };
|
|
390
|
+
const cfg = loadConfig() as unknown as Record<string, unknown>;
|
|
391
|
+
const top = parts[0]!;
|
|
392
|
+
const cur =
|
|
393
|
+
cfg[top] && typeof cfg[top] === "object"
|
|
394
|
+
? { ...(cfg[top] as Record<string, unknown>) }
|
|
395
|
+
: {};
|
|
396
|
+
let node: Record<string, unknown> = cur;
|
|
397
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
398
|
+
const seg = parts[i]!;
|
|
399
|
+
const nxt =
|
|
400
|
+
node[seg] && typeof node[seg] === "object"
|
|
401
|
+
? { ...(node[seg] as Record<string, unknown>) }
|
|
402
|
+
: {};
|
|
403
|
+
node[seg] = nxt;
|
|
404
|
+
node = nxt;
|
|
405
|
+
}
|
|
406
|
+
node[parts[parts.length - 1]!] = value;
|
|
407
|
+
return { [top]: cur };
|
|
408
|
+
}
|
|
409
|
+
|
|
271
410
|
function parseFlags(argv: string[]): Record<string, string> {
|
|
272
411
|
const out: Record<string, string> = {};
|
|
273
412
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -483,7 +622,9 @@ async function main() {
|
|
|
483
622
|
}
|
|
484
623
|
try {
|
|
485
624
|
const saved = validate(value);
|
|
486
|
-
|
|
625
|
+
// Dotted keys (e.g. sync.autoPull) write into the nested config
|
|
626
|
+
// object, preserving sibling keys already on disk.
|
|
627
|
+
const file = saveConfig(setConfigPath(key!, saved));
|
|
487
628
|
console.log(`set ${key} = ${JSON.stringify(saved)} (${file})`);
|
|
488
629
|
} catch (err) {
|
|
489
630
|
console.error(`invalid value for ${key}: ${(err as Error).message}`);
|
|
@@ -855,6 +996,139 @@ function positionalArgs(argv: string[]): string[] {
|
|
|
855
996
|
return;
|
|
856
997
|
}
|
|
857
998
|
|
|
999
|
+
// §9 / D12: explicit pull — fetch + fast-forward only, never auto-merge.
|
|
1000
|
+
if (cmd === "pull") {
|
|
1001
|
+
const { GitProvider } = await import("./providers/git.ts");
|
|
1002
|
+
const root = projectRoot();
|
|
1003
|
+
try {
|
|
1004
|
+
const r = new GitProvider().pull(root);
|
|
1005
|
+
const stats = syncScope(project.key, "pull");
|
|
1006
|
+
if (r.fastForwarded) {
|
|
1007
|
+
console.log(
|
|
1008
|
+
`pulled ${r.branch} from ${r.remote}: ${r.before.slice(0, 8)} → ${r.after.slice(0, 8)} (fast-forward)`,
|
|
1009
|
+
);
|
|
1010
|
+
} else {
|
|
1011
|
+
console.log(`already up to date: ${r.branch} @ ${r.after.slice(0, 8)}`);
|
|
1012
|
+
}
|
|
1013
|
+
console.log(
|
|
1014
|
+
`index: +${stats.added} ~${stats.updated} -${stats.removed} (scanned ${stats.scanned})`,
|
|
1015
|
+
);
|
|
1016
|
+
} catch (e) {
|
|
1017
|
+
console.error(`pull failed: ${(e as Error).message}`);
|
|
1018
|
+
process.exit(2);
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Explicit push — open-memex never pushes on its own (D36).
|
|
1024
|
+
if (cmd === "push") {
|
|
1025
|
+
const { GitProvider } = await import("./providers/git.ts");
|
|
1026
|
+
const root = projectRoot();
|
|
1027
|
+
try {
|
|
1028
|
+
const r = new GitProvider().push(root);
|
|
1029
|
+
syncScope(project.key, "push");
|
|
1030
|
+
console.log(`pushed ${r.branch} to ${r.remote} @ ${r.head.slice(0, 8)}`);
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
console.error(`push failed: ${(e as Error).message}`);
|
|
1033
|
+
process.exit(2);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// §9 / D40: portable export bundle (markdown + manifest).
|
|
1039
|
+
if (cmd === "export") {
|
|
1040
|
+
const { exportMemories } = await import("./export.ts");
|
|
1041
|
+
const flags = parseFlags(rest);
|
|
1042
|
+
const all = flags["all"] === "true" || flags["a"] === "true" || rest.includes("--all") || rest.includes("-a");
|
|
1043
|
+
const scopeFlag = flags["scope"] ?? "project";
|
|
1044
|
+
// parseFlags only handles `--` flags; `-o <file>` is picked up here.
|
|
1045
|
+
const oIdx = rest.findIndex((a) => a === "-o");
|
|
1046
|
+
const outFile = flags["o"] ?? flags["output"] ?? (oIdx >= 0 ? rest[oIdx + 1] : undefined);
|
|
1047
|
+
const scopeKeys =
|
|
1048
|
+
scopeFlag === "both"
|
|
1049
|
+
? [project.key, PERSONAL_SCOPE.key]
|
|
1050
|
+
: scopeFlag === "personal"
|
|
1051
|
+
? [PERSONAL_SCOPE.key]
|
|
1052
|
+
: [project.key];
|
|
1053
|
+
try {
|
|
1054
|
+
const r = exportMemories({
|
|
1055
|
+
scopeKeys,
|
|
1056
|
+
type: flags["type"],
|
|
1057
|
+
tag: flags["tag"],
|
|
1058
|
+
includePrivate: all,
|
|
1059
|
+
outFile,
|
|
1060
|
+
});
|
|
1061
|
+
console.log(`exported ${r.exported} memories → ${r.file}`);
|
|
1062
|
+
if (!r.includePrivate && r.skippedPrivate > 0) {
|
|
1063
|
+
console.log(`skipped ${r.skippedPrivate} private memories (use --all to include them)`);
|
|
1064
|
+
}
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
console.error(`export failed: ${(e as Error).message}`);
|
|
1067
|
+
process.exit(2);
|
|
1068
|
+
}
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (cmd === "import") {
|
|
1073
|
+
const { importBundle } = await import("./export.ts");
|
|
1074
|
+
const flags = parseFlags(rest);
|
|
1075
|
+
const bundle = positionalArgs(rest)[0];
|
|
1076
|
+
if (!bundle) {
|
|
1077
|
+
console.error(`usage: open-memex import <bundle.tar.gz> [--dry-run]`);
|
|
1078
|
+
process.exit(2);
|
|
1079
|
+
}
|
|
1080
|
+
const dryRun = flags["dry-run"] === "true";
|
|
1081
|
+
try {
|
|
1082
|
+
const r = importBundle(bundle, { projectScopeKey: project.key, dryRun });
|
|
1083
|
+
if (!dryRun) {
|
|
1084
|
+
syncScope(project.key, "cli");
|
|
1085
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
1086
|
+
}
|
|
1087
|
+
console.log(
|
|
1088
|
+
`${dryRun ? "DRY RUN: " : ""}imported ${r.imported}, skipped ${r.skippedIdentical} identical`,
|
|
1089
|
+
);
|
|
1090
|
+
for (const c of r.skippedConflict) {
|
|
1091
|
+
console.log(` conflict (kept existing): ${c.id} from ${c.file}`);
|
|
1092
|
+
}
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
console.error(`import failed: ${(e as Error).message}`);
|
|
1095
|
+
process.exit(2);
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Phase 3: distill project memories into a proposed AGENTS.md snippet.
|
|
1101
|
+
if (cmd === "distill-agents") {
|
|
1102
|
+
const { distillAgentsMarkdown } = await import("./distill-agents.ts");
|
|
1103
|
+
const flags = parseFlags(rest);
|
|
1104
|
+
const scopeFlag = flags["scope"] ?? "project";
|
|
1105
|
+
const scopeKeys =
|
|
1106
|
+
scopeFlag === "personal" ? [PERSONAL_SCOPE.key] : [project.key];
|
|
1107
|
+
const types = flags["type"]
|
|
1108
|
+
? flags["type"].split(",").map((t) => t.trim()).filter(Boolean)
|
|
1109
|
+
: undefined;
|
|
1110
|
+
const limit = flags["limit"] ? parseInt(flags["limit"], 10) : undefined;
|
|
1111
|
+
const oIdx = rest.findIndex((a) => a === "-o");
|
|
1112
|
+
const outFile = oIdx >= 0 ? rest[oIdx + 1] : undefined;
|
|
1113
|
+
try {
|
|
1114
|
+
const md = distillAgentsMarkdown({ scopeKeys, types, limit });
|
|
1115
|
+
if (!md) {
|
|
1116
|
+
console.log("no distillable memories found (decisions, constraints, lessons, gotchas, howtos)");
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (outFile) {
|
|
1120
|
+
fs.writeFileSync(outFile, md, "utf8");
|
|
1121
|
+
console.log(`wrote proposed AGENTS.md snippet → ${outFile} (review and merge by hand)`);
|
|
1122
|
+
} else {
|
|
1123
|
+
console.log(md);
|
|
1124
|
+
}
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
console.error(`distill-agents failed: ${(e as Error).message}`);
|
|
1127
|
+
process.exit(2);
|
|
1128
|
+
}
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
858
1132
|
if (cmd === "pr-status") {
|
|
859
1133
|
const flags = parseFlags(rest);
|
|
860
1134
|
try {
|
|
@@ -923,6 +1197,10 @@ function positionalArgs(argv: string[]): string[] {
|
|
|
923
1197
|
entries.push({ key: name, files, marker });
|
|
924
1198
|
}
|
|
925
1199
|
entries.sort((a, b) => b.files - a.files);
|
|
1200
|
+
if (entries.length === 0) {
|
|
1201
|
+
console.log("(no scopes with memories yet — add one with `open-memex add`)");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
926
1204
|
for (const e of entries) {
|
|
927
1205
|
console.log(` ${e.files.toString().padStart(4)} ${e.key}${e.marker}`);
|
|
928
1206
|
}
|
package/src/config.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface MyOMemoryConfig {
|
|
|
19
19
|
* memories can never escape the repo.
|
|
20
20
|
*/
|
|
21
21
|
memoryDir: string;
|
|
22
|
+
/**
|
|
23
|
+
* Sync behavior (§9, D12). Pulls are explicit by default; session start
|
|
24
|
+
* never touches the network unless autoPull is true — and even then a
|
|
25
|
+
* failed pull never blocks the session.
|
|
26
|
+
*/
|
|
27
|
+
sync: {
|
|
28
|
+
autoPull: boolean;
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export const DEFAULT_CONFIG: MyOMemoryConfig = {
|
|
@@ -54,6 +62,7 @@ export const DEFAULT_CONFIG: MyOMemoryConfig = {
|
|
|
54
62
|
redactPatterns: [],
|
|
55
63
|
logLevel: "info",
|
|
56
64
|
memoryDir: ".ai/open-memex",
|
|
65
|
+
sync: { autoPull: false },
|
|
57
66
|
};
|
|
58
67
|
|
|
59
68
|
function stripJsonComments(raw: string): string {
|
|
@@ -102,6 +111,7 @@ export const SETTABLE_KEYS: Record<string, (v: unknown) => unknown> = {
|
|
|
102
111
|
injectOnFirstTurn: toBool,
|
|
103
112
|
keywordCaptureEnabled: toBool,
|
|
104
113
|
memoryDir: toRelativeDir,
|
|
114
|
+
"sync.autoPull": toBool,
|
|
105
115
|
logLevel: (v) => {
|
|
106
116
|
if (v !== "info" && v !== "debug") throw new Error('must be "info" or "debug"');
|
|
107
117
|
return v;
|
|
@@ -0,0 +1,85 @@
|
|
|
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.ts";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TYPES = ["decision", "constraint", "lesson", "gotcha", "howto"];
|
|
12
|
+
|
|
13
|
+
export interface DistillAgentsOptions {
|
|
14
|
+
scopeKeys: string[];
|
|
15
|
+
types?: string[];
|
|
16
|
+
limit?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Row {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
tags: string;
|
|
23
|
+
content: string;
|
|
24
|
+
updated_at: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function distillAgentsMarkdown(opts: DistillAgentsOptions): string {
|
|
28
|
+
const types = opts.types?.length ? opts.types : DEFAULT_TYPES;
|
|
29
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 30, 200));
|
|
30
|
+
const sql = `
|
|
31
|
+
SELECT id, type, tags, content, updated_at FROM memories
|
|
32
|
+
WHERE scope_key IN (${opts.scopeKeys.map(() => "?").join(",")})
|
|
33
|
+
AND type IN (${types.map(() => "?").join(",")})
|
|
34
|
+
AND status = 'active'
|
|
35
|
+
ORDER BY updated_at DESC
|
|
36
|
+
LIMIT ?`;
|
|
37
|
+
const rows = db()
|
|
38
|
+
.prepare(sql)
|
|
39
|
+
.all(...(opts.scopeKeys as any[]), ...(types as any[]), limit) as Row[];
|
|
40
|
+
|
|
41
|
+
if (rows.length === 0) return "";
|
|
42
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
43
|
+
const lines: string[] = [
|
|
44
|
+
`## Learned (distilled from open-memex, ${date})`,
|
|
45
|
+
``,
|
|
46
|
+
`<!-- Proposed by \`open-memex distill-agents\`. Review each line, keep`,
|
|
47
|
+
`what's true, delete the rest, then merge into this file by hand. -->`,
|
|
48
|
+
``,
|
|
49
|
+
];
|
|
50
|
+
// Group by type, preserving recency within each group.
|
|
51
|
+
const byType = new Map<string, Row[]>();
|
|
52
|
+
for (const r of rows) {
|
|
53
|
+
const g = byType.get(r.type) ?? [];
|
|
54
|
+
g.push(r);
|
|
55
|
+
byType.set(r.type, g);
|
|
56
|
+
}
|
|
57
|
+
for (const t of types) {
|
|
58
|
+
const g = byType.get(t);
|
|
59
|
+
if (!g?.length) continue;
|
|
60
|
+
lines.push(`### ${t}`);
|
|
61
|
+
lines.push(``);
|
|
62
|
+
for (const r of g) {
|
|
63
|
+
const first = r.content.split("\n").map((l) => l.trim()).find((l) => l) ?? "";
|
|
64
|
+
const snippet = first.length > 160 ? first.slice(0, 160) + "…" : first;
|
|
65
|
+
lines.push(`- ${snippet} \`[${r.id.slice(0, 8)}]\``);
|
|
66
|
+
}
|
|
67
|
+
lines.push(``);
|
|
68
|
+
}
|
|
69
|
+
// D43 — §3.5 memory-hygiene footer (double insurance for opencode users,
|
|
70
|
+
// who never see the MCP handshake / init instructions): teach the agent
|
|
71
|
+
// reading this AGENTS.md to propose distilled captures at checkpoints.
|
|
72
|
+
lines.push(`### Memory hygiene (open-memex)`);
|
|
73
|
+
lines.push(``);
|
|
74
|
+
lines.push(
|
|
75
|
+
`- At checkpoints (session start, end of a work chunk, after the user commits),`,
|
|
76
|
+
` distill the session: propose 1–3 short memories capturing the useful`,
|
|
77
|
+
` conclusion — what was learned or decided, how an issue was resolved, what`,
|
|
78
|
+
` to avoid, where the authoritative doc lives — not the raw transcript.`,
|
|
79
|
+
` Save nothing without user approval.`,
|
|
80
|
+
`- If the knowledge already lives in project docs, save a \`reference\` memory`,
|
|
81
|
+
` pointing at the doc instead of copying it.`,
|
|
82
|
+
);
|
|
83
|
+
lines.push(``);
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|