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/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]
|
|
@@ -147,13 +227,21 @@ Flags:
|
|
|
147
227
|
--on-conflict newer (default), overwrite, or skip
|
|
148
228
|
--to-v2 convert a legacy my-o-memory data dir to the v2 layout
|
|
149
229
|
|
|
150
|
-
Always preview with --dry-run first; nothing moves without confirmation
|
|
230
|
+
Always preview with --dry-run first; nothing moves without confirmation.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
open-memex migrate --dry-run
|
|
234
|
+
open-memex migrate --from personal --to project --dry-run`,
|
|
151
235
|
mcp: `Start the stdio MCP server (the same server editors connect to).
|
|
152
236
|
|
|
153
237
|
Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
154
238
|
|
|
155
239
|
Flags:
|
|
156
|
-
--print-config print the MCP client config instead of starting the server
|
|
240
|
+
--print-config print the MCP client config instead of starting the server
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
open-memex mcp
|
|
244
|
+
open-memex mcp --print-config vscode`,
|
|
157
245
|
init: `One-command project setup: writes the MCP config for your editor and the
|
|
158
246
|
agent memory instructions. Existing files are merged, never clobbered.
|
|
159
247
|
|
|
@@ -164,21 +252,32 @@ Flags:
|
|
|
164
252
|
--client editor to configure (default: auto-detect)
|
|
165
253
|
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
166
254
|
--force overwrite existing config
|
|
167
|
-
--yes accept all defaults, never prompt
|
|
255
|
+
--yes accept all defaults, never prompt
|
|
256
|
+
|
|
257
|
+
Examples:
|
|
258
|
+
open-memex init
|
|
259
|
+
open-memex init --client cursor --yes`,
|
|
168
260
|
config: `Show config, or set a key.
|
|
169
261
|
|
|
170
262
|
Usage: open-memex config [set <key> <value>]
|
|
171
263
|
|
|
172
|
-
|
|
264
|
+
Examples:
|
|
265
|
+
open-memex config
|
|
173
266
|
open-memex config set sync.autoPull false`,
|
|
174
267
|
capture: `Preview what the keyword-capture watcher would extract from text.
|
|
175
268
|
|
|
176
|
-
Usage: open-memex capture --dry-run "text"
|
|
269
|
+
Usage: open-memex capture --dry-run "text"
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
open-memex capture --dry-run "remember: we deploy on Fridays"`,
|
|
177
273
|
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
178
274
|
storage writability, then boots a real MCP server and runs initialize +
|
|
179
275
|
tools/list against it — all eleven tools must show up.
|
|
180
276
|
|
|
181
|
-
Usage: open-memex doctor
|
|
277
|
+
Usage: open-memex doctor
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
open-memex doctor`,
|
|
182
281
|
};
|
|
183
282
|
function usage(exitCode = 1) {
|
|
184
283
|
console.log(`open-memex CLI
|
|
@@ -195,6 +294,11 @@ Usage:
|
|
|
195
294
|
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
196
295
|
open-memex resolve [id-or-path]
|
|
197
296
|
open-memex sync-status
|
|
297
|
+
open-memex pull
|
|
298
|
+
open-memex push
|
|
299
|
+
open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
|
|
300
|
+
open-memex import <bundle.tar.gz> [--dry-run]
|
|
301
|
+
open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
198
302
|
open-memex submit <id...> [--branch <name>] [--base <branch>]
|
|
199
303
|
open-memex pr-status [--apply]
|
|
200
304
|
open-memex reindex
|
|
@@ -209,6 +313,9 @@ Usage:
|
|
|
209
313
|
open-memex capture --dry-run "text"
|
|
210
314
|
open-memex doctor
|
|
211
315
|
|
|
316
|
+
Every command has its own help with description and examples:
|
|
317
|
+
open-memex <command> --help (or -h)
|
|
318
|
+
|
|
212
319
|
One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) writes
|
|
213
320
|
the MCP config for your editor (\`.vscode/mcp.json\`, \`.cursor/mcp.json\`,
|
|
214
321
|
\`opencode.jsonc\`, or Visual Studio's solution-level \`.mcp.json\`) — no copy-paste
|
|
@@ -239,6 +346,31 @@ type: instruction→role split. Always preview with --dry-run first.
|
|
|
239
346
|
Run \`open-memex <command> --help\` for details on a single command.`);
|
|
240
347
|
process.exit(exitCode);
|
|
241
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Build a saveConfig patch for a (possibly dotted) config key, preserving
|
|
351
|
+
* sibling keys already present in the nested object.
|
|
352
|
+
*/
|
|
353
|
+
function setConfigPath(key, value) {
|
|
354
|
+
const parts = key.split(".");
|
|
355
|
+
if (parts.length === 1)
|
|
356
|
+
return { [key]: value };
|
|
357
|
+
const cfg = loadConfig();
|
|
358
|
+
const top = parts[0];
|
|
359
|
+
const cur = cfg[top] && typeof cfg[top] === "object"
|
|
360
|
+
? { ...cfg[top] }
|
|
361
|
+
: {};
|
|
362
|
+
let node = cur;
|
|
363
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
364
|
+
const seg = parts[i];
|
|
365
|
+
const nxt = node[seg] && typeof node[seg] === "object"
|
|
366
|
+
? { ...node[seg] }
|
|
367
|
+
: {};
|
|
368
|
+
node[seg] = nxt;
|
|
369
|
+
node = nxt;
|
|
370
|
+
}
|
|
371
|
+
node[parts[parts.length - 1]] = value;
|
|
372
|
+
return { [top]: cur };
|
|
373
|
+
}
|
|
242
374
|
function parseFlags(argv) {
|
|
243
375
|
const out = {};
|
|
244
376
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -418,7 +550,9 @@ async function main() {
|
|
|
418
550
|
}
|
|
419
551
|
try {
|
|
420
552
|
const saved = validate(value);
|
|
421
|
-
|
|
553
|
+
// Dotted keys (e.g. sync.autoPull) write into the nested config
|
|
554
|
+
// object, preserving sibling keys already on disk.
|
|
555
|
+
const file = saveConfig(setConfigPath(key, saved));
|
|
422
556
|
console.log(`set ${key} = ${JSON.stringify(saved)} (${file})`);
|
|
423
557
|
}
|
|
424
558
|
catch (err) {
|
|
@@ -781,6 +915,133 @@ async function main() {
|
|
|
781
915
|
}
|
|
782
916
|
return;
|
|
783
917
|
}
|
|
918
|
+
// §9 / D12: explicit pull — fetch + fast-forward only, never auto-merge.
|
|
919
|
+
if (cmd === "pull") {
|
|
920
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
921
|
+
const root = projectRoot();
|
|
922
|
+
try {
|
|
923
|
+
const r = new GitProvider().pull(root);
|
|
924
|
+
const stats = syncScope(project.key, "pull");
|
|
925
|
+
if (r.fastForwarded) {
|
|
926
|
+
console.log(`pulled ${r.branch} from ${r.remote}: ${r.before.slice(0, 8)} → ${r.after.slice(0, 8)} (fast-forward)`);
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
console.log(`already up to date: ${r.branch} @ ${r.after.slice(0, 8)}`);
|
|
930
|
+
}
|
|
931
|
+
console.log(`index: +${stats.added} ~${stats.updated} -${stats.removed} (scanned ${stats.scanned})`);
|
|
932
|
+
}
|
|
933
|
+
catch (e) {
|
|
934
|
+
console.error(`pull failed: ${e.message}`);
|
|
935
|
+
process.exit(2);
|
|
936
|
+
}
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
// Explicit push — open-memex never pushes on its own (D36).
|
|
940
|
+
if (cmd === "push") {
|
|
941
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
942
|
+
const root = projectRoot();
|
|
943
|
+
try {
|
|
944
|
+
const r = new GitProvider().push(root);
|
|
945
|
+
syncScope(project.key, "push");
|
|
946
|
+
console.log(`pushed ${r.branch} to ${r.remote} @ ${r.head.slice(0, 8)}`);
|
|
947
|
+
}
|
|
948
|
+
catch (e) {
|
|
949
|
+
console.error(`push failed: ${e.message}`);
|
|
950
|
+
process.exit(2);
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
// §9 / D40: portable export bundle (markdown + manifest).
|
|
955
|
+
if (cmd === "export") {
|
|
956
|
+
const { exportMemories } = await import("./export.js");
|
|
957
|
+
const flags = parseFlags(rest);
|
|
958
|
+
const all = flags["all"] === "true" || flags["a"] === "true" || rest.includes("--all") || rest.includes("-a");
|
|
959
|
+
const scopeFlag = flags["scope"] ?? "project";
|
|
960
|
+
// parseFlags only handles `--` flags; `-o <file>` is picked up here.
|
|
961
|
+
const oIdx = rest.findIndex((a) => a === "-o");
|
|
962
|
+
const outFile = flags["o"] ?? flags["output"] ?? (oIdx >= 0 ? rest[oIdx + 1] : undefined);
|
|
963
|
+
const scopeKeys = scopeFlag === "both"
|
|
964
|
+
? [project.key, PERSONAL_SCOPE.key]
|
|
965
|
+
: scopeFlag === "personal"
|
|
966
|
+
? [PERSONAL_SCOPE.key]
|
|
967
|
+
: [project.key];
|
|
968
|
+
try {
|
|
969
|
+
const r = exportMemories({
|
|
970
|
+
scopeKeys,
|
|
971
|
+
type: flags["type"],
|
|
972
|
+
tag: flags["tag"],
|
|
973
|
+
includePrivate: all,
|
|
974
|
+
outFile,
|
|
975
|
+
});
|
|
976
|
+
console.log(`exported ${r.exported} memories → ${r.file}`);
|
|
977
|
+
if (!r.includePrivate && r.skippedPrivate > 0) {
|
|
978
|
+
console.log(`skipped ${r.skippedPrivate} private memories (use --all to include them)`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
catch (e) {
|
|
982
|
+
console.error(`export failed: ${e.message}`);
|
|
983
|
+
process.exit(2);
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (cmd === "import") {
|
|
988
|
+
const { importBundle } = await import("./export.js");
|
|
989
|
+
const flags = parseFlags(rest);
|
|
990
|
+
const bundle = positionalArgs(rest)[0];
|
|
991
|
+
if (!bundle) {
|
|
992
|
+
console.error(`usage: open-memex import <bundle.tar.gz> [--dry-run]`);
|
|
993
|
+
process.exit(2);
|
|
994
|
+
}
|
|
995
|
+
const dryRun = flags["dry-run"] === "true";
|
|
996
|
+
try {
|
|
997
|
+
const r = importBundle(bundle, { projectScopeKey: project.key, dryRun });
|
|
998
|
+
if (!dryRun) {
|
|
999
|
+
syncScope(project.key, "cli");
|
|
1000
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
1001
|
+
}
|
|
1002
|
+
console.log(`${dryRun ? "DRY RUN: " : ""}imported ${r.imported}, skipped ${r.skippedIdentical} identical`);
|
|
1003
|
+
for (const c of r.skippedConflict) {
|
|
1004
|
+
console.log(` conflict (kept existing): ${c.id} from ${c.file}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
catch (e) {
|
|
1008
|
+
console.error(`import failed: ${e.message}`);
|
|
1009
|
+
process.exit(2);
|
|
1010
|
+
}
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
// Phase 3: distill project memories into a proposed AGENTS.md snippet.
|
|
1014
|
+
if (cmd === "distill-agents") {
|
|
1015
|
+
const { distillAgentsMarkdown } = await import("./distill-agents.js");
|
|
1016
|
+
const flags = parseFlags(rest);
|
|
1017
|
+
const scopeFlag = flags["scope"] ?? "project";
|
|
1018
|
+
const scopeKeys = scopeFlag === "personal" ? [PERSONAL_SCOPE.key] : [project.key];
|
|
1019
|
+
const types = flags["type"]
|
|
1020
|
+
? flags["type"].split(",").map((t) => t.trim()).filter(Boolean)
|
|
1021
|
+
: undefined;
|
|
1022
|
+
const limit = flags["limit"] ? parseInt(flags["limit"], 10) : undefined;
|
|
1023
|
+
const oIdx = rest.findIndex((a) => a === "-o");
|
|
1024
|
+
const outFile = oIdx >= 0 ? rest[oIdx + 1] : undefined;
|
|
1025
|
+
try {
|
|
1026
|
+
const md = distillAgentsMarkdown({ scopeKeys, types, limit });
|
|
1027
|
+
if (!md) {
|
|
1028
|
+
console.log("no distillable memories found (decisions, constraints, lessons, gotchas, howtos)");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (outFile) {
|
|
1032
|
+
fs.writeFileSync(outFile, md, "utf8");
|
|
1033
|
+
console.log(`wrote proposed AGENTS.md snippet → ${outFile} (review and merge by hand)`);
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
console.log(md);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
catch (e) {
|
|
1040
|
+
console.error(`distill-agents failed: ${e.message}`);
|
|
1041
|
+
process.exit(2);
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
784
1045
|
if (cmd === "pr-status") {
|
|
785
1046
|
const flags = parseFlags(rest);
|
|
786
1047
|
try {
|
|
@@ -852,6 +1113,10 @@ async function main() {
|
|
|
852
1113
|
entries.push({ key: name, files, marker });
|
|
853
1114
|
}
|
|
854
1115
|
entries.sort((a, b) => b.files - a.files);
|
|
1116
|
+
if (entries.length === 0) {
|
|
1117
|
+
console.log("(no scopes with memories yet — add one with `open-memex add`)");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
855
1120
|
for (const e of entries) {
|
|
856
1121
|
console.log(` ${e.files.toString().padStart(4)} ${e.key}${e.marker}`);
|
|
857
1122
|
}
|
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
|
+
}
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export / import: portable memory archives (§9, D40).
|
|
3
|
+
*
|
|
4
|
+
* `export` bundles selected memories (markdown source of truth + manifest)
|
|
5
|
+
* into a single .tar.gz for moving to another machine or another app.
|
|
6
|
+
* `import` restores a bundle: personal memories go to the personal dir,
|
|
7
|
+
* project memories are re-keyed to the current project and land in the
|
|
8
|
+
* outbox as drafts (submit moves them into the repo).
|
|
9
|
+
*
|
|
10
|
+
* D40: export excludes `visibility: private` by default; `--all` / `-a`
|
|
11
|
+
* includes everything — the full-migration escape hatch.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { db } from "./store/db.js";
|
|
18
|
+
import { readMemoryFile, writeMemoryFile, normalizeFrontmatter, } from "./store/markdown.js";
|
|
19
|
+
import { upsertFromFile } from "./store/sync.js";
|
|
20
|
+
import { contentHash } from "./store/lifecycle.js";
|
|
21
|
+
const EXPORT_FORMAT = "open-memex-export/1";
|
|
22
|
+
function fail(msg) {
|
|
23
|
+
throw new Error(`[open-memex] ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
function checkTar() {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync("tar", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
fail("the `tar` command is required for export/import but was not found on PATH");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function exportMemories(opts) {
|
|
34
|
+
checkTar();
|
|
35
|
+
const includePrivate = opts.includePrivate ?? false;
|
|
36
|
+
let sql = `SELECT id, scope_key, scope, visibility, type, file_path FROM memories WHERE scope_key IN (${opts.scopeKeys.map(() => "?").join(",")})`;
|
|
37
|
+
const params = [...opts.scopeKeys];
|
|
38
|
+
if (!includePrivate)
|
|
39
|
+
sql += ` AND visibility != 'private'`;
|
|
40
|
+
if (opts.type) {
|
|
41
|
+
sql += ` AND type = ?`;
|
|
42
|
+
params.push(opts.type);
|
|
43
|
+
}
|
|
44
|
+
if (opts.tag) {
|
|
45
|
+
sql += ` AND (',' || tags || ',' LIKE ?)`;
|
|
46
|
+
params.push(`%,${opts.tag},%`);
|
|
47
|
+
}
|
|
48
|
+
sql += ` ORDER BY updated_at DESC`;
|
|
49
|
+
const rows = db().prepare(sql).all(...params);
|
|
50
|
+
const skippedPrivate = includePrivate
|
|
51
|
+
? 0
|
|
52
|
+
: db().prepare(`SELECT COUNT(*) AS n FROM memories WHERE scope_key IN (${opts.scopeKeys.map(() => "?").join(",")}) AND visibility = 'private'`).get(...opts.scopeKeys).n;
|
|
53
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), "open-memex-export-"));
|
|
54
|
+
try {
|
|
55
|
+
const memDir = path.join(stage, "memories");
|
|
56
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
57
|
+
const manifestEntries = [];
|
|
58
|
+
let exported = 0;
|
|
59
|
+
for (const r of rows) {
|
|
60
|
+
const mf = readMemoryFile(r.file_path);
|
|
61
|
+
if (!mf)
|
|
62
|
+
continue; // stale index row — skip, don't fail the export
|
|
63
|
+
const rel = path.join(r.scope, `${r.id}.md`);
|
|
64
|
+
const dest = path.join(memDir, rel);
|
|
65
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
66
|
+
fs.copyFileSync(r.file_path, dest);
|
|
67
|
+
manifestEntries.push({ id: r.id, scope: r.scope, file: path.join("memories", rel) });
|
|
68
|
+
exported++;
|
|
69
|
+
}
|
|
70
|
+
const manifest = {
|
|
71
|
+
format: EXPORT_FORMAT,
|
|
72
|
+
exported_at: new Date().toISOString(),
|
|
73
|
+
open_memex_version: readPackageVersion(),
|
|
74
|
+
include_private: includePrivate,
|
|
75
|
+
filters: {
|
|
76
|
+
scope_keys: opts.scopeKeys,
|
|
77
|
+
type: opts.type ?? null,
|
|
78
|
+
tag: opts.tag ?? null,
|
|
79
|
+
},
|
|
80
|
+
memories: manifestEntries,
|
|
81
|
+
};
|
|
82
|
+
fs.writeFileSync(path.join(stage, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
83
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15);
|
|
84
|
+
const outFile = opts.outFile ?? path.resolve(`open-memex-export-${stamp}.tar.gz`);
|
|
85
|
+
execFileSync("tar", ["-czf", outFile, "-C", stage, "manifest.json", "memories"], {
|
|
86
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
+
});
|
|
88
|
+
return { file: outFile, exported, skippedPrivate, includePrivate };
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function readPackageVersion() {
|
|
95
|
+
try {
|
|
96
|
+
const here = new URL(import.meta.url);
|
|
97
|
+
const pkg = path.join(path.dirname(here.pathname), "..", "package.json");
|
|
98
|
+
return JSON.parse(fs.readFileSync(pkg, "utf8")).version;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return "unknown";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function importBundle(bundlePath, opts) {
|
|
105
|
+
checkTar();
|
|
106
|
+
if (!fs.existsSync(bundlePath))
|
|
107
|
+
fail(`bundle not found: ${bundlePath}`);
|
|
108
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), "open-memex-import-"));
|
|
109
|
+
try {
|
|
110
|
+
execFileSync("tar", ["-xzf", path.resolve(bundlePath), "-C", stage], {
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
});
|
|
113
|
+
const manifestPath = path.join(stage, "manifest.json");
|
|
114
|
+
if (!fs.existsSync(manifestPath))
|
|
115
|
+
fail("not an open-memex export bundle (manifest.json missing)");
|
|
116
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
117
|
+
if (manifest.format !== EXPORT_FORMAT) {
|
|
118
|
+
fail(`unsupported bundle format: ${manifest.format} (expected ${EXPORT_FORMAT})`);
|
|
119
|
+
}
|
|
120
|
+
const result = { imported: 0, skippedIdentical: 0, skippedConflict: [] };
|
|
121
|
+
const existingStmt = db().prepare(`SELECT content_hash FROM memories WHERE id = ?`);
|
|
122
|
+
for (const entry of manifest.memories ?? []) {
|
|
123
|
+
const src = path.join(stage, entry.file);
|
|
124
|
+
const mf = readMemoryFile(src);
|
|
125
|
+
if (!mf)
|
|
126
|
+
continue;
|
|
127
|
+
const fm = normalizeFrontmatter({ ...mf.fm });
|
|
128
|
+
// Re-key to this machine: personal stays personal; project memories
|
|
129
|
+
// adopt the current project's scope key (keys embed a path hash).
|
|
130
|
+
if (fm.scope === "project")
|
|
131
|
+
fm.scope_key = opts.projectScopeKey;
|
|
132
|
+
else if (fm.scope === "personal")
|
|
133
|
+
fm.scope_key = "personal";
|
|
134
|
+
const existing = existingStmt.get(fm.id);
|
|
135
|
+
if (existing) {
|
|
136
|
+
if (existing.content_hash === contentHash(mf.body))
|
|
137
|
+
result.skippedIdentical++;
|
|
138
|
+
else
|
|
139
|
+
result.skippedConflict.push({ id: fm.id, file: entry.file });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (opts.dryRun) {
|
|
143
|
+
result.imported++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const { filePath } = writeMemoryFile(fm, mf.body);
|
|
147
|
+
const written = readMemoryFile(filePath);
|
|
148
|
+
if (written)
|
|
149
|
+
upsertFromFile(written);
|
|
150
|
+
result.imported++;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -104,6 +104,20 @@ export async function runMcpServer() {
|
|
|
104
104
|
syncScope(scope.key, "session");
|
|
105
105
|
syncScope(PERSONAL_SCOPE.key, "session");
|
|
106
106
|
console.error(`[open-memex] MCP server up. scope=${scope.key}`);
|
|
107
|
+
// D12: pulls are explicit by default — session start never touches the
|
|
108
|
+
// network. With sync.autoPull, one best-effort pull; a failure never
|
|
109
|
+
// blocks the session, it just logs and continues.
|
|
110
|
+
if (cfg.sync?.autoPull) {
|
|
111
|
+
try {
|
|
112
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
113
|
+
const r = new GitProvider().pull(process.cwd());
|
|
114
|
+
syncScope(scope.key, "pull");
|
|
115
|
+
console.error(`[open-memex] auto-pull: ${r.branch} ${r.fastForwarded ? "fast-forwarded" : "already up to date"}`);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(`[open-memex] auto-pull skipped: ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
107
121
|
// D26: re-sync on every request, not just at startup. The in-repo dir
|
|
108
122
|
// follows the current git branch, so a branch switch mid-session would
|
|
109
123
|
// otherwise leave the index pointing at files that no longer exist.
|