open-memex 0.3.0 → 0.4.0-alpha.4
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 +12 -1
- package/README.md +78 -8
- package/README.zh-CN.md +67 -7
- package/dist/cli.js +389 -12
- package/dist/config.js +10 -0
- package/dist/github.js +215 -0
- package/dist/index.js +6 -0
- package/dist/init.js +33 -18
- package/dist/mcp.js +71 -9
- package/dist/paths.js +31 -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 +49 -8
- package/dist/store/sync.js +74 -6
- package/dist/submit.js +339 -0
- package/dist/tools/ops.js +163 -6
- package/docs/USER-GUIDE.md +195 -0
- package/docs/USER-GUIDE.zh-CN.md +160 -0
- package/docs/V2-DESIGN.md +129 -6
- package/package.json +1 -1
- package/scripts/smoke-mcp.ts +2 -2
- package/src/cli.ts +401 -13
- package/src/config.ts +16 -0
- package/src/github.ts +257 -0
- package/src/index.ts +6 -0
- package/src/init.ts +33 -17
- package/src/mcp.ts +117 -8
- package/src/paths.ts +32 -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 +83 -8
- package/src/store/sync.ts +85 -4
- package/src/submit.ts +411 -0
- package/src/tools/ops.ts +192 -7
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,11 @@ 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
15
|
import { paths } from "./paths.js";
|
|
13
16
|
import { redact } from "./redact.js";
|
|
@@ -15,6 +18,168 @@ 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
|
+
submit: `Move outbox drafts into a git branch for review: creates a branch
|
|
110
|
+
(default mem/sync-*), copies the drafts into the repo memory dir as proposed
|
|
111
|
+
(local-approved copies keep their approval), commits locally, and moves the
|
|
112
|
+
outbox originals out. Prints the push and PR commands — those need your
|
|
113
|
+
explicit approval and are never run automatically.
|
|
114
|
+
|
|
115
|
+
Usage: open-memex submit <id...> [--onto <branch>] [--base <branch>]
|
|
116
|
+
|
|
117
|
+
Flags:
|
|
118
|
+
--onto submit onto the current branch instead of creating mem/sync-*
|
|
119
|
+
--base base branch for the PR suggestion (default: the branch you're on)
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
open-memex submit 01ABC 01DEF`,
|
|
123
|
+
"pr-status": `Map the current branch's GitHub PR state back onto review_state:
|
|
124
|
+
merged → published, approval → approved (approved_by = the reviewer),
|
|
125
|
+
changes-requested → suggestion only (never auto-rejects).
|
|
126
|
+
Each memory in the PR is mapped independently; a human rejection is never
|
|
127
|
+
overwritten. Report-only by default.
|
|
128
|
+
|
|
129
|
+
Usage: open-memex pr-status [--apply]
|
|
130
|
+
|
|
131
|
+
Flags:
|
|
132
|
+
--apply write the transitions locally (still never pushes)`,
|
|
133
|
+
reindex: `Rebuild the SQLite index from the markdown files.
|
|
134
|
+
|
|
135
|
+
Usage: open-memex reindex`,
|
|
136
|
+
scopes: `List the known scopes (personal + project).
|
|
137
|
+
|
|
138
|
+
Usage: open-memex scopes`,
|
|
139
|
+
migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
|
|
140
|
+
|
|
141
|
+
Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict newer|overwrite|skip]
|
|
142
|
+
open-memex migrate --to-v2 [--dry-run]
|
|
143
|
+
|
|
144
|
+
Flags:
|
|
145
|
+
--from / --to scope keys (default: current project → personal)
|
|
146
|
+
--dry-run preview without moving anything
|
|
147
|
+
--on-conflict newer (default), overwrite, or skip
|
|
148
|
+
--to-v2 convert a legacy my-o-memory data dir to the v2 layout
|
|
149
|
+
|
|
150
|
+
Always preview with --dry-run first; nothing moves without confirmation.`,
|
|
151
|
+
mcp: `Start the stdio MCP server (the same server editors connect to).
|
|
152
|
+
|
|
153
|
+
Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
154
|
+
|
|
155
|
+
Flags:
|
|
156
|
+
--print-config print the MCP client config instead of starting the server`,
|
|
157
|
+
init: `One-command project setup: writes the MCP config for your editor and the
|
|
158
|
+
agent memory instructions. Existing files are merged, never clobbered.
|
|
159
|
+
|
|
160
|
+
Usage: open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
161
|
+
[--instructions personal|project] [--force] [--yes]
|
|
162
|
+
|
|
163
|
+
Flags:
|
|
164
|
+
--client editor to configure (default: auto-detect)
|
|
165
|
+
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
166
|
+
--force overwrite existing config
|
|
167
|
+
--yes accept all defaults, never prompt`,
|
|
168
|
+
config: `Show config, or set a key.
|
|
169
|
+
|
|
170
|
+
Usage: open-memex config [set <key> <value>]
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
open-memex config set sync.autoPull false`,
|
|
174
|
+
capture: `Preview what the keyword-capture watcher would extract from text.
|
|
175
|
+
|
|
176
|
+
Usage: open-memex capture --dry-run "text"`,
|
|
177
|
+
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
178
|
+
storage writability, then boots a real MCP server and runs initialize +
|
|
179
|
+
tools/list against it — all eleven tools must show up.
|
|
180
|
+
|
|
181
|
+
Usage: open-memex doctor`,
|
|
182
|
+
};
|
|
18
183
|
function usage(exitCode = 1) {
|
|
19
184
|
console.log(`open-memex CLI
|
|
20
185
|
|
|
@@ -26,6 +191,12 @@ Usage:
|
|
|
26
191
|
open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
27
192
|
open-memex status <id> active|deprecated|retracted|archived
|
|
28
193
|
open-memex forget <id>
|
|
194
|
+
open-memex propose <id...> --to project [--local-approve]
|
|
195
|
+
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
196
|
+
open-memex resolve [id-or-path]
|
|
197
|
+
open-memex sync-status
|
|
198
|
+
open-memex submit <id...> [--onto <branch>] [--base <branch>]
|
|
199
|
+
open-memex pr-status [--apply]
|
|
29
200
|
open-memex reindex
|
|
30
201
|
open-memex scopes
|
|
31
202
|
open-memex migrate [--from <key>] [--to <key>]
|
|
@@ -63,7 +234,9 @@ the \`--from\` key when migrating.
|
|
|
63
234
|
git remote after memories were already stored under the cwd-based key.
|
|
64
235
|
\`migrate --to-v2\` converts v1 memory files to the v2 format (§19):
|
|
65
236
|
user→personal scope rename, epoch→RFC 3339 times, priority→importance,
|
|
66
|
-
type: instruction→role split. Always preview with --dry-run first
|
|
237
|
+
type: instruction→role split. Always preview with --dry-run first.
|
|
238
|
+
|
|
239
|
+
Run \`open-memex <command> --help\` for details on a single command.`);
|
|
67
240
|
process.exit(exitCode);
|
|
68
241
|
}
|
|
69
242
|
function parseFlags(argv) {
|
|
@@ -163,6 +336,16 @@ async function main() {
|
|
|
163
336
|
console.log(`open-memex ${pkg.version}`);
|
|
164
337
|
return;
|
|
165
338
|
}
|
|
339
|
+
// Per-command help: `open-memex <command> --help`. Checked before loadConfig()
|
|
340
|
+
// so it works even when the environment is broken.
|
|
341
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
342
|
+
const h = COMMAND_HELP[cmd];
|
|
343
|
+
if (h) {
|
|
344
|
+
console.log(`open-memex ${cmd}\n\n${h}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
usage(0);
|
|
348
|
+
}
|
|
166
349
|
const cfg = loadConfig();
|
|
167
350
|
const project = resolveProjectScope(process.cwd());
|
|
168
351
|
// `migrate --to-v2` is a pure file operation (V2-DESIGN §19) — it runs
|
|
@@ -294,15 +477,15 @@ async function main() {
|
|
|
294
477
|
return;
|
|
295
478
|
}
|
|
296
479
|
if (cmd === "reindex") {
|
|
297
|
-
const a = syncScope(project.key);
|
|
298
|
-
const b = syncScope(PERSONAL_SCOPE.key);
|
|
480
|
+
const a = syncScope(project.key, "cli");
|
|
481
|
+
const b = syncScope(PERSONAL_SCOPE.key, "cli");
|
|
299
482
|
console.log(`reindexed. project: +${a.added} ~${a.updated} -${a.removed} (scanned ${a.scanned}), personal: +${b.added} ~${b.updated} -${b.removed} (scanned ${b.scanned})`);
|
|
300
483
|
return;
|
|
301
484
|
}
|
|
302
485
|
if (cmd === "list") {
|
|
303
486
|
const flags = parseFlags(rest);
|
|
304
487
|
const s = resolveCliScope(flags, project);
|
|
305
|
-
syncScope(s.key);
|
|
488
|
+
syncScope(s.key, "cli");
|
|
306
489
|
const hits = list(s.key, {
|
|
307
490
|
type: flags.type,
|
|
308
491
|
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
@@ -313,7 +496,8 @@ async function main() {
|
|
|
313
496
|
}
|
|
314
497
|
for (const h of hits) {
|
|
315
498
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
316
|
-
|
|
499
|
+
const rev = hitStateLabel(h);
|
|
500
|
+
console.log(`[${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
317
501
|
}
|
|
318
502
|
return;
|
|
319
503
|
}
|
|
@@ -322,8 +506,8 @@ async function main() {
|
|
|
322
506
|
if (!query || query.startsWith("--"))
|
|
323
507
|
usage();
|
|
324
508
|
const flags = parseFlags(rest.slice(1));
|
|
325
|
-
syncScope(project.key);
|
|
326
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
509
|
+
syncScope(project.key, "cli");
|
|
510
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
327
511
|
const keys = flags.scope === "personal" || flags.scope === "user"
|
|
328
512
|
? [PERSONAL_SCOPE.key]
|
|
329
513
|
: flags.scope === "project"
|
|
@@ -341,7 +525,8 @@ async function main() {
|
|
|
341
525
|
for (const h of hits) {
|
|
342
526
|
const tag = h.scope_key === PERSONAL_SCOPE.key ? "personal" : "project";
|
|
343
527
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
344
|
-
|
|
528
|
+
const rev = hitStateLabel(h);
|
|
529
|
+
console.log(`[${tag}/${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
345
530
|
}
|
|
346
531
|
return;
|
|
347
532
|
}
|
|
@@ -384,6 +569,12 @@ async function main() {
|
|
|
384
569
|
updated_at: rfc,
|
|
385
570
|
supersedes: null,
|
|
386
571
|
superseded_by: null,
|
|
572
|
+
review_state: "draft",
|
|
573
|
+
proposed_by: null,
|
|
574
|
+
approved_by: null,
|
|
575
|
+
derived_from: null,
|
|
576
|
+
review_note: null,
|
|
577
|
+
review_history: [],
|
|
387
578
|
};
|
|
388
579
|
const { filePath } = writeMemoryFile(fm, red);
|
|
389
580
|
const mf = readMemoryFile(filePath);
|
|
@@ -447,16 +638,202 @@ async function main() {
|
|
|
447
638
|
const id = rest[0];
|
|
448
639
|
if (!id)
|
|
449
640
|
usage();
|
|
450
|
-
const row = db().prepare(`SELECT
|
|
641
|
+
const row = db().prepare(`SELECT file_path FROM memories WHERE id = ?`).get(id);
|
|
451
642
|
if (!row) {
|
|
452
643
|
console.error("not found");
|
|
453
644
|
process.exit(1);
|
|
454
645
|
}
|
|
455
|
-
|
|
646
|
+
// Delete by indexed file_path — location-agnostic (appdata or in-repo, 2B/D24).
|
|
647
|
+
if (row.file_path) {
|
|
648
|
+
try {
|
|
649
|
+
fs.unlinkSync(row.file_path);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
/* already gone */
|
|
653
|
+
}
|
|
654
|
+
}
|
|
456
655
|
deleteFromIndex(id);
|
|
457
656
|
console.log(`deleted ${id}`);
|
|
458
657
|
return;
|
|
459
658
|
}
|
|
659
|
+
/** Positional args with flags (and their values) skipped. */
|
|
660
|
+
function positionalArgs(argv) {
|
|
661
|
+
const out = [];
|
|
662
|
+
for (let i = 0; i < argv.length; i++) {
|
|
663
|
+
const a = argv[i];
|
|
664
|
+
if (a.startsWith("--")) {
|
|
665
|
+
const val = argv[i + 1];
|
|
666
|
+
if (val !== undefined && !val.startsWith("--"))
|
|
667
|
+
i++; // skip the flag's value
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
out.push(a);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
675
|
+
if (cmd === "propose") {
|
|
676
|
+
const flags = parseFlags(rest);
|
|
677
|
+
const ids = positionalArgs(rest);
|
|
678
|
+
if (ids.length === 0)
|
|
679
|
+
usage();
|
|
680
|
+
const to = flags.to ?? "project";
|
|
681
|
+
if (to !== "project") {
|
|
682
|
+
console.error(`propose --to "${to}" is not supported yet (org sharing is Phase 4).`);
|
|
683
|
+
process.exit(2);
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const batch = proposeMemories(ids, { localApprove: flags["local-approve"] === "true" });
|
|
687
|
+
for (const { sourceId, result: r } of batch) {
|
|
688
|
+
console.log(`proposed ${sourceId} → ${r.id} [${r.reviewState}]`);
|
|
689
|
+
}
|
|
690
|
+
console.log(`Next: these are drafts in the outbox (appdata) — nothing is in git yet.`);
|
|
691
|
+
console.log(` open-memex sync-status`);
|
|
692
|
+
console.log(` open-memex submit ${batch.map(({ result: r }) => r.id).join(" ")} # new branch, local commit; prints push + PR commands`);
|
|
693
|
+
}
|
|
694
|
+
catch (e) {
|
|
695
|
+
console.error(`propose failed: ${e.message}`);
|
|
696
|
+
process.exit(2);
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (cmd === "promote") {
|
|
701
|
+
const flags = parseFlags(rest);
|
|
702
|
+
const id = rest.find((a) => !a.startsWith("--"));
|
|
703
|
+
if (!id)
|
|
704
|
+
usage();
|
|
705
|
+
try {
|
|
706
|
+
const r = promoteMemory(id, {
|
|
707
|
+
reject: flags.reject === "true",
|
|
708
|
+
resubmit: flags.resubmit === "true",
|
|
709
|
+
note: flags.note,
|
|
710
|
+
by: flags.by,
|
|
711
|
+
});
|
|
712
|
+
console.log(`promote ${r.id}: ${r.from} → ${r.to}`);
|
|
713
|
+
if (r.to === "published") {
|
|
714
|
+
console.log(` merged to the shared branch? Teammates pick it up with an explicit pull.`);
|
|
715
|
+
}
|
|
716
|
+
if (r.to === "rejected") {
|
|
717
|
+
console.log(` not deleted — the file stays on your branch. Your call:`);
|
|
718
|
+
console.log(` accept: close the PR and delete the branch;`);
|
|
719
|
+
console.log(` revise: edit the file, then \`open-memex promote ${r.id} --resubmit\`;`);
|
|
720
|
+
console.log(` keep: leave it as a [rejected] record.`);
|
|
721
|
+
}
|
|
722
|
+
if (r.history.length > 0) {
|
|
723
|
+
console.log(` history (${r.history.length}):`);
|
|
724
|
+
for (const h of formatReviewHistory(r.history))
|
|
725
|
+
console.log(h);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
catch (e) {
|
|
729
|
+
console.error(`promote failed: ${e.message}`);
|
|
730
|
+
process.exit(2);
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (cmd === "resolve") {
|
|
735
|
+
const target = rest.find((a) => !a.startsWith("--"));
|
|
736
|
+
try {
|
|
737
|
+
if (!target) {
|
|
738
|
+
const conflicts = listConflicts();
|
|
739
|
+
if (conflicts.length === 0) {
|
|
740
|
+
console.log("(no conflicted memory files)");
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
for (const c of conflicts)
|
|
744
|
+
console.log(`conflicted: ${c.id} ${c.filePath}`);
|
|
745
|
+
console.log(`\nRun \`open-memex resolve <id>\` to attempt a field-level 3-way merge.`);
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const outcome = resolveConflict(target);
|
|
750
|
+
if (!outcome.ok) {
|
|
751
|
+
console.error(`cannot auto-resolve ${path.basename(outcome.filePath)} — semantic conflicts need a human:`);
|
|
752
|
+
for (const c of outcome.conflicts) {
|
|
753
|
+
console.error(` ${c.field}:`);
|
|
754
|
+
console.error(` base: ${c.base}`);
|
|
755
|
+
console.error(` ours: ${c.ours}`);
|
|
756
|
+
console.error(` theirs: ${c.theirs}`);
|
|
757
|
+
}
|
|
758
|
+
console.error(`Edit the file manually, then \`git add\` it. Nothing was written.`);
|
|
759
|
+
process.exit(3);
|
|
760
|
+
}
|
|
761
|
+
console.log(`resolved ${path.basename(outcome.filePath)}`);
|
|
762
|
+
if (outcome.autoMerged.length > 0)
|
|
763
|
+
console.log(` auto-merged: ${outcome.autoMerged.join(", ")}`);
|
|
764
|
+
console.log(` next: git add ${path.relative(process.cwd(), outcome.filePath)}`);
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
console.error(`resolve failed: ${e.message}`);
|
|
768
|
+
process.exit(2);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (cmd === "sync-status") {
|
|
773
|
+
syncScope(project.key, "cli");
|
|
774
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
775
|
+
try {
|
|
776
|
+
console.log(formatSyncStatus(getSyncStatus()));
|
|
777
|
+
}
|
|
778
|
+
catch (e) {
|
|
779
|
+
console.error(`sync-status failed: ${e.message}`);
|
|
780
|
+
process.exit(2);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (cmd === "pr-status") {
|
|
785
|
+
const flags = parseFlags(rest);
|
|
786
|
+
try {
|
|
787
|
+
const st = getPrStatus();
|
|
788
|
+
console.log(formatPrStatus(st));
|
|
789
|
+
if (flags.apply === "true") {
|
|
790
|
+
const applied = applyPrStatus(st);
|
|
791
|
+
if (applied.length === 0) {
|
|
792
|
+
console.log(`nothing to apply.`);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
for (const a of applied) {
|
|
796
|
+
console.log(`applied ${a.memoryId.slice(0, 8)}: ${a.from} → ${a.to} (by ${a.by})`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (e) {
|
|
802
|
+
console.error(`pr-status failed: ${e.message}`);
|
|
803
|
+
process.exit(2);
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (cmd === "submit") {
|
|
808
|
+
const flags = parseFlags(rest);
|
|
809
|
+
const ids = positionalArgs(rest);
|
|
810
|
+
if (ids.length === 0)
|
|
811
|
+
usage();
|
|
812
|
+
syncScope(project.key, "submit");
|
|
813
|
+
try {
|
|
814
|
+
const r = submitMemories(ids, { onto: flags.onto, base: flags.base });
|
|
815
|
+
for (const s of r.submitted) {
|
|
816
|
+
console.log(`submitted ${s.id} → ${path.relative(process.cwd(), s.filePath)} [${s.reviewState}]`);
|
|
817
|
+
}
|
|
818
|
+
for (const id of r.skippedIdentical) {
|
|
819
|
+
console.log(`already on branch: ${id} (identical content — outbox copy removed)`);
|
|
820
|
+
}
|
|
821
|
+
if (!r.committed) {
|
|
822
|
+
console.log(`nothing new to commit — branch ${r.branch} already holds these memories.`);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
console.log(`committed on ${r.branch}.`);
|
|
826
|
+
}
|
|
827
|
+
console.log(`Next (needs your approval — open-memex never pushes for you):`);
|
|
828
|
+
console.log(` ${r.pushCommand}`);
|
|
829
|
+
console.log(` ${r.prCommand}`);
|
|
830
|
+
}
|
|
831
|
+
catch (e) {
|
|
832
|
+
console.error(`submit failed: ${e.message}`);
|
|
833
|
+
process.exit(2);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
460
837
|
if (cmd === "scopes") {
|
|
461
838
|
const { memories } = paths();
|
|
462
839
|
if (!fs.existsSync(memories)) {
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,7 @@ export const DEFAULT_CONFIG = {
|
|
|
33
33
|
// Add only your own extra patterns here.
|
|
34
34
|
redactPatterns: [],
|
|
35
35
|
logLevel: "info",
|
|
36
|
+
memoryDir: ".ai/open-memex",
|
|
36
37
|
};
|
|
37
38
|
function stripJsonComments(raw) {
|
|
38
39
|
// Line comments
|
|
@@ -63,12 +64,21 @@ function toNonNegInt(v) {
|
|
|
63
64
|
throw new Error("must be a non-negative integer");
|
|
64
65
|
return n;
|
|
65
66
|
}
|
|
67
|
+
function toRelativeDir(v) {
|
|
68
|
+
if (typeof v !== "string" || !v.trim())
|
|
69
|
+
throw new Error("must be a non-empty relative directory");
|
|
70
|
+
const t = v.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
71
|
+
if (!t || path.isAbsolute(t) || t.split("/").includes(".."))
|
|
72
|
+
throw new Error('must be a relative path without ".." (e.g. ".ai/open-memex")');
|
|
73
|
+
return t;
|
|
74
|
+
}
|
|
66
75
|
/** Keys users may change via `open-memex config set <key> <value>`, with validators. */
|
|
67
76
|
export const SETTABLE_KEYS = {
|
|
68
77
|
maxProjectMemories: toNonNegInt,
|
|
69
78
|
maxProfileItems: toNonNegInt,
|
|
70
79
|
injectOnFirstTurn: toBool,
|
|
71
80
|
keywordCaptureEnabled: toBool,
|
|
81
|
+
memoryDir: toRelativeDir,
|
|
72
82
|
logLevel: (v) => {
|
|
73
83
|
if (v !== "info" && v !== "debug")
|
|
74
84
|
throw new Error('must be "info" or "debug"');
|