open-memex 0.3.0 → 0.4.0-alpha.10
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 +20 -1
- package/README.md +117 -9
- package/README.zh-CN.md +103 -9
- package/dist/cli.js +583 -14
- package/dist/config.js +12 -0
- package/dist/distill-agents.js +55 -0
- package/dist/doctor.js +1 -1
- package/dist/export.js +157 -0
- package/dist/github.js +215 -0
- package/dist/index.js +6 -0
- package/dist/init.js +43 -18
- package/dist/mcp.js +119 -12
- package/dist/paths.js +31 -0
- package/dist/providers/git.js +142 -0
- package/dist/retrieve/inject.js +2 -2
- package/dist/retrieve/search.js +31 -6
- package/dist/review.js +437 -0
- package/dist/store/db.js +3 -2
- package/dist/store/lifecycle.js +28 -13
- package/dist/store/markdown.js +57 -14
- package/dist/store/sync.js +74 -6
- package/dist/submit.js +347 -0
- package/dist/tools/ops.js +171 -8
- package/docs/CURATOR.md +59 -0
- package/docs/USER-GUIDE.md +197 -0
- package/docs/USER-GUIDE.zh-CN.md +161 -0
- package/docs/V2-DESIGN.md +287 -13
- package/package.json +1 -1
- package/scripts/smoke-mcp.ts +2 -2
- package/src/cli.ts +608 -15
- package/src/config.ts +26 -0
- package/src/distill-agents.ts +70 -0
- package/src/doctor.ts +1 -1
- package/src/export.ts +206 -0
- package/src/github.ts +257 -0
- package/src/index.ts +6 -0
- package/src/init.ts +43 -17
- package/src/mcp.ts +166 -11
- package/src/paths.ts +32 -0
- package/src/providers/git.ts +191 -0
- package/src/retrieve/inject.ts +2 -2
- package/src/retrieve/search.ts +33 -6
- package/src/review.ts +504 -0
- package/src/store/db.ts +3 -2
- package/src/store/lifecycle.ts +30 -12
- package/src/store/markdown.ts +91 -14
- package/src/store/sync.ts +85 -4
- package/src/submit.ts +424 -0
- package/src/tools/ops.ts +203 -9
package/dist/cli.js
CHANGED
|
@@ -6,15 +6,213 @@ import { syncScope, upsertFromFile, deleteFromIndex } from "./store/sync.js";
|
|
|
6
6
|
import { migrateScope, scopeHasFiles } from "./store/migrate.js";
|
|
7
7
|
import { migrateV2 } from "./store/v2migrate.js";
|
|
8
8
|
import { findDuplicates, supersede, setStatus } from "./store/lifecycle.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { proposeMemories, promoteMemory, listConflicts, resolveConflict, formatReviewHistory } from "./review.js";
|
|
10
|
+
import { getSyncStatus, formatSyncStatus, submitMemories } from "./submit.js";
|
|
11
|
+
import { getPrStatus, formatPrStatus, applyPrStatus } from "./github.js";
|
|
12
|
+
import { search, list, hitStateLabel } from "./retrieve/search.js";
|
|
13
|
+
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, } from "./store/markdown.js";
|
|
11
14
|
import { loadConfig } from "./config.js";
|
|
12
|
-
import { paths } from "./paths.js";
|
|
15
|
+
import { paths, projectRoot } from "./paths.js";
|
|
13
16
|
import { redact } from "./redact.js";
|
|
14
17
|
import { resolveMcpCommand } from "./init.js";
|
|
15
18
|
import fs from "node:fs";
|
|
16
19
|
import path from "node:path";
|
|
17
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
+
/** Per-command help, printed by `open-memex <command> --help`.
|
|
22
|
+
AI assistants discover the CLI through --help, so every command needs one. */
|
|
23
|
+
const COMMAND_HELP = {
|
|
24
|
+
where: `Show which project scope the current directory resolves to, and where its data lives.
|
|
25
|
+
|
|
26
|
+
Usage: open-memex where`,
|
|
27
|
+
list: `List memories in a scope, newest first.
|
|
28
|
+
|
|
29
|
+
Usage: open-memex list [--scope project|personal] [--type T] [--limit N]
|
|
30
|
+
|
|
31
|
+
Flags:
|
|
32
|
+
--scope project (default) or personal
|
|
33
|
+
--type filter by memory type
|
|
34
|
+
--limit max results
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
open-memex list
|
|
38
|
+
open-memex list --scope personal --limit 20`,
|
|
39
|
+
search: `Search memories by keyword (BM25 full-text), best matches first.
|
|
40
|
+
|
|
41
|
+
Usage: open-memex search "query" [--scope project|personal|both] [--type T] [--limit N]
|
|
42
|
+
|
|
43
|
+
Flags:
|
|
44
|
+
--scope project (default), personal, or both
|
|
45
|
+
--type filter by memory type
|
|
46
|
+
--limit max results
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
open-memex search "deploy checklist" --scope both`,
|
|
50
|
+
add: `Save a fact, preference, decision, or note to local memory.
|
|
51
|
+
|
|
52
|
+
Usage: open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2]
|
|
53
|
+
|
|
54
|
+
Flags:
|
|
55
|
+
--scope project (default) or personal (personal never leaves this machine)
|
|
56
|
+
--type memory type (default: fact)
|
|
57
|
+
--tag comma-separated tags
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
open-memex add "We deploy on Fridays" --scope project --tag process`,
|
|
61
|
+
supersede: `Replace a memory with a newer version. The old one is kept as history.
|
|
62
|
+
|
|
63
|
+
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]`,
|
|
64
|
+
status: `Change a memory's lifecycle status.
|
|
65
|
+
|
|
66
|
+
Usage: open-memex status <id> active|deprecated|retracted|archived`,
|
|
67
|
+
forget: `Delete a memory by id.
|
|
68
|
+
|
|
69
|
+
Usage: open-memex forget <id>`,
|
|
70
|
+
propose: `Copy personal memories into the project outbox as review drafts.
|
|
71
|
+
The personal originals stay put. Nothing enters git at this step.
|
|
72
|
+
|
|
73
|
+
Usage: open-memex propose <id...> --to project [--local-approve]
|
|
74
|
+
|
|
75
|
+
Flags:
|
|
76
|
+
--to project (required)
|
|
77
|
+
--local-approve mark the copies approved right away (solo-dev shortcut)
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
open-memex propose 01ABC 01DEF --to project`,
|
|
81
|
+
promote: `Advance a project memory one step up the review ladder
|
|
82
|
+
(proposed → approved → published), or reject it with a note.
|
|
83
|
+
Every transition is appended to the memory's review_history.
|
|
84
|
+
Rejected memories are never deleted — they can be revised and resubmitted.
|
|
85
|
+
|
|
86
|
+
Usage: open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
87
|
+
|
|
88
|
+
Flags:
|
|
89
|
+
--reject move back to rejected (requires --note)
|
|
90
|
+
--resubmit move a rejected memory back to proposed
|
|
91
|
+
--note reason for the transition (recorded in review_history)
|
|
92
|
+
--by reviewer name (defaults to the git user)
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
open-memex promote 01ABC --note "verified against the runbook"
|
|
96
|
+
open-memex promote 01ABC --reject --note "outdated after the migration"`,
|
|
97
|
+
resolve: `List conflicted memories, or 3-way-merge one.
|
|
98
|
+
|
|
99
|
+
Usage: open-memex resolve [id-or-path]
|
|
100
|
+
|
|
101
|
+
With no argument, lists conflicts. With an id or file path, shows the
|
|
102
|
+
3-way merge (base / outbox / repo) so you can resolve it by hand.
|
|
103
|
+
Conflicts are never auto-resolved.`,
|
|
104
|
+
"sync-status": `Show the project memory sync pipeline: when the index last synced
|
|
105
|
+
and what triggered it, drafts waiting in the outbox (appdata), memories in the
|
|
106
|
+
repo awaiting review or published, and repo files not yet committed.
|
|
107
|
+
|
|
108
|
+
Usage: open-memex sync-status`,
|
|
109
|
+
pull: `Pull shared project memories from the git remote: fetch + fast-forward
|
|
110
|
+
only. Never auto-merges — a diverged branch fails with a clear message and is
|
|
111
|
+
left for you to resolve by hand. On success the local index re-syncs.
|
|
112
|
+
|
|
113
|
+
Usage: open-memex pull`,
|
|
114
|
+
push: `Push the current branch (with its submitted memories) to the git
|
|
115
|
+
remote. Explicit only — open-memex never pushes on its own.
|
|
116
|
+
|
|
117
|
+
Usage: open-memex push`,
|
|
118
|
+
export: `Export memories to a portable .tar.gz bundle (markdown source of
|
|
119
|
+
truth + manifest.json) for moving to another machine or another app.
|
|
120
|
+
Excludes visibility:private memories by default; --all includes everything.
|
|
121
|
+
|
|
122
|
+
Usage: open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
|
|
123
|
+
|
|
124
|
+
Flags:
|
|
125
|
+
--scope project (default), personal, or both
|
|
126
|
+
--type filter by memory type
|
|
127
|
+
--tag filter by tag
|
|
128
|
+
--all, -a include private memories (full migration)
|
|
129
|
+
-o output file (default: ./open-memex-export-<timestamp>.tar.gz)`,
|
|
130
|
+
import: `Import a bundle created by \`open-memex export\`. Personal memories
|
|
131
|
+
go to the personal dir; project memories are re-keyed to the current project
|
|
132
|
+
and land in the outbox as drafts. Existing identical memories are skipped;
|
|
133
|
+
conflicting ids are reported, never overwritten.
|
|
134
|
+
|
|
135
|
+
Usage: open-memex import <bundle.tar.gz> [--dry-run]`,
|
|
136
|
+
"distill-agents": `Propose an AGENTS.md snippet distilled from project
|
|
137
|
+
memories (decisions, constraints, lessons, gotchas, howtos). Prints markdown
|
|
138
|
+
to stdout, or writes it with -o. Review and merge by hand — open-memex never
|
|
139
|
+
rewrites your AGENTS.md on its own.
|
|
140
|
+
|
|
141
|
+
Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]`,
|
|
142
|
+
submit: `Move outbox drafts into the repo for review: copies the drafts into
|
|
143
|
+
the repo memory dir as proposed (a local-approved copy keeps its approval),
|
|
144
|
+
commits locally on the CURRENT branch, and moves the outbox originals out.
|
|
145
|
+
Never creates a branch on its own — branch creation is your call.
|
|
146
|
+
|
|
147
|
+
Usage: open-memex submit <id...> [--branch <name>] [--base <branch>]
|
|
148
|
+
|
|
149
|
+
Flags:
|
|
150
|
+
--branch create this branch and submit onto it (only with your explicit
|
|
151
|
+
approval for the full chain); default: stay on current branch
|
|
152
|
+
--base PR base override (default: the branch you're on)
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
open-memex submit 01ABC 01DEF`,
|
|
156
|
+
"pr-status": `Map the current branch's GitHub PR state back onto review_state:
|
|
157
|
+
merged → published, approval → approved (approved_by = the reviewer),
|
|
158
|
+
changes-requested → suggestion only (never auto-rejects).
|
|
159
|
+
Each memory in the PR is mapped independently; a human rejection is never
|
|
160
|
+
overwritten. Report-only by default.
|
|
161
|
+
|
|
162
|
+
Usage: open-memex pr-status [--apply]
|
|
163
|
+
|
|
164
|
+
Flags:
|
|
165
|
+
--apply write the transitions locally (still never pushes)`,
|
|
166
|
+
reindex: `Rebuild the SQLite index from the markdown files.
|
|
167
|
+
|
|
168
|
+
Usage: open-memex reindex`,
|
|
169
|
+
scopes: `List the known scopes (personal + project).
|
|
170
|
+
|
|
171
|
+
Usage: open-memex scopes`,
|
|
172
|
+
migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
|
|
173
|
+
|
|
174
|
+
Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict newer|overwrite|skip]
|
|
175
|
+
open-memex migrate --to-v2 [--dry-run]
|
|
176
|
+
|
|
177
|
+
Flags:
|
|
178
|
+
--from / --to scope keys (default: current project → personal)
|
|
179
|
+
--dry-run preview without moving anything
|
|
180
|
+
--on-conflict newer (default), overwrite, or skip
|
|
181
|
+
--to-v2 convert a legacy my-o-memory data dir to the v2 layout
|
|
182
|
+
|
|
183
|
+
Always preview with --dry-run first; nothing moves without confirmation.`,
|
|
184
|
+
mcp: `Start the stdio MCP server (the same server editors connect to).
|
|
185
|
+
|
|
186
|
+
Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
187
|
+
|
|
188
|
+
Flags:
|
|
189
|
+
--print-config print the MCP client config instead of starting the server`,
|
|
190
|
+
init: `One-command project setup: writes the MCP config for your editor and the
|
|
191
|
+
agent memory instructions. Existing files are merged, never clobbered.
|
|
192
|
+
|
|
193
|
+
Usage: open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
194
|
+
[--instructions personal|project] [--force] [--yes]
|
|
195
|
+
|
|
196
|
+
Flags:
|
|
197
|
+
--client editor to configure (default: auto-detect)
|
|
198
|
+
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
199
|
+
--force overwrite existing config
|
|
200
|
+
--yes accept all defaults, never prompt`,
|
|
201
|
+
config: `Show config, or set a key.
|
|
202
|
+
|
|
203
|
+
Usage: open-memex config [set <key> <value>]
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
open-memex config set sync.autoPull false`,
|
|
207
|
+
capture: `Preview what the keyword-capture watcher would extract from text.
|
|
208
|
+
|
|
209
|
+
Usage: open-memex capture --dry-run "text"`,
|
|
210
|
+
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
211
|
+
storage writability, then boots a real MCP server and runs initialize +
|
|
212
|
+
tools/list against it — all eleven tools must show up.
|
|
213
|
+
|
|
214
|
+
Usage: open-memex doctor`,
|
|
215
|
+
};
|
|
18
216
|
function usage(exitCode = 1) {
|
|
19
217
|
console.log(`open-memex CLI
|
|
20
218
|
|
|
@@ -26,6 +224,17 @@ Usage:
|
|
|
26
224
|
open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
27
225
|
open-memex status <id> active|deprecated|retracted|archived
|
|
28
226
|
open-memex forget <id>
|
|
227
|
+
open-memex propose <id...> --to project [--local-approve]
|
|
228
|
+
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
229
|
+
open-memex resolve [id-or-path]
|
|
230
|
+
open-memex sync-status
|
|
231
|
+
open-memex pull
|
|
232
|
+
open-memex push
|
|
233
|
+
open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
|
|
234
|
+
open-memex import <bundle.tar.gz> [--dry-run]
|
|
235
|
+
open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
236
|
+
open-memex submit <id...> [--branch <name>] [--base <branch>]
|
|
237
|
+
open-memex pr-status [--apply]
|
|
29
238
|
open-memex reindex
|
|
30
239
|
open-memex scopes
|
|
31
240
|
open-memex migrate [--from <key>] [--to <key>]
|
|
@@ -63,9 +272,36 @@ the \`--from\` key when migrating.
|
|
|
63
272
|
git remote after memories were already stored under the cwd-based key.
|
|
64
273
|
\`migrate --to-v2\` converts v1 memory files to the v2 format (§19):
|
|
65
274
|
user→personal scope rename, epoch→RFC 3339 times, priority→importance,
|
|
66
|
-
type: instruction→role split. Always preview with --dry-run first
|
|
275
|
+
type: instruction→role split. Always preview with --dry-run first.
|
|
276
|
+
|
|
277
|
+
Run \`open-memex <command> --help\` for details on a single command.`);
|
|
67
278
|
process.exit(exitCode);
|
|
68
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Build a saveConfig patch for a (possibly dotted) config key, preserving
|
|
282
|
+
* sibling keys already present in the nested object.
|
|
283
|
+
*/
|
|
284
|
+
function setConfigPath(key, value) {
|
|
285
|
+
const parts = key.split(".");
|
|
286
|
+
if (parts.length === 1)
|
|
287
|
+
return { [key]: value };
|
|
288
|
+
const cfg = loadConfig();
|
|
289
|
+
const top = parts[0];
|
|
290
|
+
const cur = cfg[top] && typeof cfg[top] === "object"
|
|
291
|
+
? { ...cfg[top] }
|
|
292
|
+
: {};
|
|
293
|
+
let node = cur;
|
|
294
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
295
|
+
const seg = parts[i];
|
|
296
|
+
const nxt = node[seg] && typeof node[seg] === "object"
|
|
297
|
+
? { ...node[seg] }
|
|
298
|
+
: {};
|
|
299
|
+
node[seg] = nxt;
|
|
300
|
+
node = nxt;
|
|
301
|
+
}
|
|
302
|
+
node[parts[parts.length - 1]] = value;
|
|
303
|
+
return { [top]: cur };
|
|
304
|
+
}
|
|
69
305
|
function parseFlags(argv) {
|
|
70
306
|
const out = {};
|
|
71
307
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -163,6 +399,16 @@ async function main() {
|
|
|
163
399
|
console.log(`open-memex ${pkg.version}`);
|
|
164
400
|
return;
|
|
165
401
|
}
|
|
402
|
+
// Per-command help: `open-memex <command> --help`. Checked before loadConfig()
|
|
403
|
+
// so it works even when the environment is broken.
|
|
404
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
405
|
+
const h = COMMAND_HELP[cmd];
|
|
406
|
+
if (h) {
|
|
407
|
+
console.log(`open-memex ${cmd}\n\n${h}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
usage(0);
|
|
411
|
+
}
|
|
166
412
|
const cfg = loadConfig();
|
|
167
413
|
const project = resolveProjectScope(process.cwd());
|
|
168
414
|
// `migrate --to-v2` is a pure file operation (V2-DESIGN §19) — it runs
|
|
@@ -235,7 +481,9 @@ async function main() {
|
|
|
235
481
|
}
|
|
236
482
|
try {
|
|
237
483
|
const saved = validate(value);
|
|
238
|
-
|
|
484
|
+
// Dotted keys (e.g. sync.autoPull) write into the nested config
|
|
485
|
+
// object, preserving sibling keys already on disk.
|
|
486
|
+
const file = saveConfig(setConfigPath(key, saved));
|
|
239
487
|
console.log(`set ${key} = ${JSON.stringify(saved)} (${file})`);
|
|
240
488
|
}
|
|
241
489
|
catch (err) {
|
|
@@ -294,15 +542,15 @@ async function main() {
|
|
|
294
542
|
return;
|
|
295
543
|
}
|
|
296
544
|
if (cmd === "reindex") {
|
|
297
|
-
const a = syncScope(project.key);
|
|
298
|
-
const b = syncScope(PERSONAL_SCOPE.key);
|
|
545
|
+
const a = syncScope(project.key, "cli");
|
|
546
|
+
const b = syncScope(PERSONAL_SCOPE.key, "cli");
|
|
299
547
|
console.log(`reindexed. project: +${a.added} ~${a.updated} -${a.removed} (scanned ${a.scanned}), personal: +${b.added} ~${b.updated} -${b.removed} (scanned ${b.scanned})`);
|
|
300
548
|
return;
|
|
301
549
|
}
|
|
302
550
|
if (cmd === "list") {
|
|
303
551
|
const flags = parseFlags(rest);
|
|
304
552
|
const s = resolveCliScope(flags, project);
|
|
305
|
-
syncScope(s.key);
|
|
553
|
+
syncScope(s.key, "cli");
|
|
306
554
|
const hits = list(s.key, {
|
|
307
555
|
type: flags.type,
|
|
308
556
|
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
@@ -313,7 +561,8 @@ async function main() {
|
|
|
313
561
|
}
|
|
314
562
|
for (const h of hits) {
|
|
315
563
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
316
|
-
|
|
564
|
+
const rev = hitStateLabel(h);
|
|
565
|
+
console.log(`[${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
317
566
|
}
|
|
318
567
|
return;
|
|
319
568
|
}
|
|
@@ -322,8 +571,8 @@ async function main() {
|
|
|
322
571
|
if (!query || query.startsWith("--"))
|
|
323
572
|
usage();
|
|
324
573
|
const flags = parseFlags(rest.slice(1));
|
|
325
|
-
syncScope(project.key);
|
|
326
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
574
|
+
syncScope(project.key, "cli");
|
|
575
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
327
576
|
const keys = flags.scope === "personal" || flags.scope === "user"
|
|
328
577
|
? [PERSONAL_SCOPE.key]
|
|
329
578
|
: flags.scope === "project"
|
|
@@ -341,7 +590,8 @@ async function main() {
|
|
|
341
590
|
for (const h of hits) {
|
|
342
591
|
const tag = h.scope_key === PERSONAL_SCOPE.key ? "personal" : "project";
|
|
343
592
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
344
|
-
|
|
593
|
+
const rev = hitStateLabel(h);
|
|
594
|
+
console.log(`[${tag}/${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
345
595
|
}
|
|
346
596
|
return;
|
|
347
597
|
}
|
|
@@ -384,6 +634,12 @@ async function main() {
|
|
|
384
634
|
updated_at: rfc,
|
|
385
635
|
supersedes: null,
|
|
386
636
|
superseded_by: null,
|
|
637
|
+
review_state: "draft",
|
|
638
|
+
proposed_by: null,
|
|
639
|
+
approved_by: null,
|
|
640
|
+
derived_from: null,
|
|
641
|
+
review_note: null,
|
|
642
|
+
review_history: [],
|
|
387
643
|
};
|
|
388
644
|
const { filePath } = writeMemoryFile(fm, red);
|
|
389
645
|
const mf = readMemoryFile(filePath);
|
|
@@ -447,16 +703,329 @@ async function main() {
|
|
|
447
703
|
const id = rest[0];
|
|
448
704
|
if (!id)
|
|
449
705
|
usage();
|
|
450
|
-
const row = db().prepare(`SELECT
|
|
706
|
+
const row = db().prepare(`SELECT file_path FROM memories WHERE id = ?`).get(id);
|
|
451
707
|
if (!row) {
|
|
452
708
|
console.error("not found");
|
|
453
709
|
process.exit(1);
|
|
454
710
|
}
|
|
455
|
-
|
|
711
|
+
// Delete by indexed file_path — location-agnostic (appdata or in-repo, 2B/D24).
|
|
712
|
+
if (row.file_path) {
|
|
713
|
+
try {
|
|
714
|
+
fs.unlinkSync(row.file_path);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
/* already gone */
|
|
718
|
+
}
|
|
719
|
+
}
|
|
456
720
|
deleteFromIndex(id);
|
|
457
721
|
console.log(`deleted ${id}`);
|
|
458
722
|
return;
|
|
459
723
|
}
|
|
724
|
+
/** Positional args with flags (and their values) skipped. */
|
|
725
|
+
function positionalArgs(argv) {
|
|
726
|
+
const out = [];
|
|
727
|
+
for (let i = 0; i < argv.length; i++) {
|
|
728
|
+
const a = argv[i];
|
|
729
|
+
if (a.startsWith("--")) {
|
|
730
|
+
const val = argv[i + 1];
|
|
731
|
+
if (val !== undefined && !val.startsWith("--"))
|
|
732
|
+
i++; // skip the flag's value
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
out.push(a);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
if (cmd === "propose") {
|
|
741
|
+
const flags = parseFlags(rest);
|
|
742
|
+
const ids = positionalArgs(rest);
|
|
743
|
+
if (ids.length === 0)
|
|
744
|
+
usage();
|
|
745
|
+
const to = flags.to ?? "project";
|
|
746
|
+
if (to !== "project") {
|
|
747
|
+
console.error(`propose --to "${to}" is not supported yet (org sharing is Phase 4).`);
|
|
748
|
+
process.exit(2);
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
const batch = proposeMemories(ids, { localApprove: flags["local-approve"] === "true" });
|
|
752
|
+
for (const { sourceId, result: r } of batch) {
|
|
753
|
+
console.log(`proposed ${sourceId} → ${r.id} [${r.reviewState}]`);
|
|
754
|
+
}
|
|
755
|
+
console.log(`Next: these are drafts in the outbox (appdata) — nothing is in git yet.`);
|
|
756
|
+
console.log(` open-memex sync-status`);
|
|
757
|
+
console.log(` open-memex submit ${batch.map(({ result: r }) => r.id).join(" ")} # new branch, local commit; prints push + PR commands`);
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
console.error(`propose failed: ${e.message}`);
|
|
761
|
+
process.exit(2);
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (cmd === "promote") {
|
|
766
|
+
const flags = parseFlags(rest);
|
|
767
|
+
const id = rest.find((a) => !a.startsWith("--"));
|
|
768
|
+
if (!id)
|
|
769
|
+
usage();
|
|
770
|
+
try {
|
|
771
|
+
const r = promoteMemory(id, {
|
|
772
|
+
reject: flags.reject === "true",
|
|
773
|
+
resubmit: flags.resubmit === "true",
|
|
774
|
+
note: flags.note,
|
|
775
|
+
by: flags.by,
|
|
776
|
+
});
|
|
777
|
+
console.log(`promote ${r.id}: ${r.from} → ${r.to}`);
|
|
778
|
+
if (r.to === "published") {
|
|
779
|
+
console.log(` merged to the shared branch? Teammates pick it up with an explicit pull.`);
|
|
780
|
+
}
|
|
781
|
+
if (r.to === "rejected") {
|
|
782
|
+
console.log(` not deleted — the file stays on your branch. Your call:`);
|
|
783
|
+
console.log(` accept: close the PR and delete the branch;`);
|
|
784
|
+
console.log(` revise: edit the file, then \`open-memex promote ${r.id} --resubmit\`;`);
|
|
785
|
+
console.log(` keep: leave it as a [rejected] record.`);
|
|
786
|
+
}
|
|
787
|
+
if (r.history.length > 0) {
|
|
788
|
+
console.log(` history (${r.history.length}):`);
|
|
789
|
+
for (const h of formatReviewHistory(r.history))
|
|
790
|
+
console.log(h);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
console.error(`promote failed: ${e.message}`);
|
|
795
|
+
process.exit(2);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (cmd === "resolve") {
|
|
800
|
+
const target = rest.find((a) => !a.startsWith("--"));
|
|
801
|
+
try {
|
|
802
|
+
if (!target) {
|
|
803
|
+
const conflicts = listConflicts();
|
|
804
|
+
if (conflicts.length === 0) {
|
|
805
|
+
console.log("(no conflicted memory files)");
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
for (const c of conflicts)
|
|
809
|
+
console.log(`conflicted: ${c.id} ${c.filePath}`);
|
|
810
|
+
console.log(`\nRun \`open-memex resolve <id>\` to attempt a field-level 3-way merge.`);
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const outcome = resolveConflict(target);
|
|
815
|
+
if (!outcome.ok) {
|
|
816
|
+
console.error(`cannot auto-resolve ${path.basename(outcome.filePath)} — semantic conflicts need a human:`);
|
|
817
|
+
for (const c of outcome.conflicts) {
|
|
818
|
+
console.error(` ${c.field}:`);
|
|
819
|
+
console.error(` base: ${c.base}`);
|
|
820
|
+
console.error(` ours: ${c.ours}`);
|
|
821
|
+
console.error(` theirs: ${c.theirs}`);
|
|
822
|
+
}
|
|
823
|
+
console.error(`Edit the file manually, then \`git add\` it. Nothing was written.`);
|
|
824
|
+
process.exit(3);
|
|
825
|
+
}
|
|
826
|
+
console.log(`resolved ${path.basename(outcome.filePath)}`);
|
|
827
|
+
if (outcome.autoMerged.length > 0)
|
|
828
|
+
console.log(` auto-merged: ${outcome.autoMerged.join(", ")}`);
|
|
829
|
+
console.log(` next: git add ${path.relative(process.cwd(), outcome.filePath)}`);
|
|
830
|
+
}
|
|
831
|
+
catch (e) {
|
|
832
|
+
console.error(`resolve failed: ${e.message}`);
|
|
833
|
+
process.exit(2);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (cmd === "sync-status") {
|
|
838
|
+
syncScope(project.key, "cli");
|
|
839
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
840
|
+
try {
|
|
841
|
+
console.log(formatSyncStatus(getSyncStatus()));
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
console.error(`sync-status failed: ${e.message}`);
|
|
845
|
+
process.exit(2);
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
// §9 / D12: explicit pull — fetch + fast-forward only, never auto-merge.
|
|
850
|
+
if (cmd === "pull") {
|
|
851
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
852
|
+
const root = projectRoot();
|
|
853
|
+
try {
|
|
854
|
+
const r = new GitProvider().pull(root);
|
|
855
|
+
const stats = syncScope(project.key, "pull");
|
|
856
|
+
if (r.fastForwarded) {
|
|
857
|
+
console.log(`pulled ${r.branch} from ${r.remote}: ${r.before.slice(0, 8)} → ${r.after.slice(0, 8)} (fast-forward)`);
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
console.log(`already up to date: ${r.branch} @ ${r.after.slice(0, 8)}`);
|
|
861
|
+
}
|
|
862
|
+
console.log(`index: +${stats.added} ~${stats.updated} -${stats.removed} (scanned ${stats.scanned})`);
|
|
863
|
+
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
console.error(`pull failed: ${e.message}`);
|
|
866
|
+
process.exit(2);
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
// Explicit push — open-memex never pushes on its own (D36).
|
|
871
|
+
if (cmd === "push") {
|
|
872
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
873
|
+
const root = projectRoot();
|
|
874
|
+
try {
|
|
875
|
+
const r = new GitProvider().push(root);
|
|
876
|
+
syncScope(project.key, "push");
|
|
877
|
+
console.log(`pushed ${r.branch} to ${r.remote} @ ${r.head.slice(0, 8)}`);
|
|
878
|
+
}
|
|
879
|
+
catch (e) {
|
|
880
|
+
console.error(`push failed: ${e.message}`);
|
|
881
|
+
process.exit(2);
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
// §9 / D40: portable export bundle (markdown + manifest).
|
|
886
|
+
if (cmd === "export") {
|
|
887
|
+
const { exportMemories } = await import("./export.js");
|
|
888
|
+
const flags = parseFlags(rest);
|
|
889
|
+
const all = flags["all"] === "true" || flags["a"] === "true" || rest.includes("--all") || rest.includes("-a");
|
|
890
|
+
const scopeFlag = flags["scope"] ?? "project";
|
|
891
|
+
// parseFlags only handles `--` flags; `-o <file>` is picked up here.
|
|
892
|
+
const oIdx = rest.findIndex((a) => a === "-o");
|
|
893
|
+
const outFile = flags["o"] ?? flags["output"] ?? (oIdx >= 0 ? rest[oIdx + 1] : undefined);
|
|
894
|
+
const scopeKeys = scopeFlag === "both"
|
|
895
|
+
? [project.key, PERSONAL_SCOPE.key]
|
|
896
|
+
: scopeFlag === "personal"
|
|
897
|
+
? [PERSONAL_SCOPE.key]
|
|
898
|
+
: [project.key];
|
|
899
|
+
try {
|
|
900
|
+
const r = exportMemories({
|
|
901
|
+
scopeKeys,
|
|
902
|
+
type: flags["type"],
|
|
903
|
+
tag: flags["tag"],
|
|
904
|
+
includePrivate: all,
|
|
905
|
+
outFile,
|
|
906
|
+
});
|
|
907
|
+
console.log(`exported ${r.exported} memories → ${r.file}`);
|
|
908
|
+
if (!r.includePrivate && r.skippedPrivate > 0) {
|
|
909
|
+
console.log(`skipped ${r.skippedPrivate} private memories (use --all to include them)`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch (e) {
|
|
913
|
+
console.error(`export failed: ${e.message}`);
|
|
914
|
+
process.exit(2);
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (cmd === "import") {
|
|
919
|
+
const { importBundle } = await import("./export.js");
|
|
920
|
+
const flags = parseFlags(rest);
|
|
921
|
+
const bundle = positionalArgs(rest)[0];
|
|
922
|
+
if (!bundle) {
|
|
923
|
+
console.error(`usage: open-memex import <bundle.tar.gz> [--dry-run]`);
|
|
924
|
+
process.exit(2);
|
|
925
|
+
}
|
|
926
|
+
const dryRun = flags["dry-run"] === "true";
|
|
927
|
+
try {
|
|
928
|
+
const r = importBundle(bundle, { projectScopeKey: project.key, dryRun });
|
|
929
|
+
if (!dryRun) {
|
|
930
|
+
syncScope(project.key, "cli");
|
|
931
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
932
|
+
}
|
|
933
|
+
console.log(`${dryRun ? "DRY RUN: " : ""}imported ${r.imported}, skipped ${r.skippedIdentical} identical`);
|
|
934
|
+
for (const c of r.skippedConflict) {
|
|
935
|
+
console.log(` conflict (kept existing): ${c.id} from ${c.file}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
catch (e) {
|
|
939
|
+
console.error(`import failed: ${e.message}`);
|
|
940
|
+
process.exit(2);
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
// Phase 3: distill project memories into a proposed AGENTS.md snippet.
|
|
945
|
+
if (cmd === "distill-agents") {
|
|
946
|
+
const { distillAgentsMarkdown } = await import("./distill-agents.js");
|
|
947
|
+
const flags = parseFlags(rest);
|
|
948
|
+
const scopeFlag = flags["scope"] ?? "project";
|
|
949
|
+
const scopeKeys = scopeFlag === "personal" ? [PERSONAL_SCOPE.key] : [project.key];
|
|
950
|
+
const types = flags["type"]
|
|
951
|
+
? flags["type"].split(",").map((t) => t.trim()).filter(Boolean)
|
|
952
|
+
: undefined;
|
|
953
|
+
const limit = flags["limit"] ? parseInt(flags["limit"], 10) : undefined;
|
|
954
|
+
const oIdx = rest.findIndex((a) => a === "-o");
|
|
955
|
+
const outFile = oIdx >= 0 ? rest[oIdx + 1] : undefined;
|
|
956
|
+
try {
|
|
957
|
+
const md = distillAgentsMarkdown({ scopeKeys, types, limit });
|
|
958
|
+
if (!md) {
|
|
959
|
+
console.log("no distillable memories found (decisions, constraints, lessons, gotchas, howtos)");
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (outFile) {
|
|
963
|
+
fs.writeFileSync(outFile, md, "utf8");
|
|
964
|
+
console.log(`wrote proposed AGENTS.md snippet → ${outFile} (review and merge by hand)`);
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
console.log(md);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
catch (e) {
|
|
971
|
+
console.error(`distill-agents failed: ${e.message}`);
|
|
972
|
+
process.exit(2);
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (cmd === "pr-status") {
|
|
977
|
+
const flags = parseFlags(rest);
|
|
978
|
+
try {
|
|
979
|
+
const st = getPrStatus();
|
|
980
|
+
console.log(formatPrStatus(st));
|
|
981
|
+
if (flags.apply === "true") {
|
|
982
|
+
const applied = applyPrStatus(st);
|
|
983
|
+
if (applied.length === 0) {
|
|
984
|
+
console.log(`nothing to apply.`);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
for (const a of applied) {
|
|
988
|
+
console.log(`applied ${a.memoryId.slice(0, 8)}: ${a.from} → ${a.to} (by ${a.by})`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
catch (e) {
|
|
994
|
+
console.error(`pr-status failed: ${e.message}`);
|
|
995
|
+
process.exit(2);
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (cmd === "submit") {
|
|
1000
|
+
const flags = parseFlags(rest);
|
|
1001
|
+
const ids = positionalArgs(rest);
|
|
1002
|
+
if (ids.length === 0)
|
|
1003
|
+
usage();
|
|
1004
|
+
syncScope(project.key, "submit");
|
|
1005
|
+
try {
|
|
1006
|
+
const r = submitMemories(ids, { branch: flags.branch, base: flags.base });
|
|
1007
|
+
for (const s of r.submitted) {
|
|
1008
|
+
console.log(`submitted ${s.id} → ${path.relative(process.cwd(), s.filePath)} [${s.reviewState}]`);
|
|
1009
|
+
}
|
|
1010
|
+
for (const id of r.skippedIdentical) {
|
|
1011
|
+
console.log(`already on branch: ${id} (identical content — outbox copy removed)`);
|
|
1012
|
+
}
|
|
1013
|
+
if (!r.committed) {
|
|
1014
|
+
console.log(`nothing new to commit — branch ${r.branch} already holds these memories.`);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
console.log(`committed on ${r.branch}.`);
|
|
1018
|
+
}
|
|
1019
|
+
console.log(`Next (needs your approval — open-memex never pushes for you):`);
|
|
1020
|
+
console.log(` ${r.pushCommand}`);
|
|
1021
|
+
console.log(` ${r.prCommand}`);
|
|
1022
|
+
}
|
|
1023
|
+
catch (e) {
|
|
1024
|
+
console.error(`submit failed: ${e.message}`);
|
|
1025
|
+
process.exit(2);
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
460
1029
|
if (cmd === "scopes") {
|
|
461
1030
|
const { memories } = paths();
|
|
462
1031
|
if (!fs.existsSync(memories)) {
|