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/src/github.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2B/D32: GitHub PR ↔ review_state mapping.
|
|
3
|
+
*
|
|
4
|
+
* Memories ride into review inside a branch + PR (dedicated memory PR or
|
|
5
|
+
* together with code). The PR's lifecycle on GitHub is the team's review
|
|
6
|
+
* signal; this module reads it back and maps it onto each memory's own
|
|
7
|
+
* review_state:
|
|
8
|
+
*
|
|
9
|
+
* PR merged → proposed/approved → published
|
|
10
|
+
* PR reviewDecision == APPROVED → proposed → approved (approved_by = reviewer login)
|
|
11
|
+
* PR reviewDecision == CHANGES_REQUESTED → suggestion only, printed for the human
|
|
12
|
+
*
|
|
13
|
+
* Per-memory independence: every memory on the branch keeps its own state.
|
|
14
|
+
* A locally `rejected` memory is never flipped by a PR approval, and a
|
|
15
|
+
* changes-requested review never auto-rejects — the human runs `promote`.
|
|
16
|
+
*
|
|
17
|
+
* Report by default; `--apply` performs the mapped transitions locally
|
|
18
|
+
* (file rewrite + index, never a push — inside the D27 local-git line).
|
|
19
|
+
* Needs the GitHub CLI (`gh`) authenticated on the user's machine.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { loadConfig } from "./config.ts";
|
|
26
|
+
import { projectRoot } from "./paths.ts";
|
|
27
|
+
import { parse as parseMemory, type ReviewState } from "./store/markdown.ts";
|
|
28
|
+
import { iterInRepoMemoryFiles } from "./store/markdown.ts";
|
|
29
|
+
import { promoteMemory, currentAuthor } from "./review.ts";
|
|
30
|
+
|
|
31
|
+
function fail(msg: string): never {
|
|
32
|
+
throw new Error(`[open-memex] ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sh(cmd: string, args: string[], cwd: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
const err = e as { stderr?: string; message?: string };
|
|
40
|
+
const detail = (err.stderr ?? err.message ?? "").trim().split("\n")[0];
|
|
41
|
+
fail(`${cmd} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PrInfo {
|
|
46
|
+
number: number;
|
|
47
|
+
url: string;
|
|
48
|
+
state: "open" | "closed"; // GitHub PR state
|
|
49
|
+
merged: boolean;
|
|
50
|
+
mergedAt: string | null;
|
|
51
|
+
reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
|
|
52
|
+
approvedBy: string[];
|
|
53
|
+
changesRequestedBy: string[];
|
|
54
|
+
base: string;
|
|
55
|
+
head: string;
|
|
56
|
+
title: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface GhPrJson {
|
|
60
|
+
number: number;
|
|
61
|
+
url: string;
|
|
62
|
+
state: string;
|
|
63
|
+
mergedAt: string | null;
|
|
64
|
+
isMerged?: boolean;
|
|
65
|
+
reviewDecision?: string | null;
|
|
66
|
+
baseRefName: string;
|
|
67
|
+
headRefName: string;
|
|
68
|
+
title: string;
|
|
69
|
+
reviews?: Array<{ author: { login: string }; state: string; submittedAt: string }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Read the PR for a branch (default: current branch) via the GitHub CLI. */
|
|
73
|
+
export function getPrInfo(branch?: string): PrInfo | null {
|
|
74
|
+
const root = projectRoot();
|
|
75
|
+
// `gh` must exist and be authenticated on the user's machine.
|
|
76
|
+
try {
|
|
77
|
+
execFileSync("gh", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
78
|
+
} catch {
|
|
79
|
+
fail("the GitHub CLI (`gh`) is not installed — install it and run `gh auth login` first.");
|
|
80
|
+
}
|
|
81
|
+
const ref = branch ?? sh("git", ["branch", "--show-current"], root);
|
|
82
|
+
if (!ref) fail("not on a branch — check out the memory branch first.");
|
|
83
|
+
let raw: string;
|
|
84
|
+
try {
|
|
85
|
+
raw = sh("gh", ["pr", "view", ref, "--json",
|
|
86
|
+
"number,url,state,mergedAt,reviewDecision,baseRefName,headRefName,title,reviews"], root);
|
|
87
|
+
} catch {
|
|
88
|
+
return null; // no PR for this branch yet
|
|
89
|
+
}
|
|
90
|
+
let j: GhPrJson;
|
|
91
|
+
try {
|
|
92
|
+
j = JSON.parse(raw) as GhPrJson;
|
|
93
|
+
} catch {
|
|
94
|
+
fail(`could not parse \`gh pr view\` output for branch ${ref}.`);
|
|
95
|
+
}
|
|
96
|
+
const approvedBy: string[] = [];
|
|
97
|
+
const changesRequestedBy: string[] = [];
|
|
98
|
+
// Latest review per author wins.
|
|
99
|
+
const latest = new Map<string, { state: string; at: string }>();
|
|
100
|
+
for (const r of j.reviews ?? []) {
|
|
101
|
+
const login = r.author?.login;
|
|
102
|
+
if (!login) continue;
|
|
103
|
+
const prev = latest.get(login);
|
|
104
|
+
if (!prev || r.submittedAt >= prev.at) latest.set(login, { state: r.state, at: r.submittedAt });
|
|
105
|
+
}
|
|
106
|
+
for (const [login, r] of latest) {
|
|
107
|
+
if (r.state === "APPROVED") approvedBy.push(login);
|
|
108
|
+
else if (r.state === "CHANGES_REQUESTED") changesRequestedBy.push(login);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
number: j.number,
|
|
112
|
+
url: j.url,
|
|
113
|
+
state: j.state === "OPEN" ? "open" : "closed",
|
|
114
|
+
merged: !!j.mergedAt,
|
|
115
|
+
mergedAt: j.mergedAt,
|
|
116
|
+
reviewDecision: (j.reviewDecision as PrInfo["reviewDecision"]) ?? null,
|
|
117
|
+
approvedBy,
|
|
118
|
+
changesRequestedBy,
|
|
119
|
+
base: j.baseRefName,
|
|
120
|
+
head: j.headRefName,
|
|
121
|
+
title: j.title,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface MemoryPrMapping {
|
|
126
|
+
memoryId: string;
|
|
127
|
+
from: ReviewState;
|
|
128
|
+
/** Null = no automatic transition; see reason. */
|
|
129
|
+
suggestedTo: ReviewState | null;
|
|
130
|
+
reason: string;
|
|
131
|
+
approvedBy: string | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PrStatus {
|
|
135
|
+
branch: string;
|
|
136
|
+
pr: PrInfo | null;
|
|
137
|
+
mappings: MemoryPrMapping[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Map the branch PR's state onto each in-repo memory's review_state. */
|
|
141
|
+
export function getPrStatus(branch?: string): PrStatus {
|
|
142
|
+
const root = projectRoot();
|
|
143
|
+
const cfg = loadConfig();
|
|
144
|
+
const current = branch ?? sh("git", ["branch", "--show-current"], root);
|
|
145
|
+
const pr = getPrInfo(current);
|
|
146
|
+
|
|
147
|
+
const mappings: MemoryPrMapping[] = [];
|
|
148
|
+
for (const fp of iterInRepoMemoryFiles(root, cfg.memoryDir)) {
|
|
149
|
+
const raw = fs.readFileSync(fp, "utf8");
|
|
150
|
+
const parsed = parseMemory(raw);
|
|
151
|
+
if (!parsed) continue;
|
|
152
|
+
const from = parsed.fm.review_state ?? "draft";
|
|
153
|
+
const id = parsed.fm.id;
|
|
154
|
+
if (!pr) {
|
|
155
|
+
mappings.push({ memoryId: id, from, suggestedTo: null, reason: `no PR for branch ${current} yet`, approvedBy: null });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (from === "published") {
|
|
159
|
+
mappings.push({ memoryId: id, from, suggestedTo: null, reason: "already published", approvedBy: null });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (from === "rejected") {
|
|
163
|
+
mappings.push({ memoryId: id, from, suggestedTo: null, reason: `stays rejected — a PR ${pr.merged ? "merge" : "approval"} never overrides a human rejection`, approvedBy: null });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (pr.merged) {
|
|
167
|
+
if (from === "proposed" || from === "approved") {
|
|
168
|
+
mappings.push({ memoryId: id, from, suggestedTo: "published", reason: `PR #${pr.number} merged`, approvedBy: null });
|
|
169
|
+
} else {
|
|
170
|
+
mappings.push({ memoryId: id, from, suggestedTo: null, reason: `PR #${pr.number} merged, but memory is "${from}"`, approvedBy: null });
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (pr.reviewDecision === "APPROVED" && from === "proposed") {
|
|
175
|
+
const by = pr.approvedBy[0] ?? null;
|
|
176
|
+
mappings.push({
|
|
177
|
+
memoryId: id, from, suggestedTo: "approved",
|
|
178
|
+
reason: `PR #${pr.number} approved${by ? ` by @${by}` : ""}`, approvedBy: by,
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (pr.reviewDecision === "CHANGES_REQUESTED") {
|
|
183
|
+
const by = pr.changesRequestedBy[0] ?? null;
|
|
184
|
+
mappings.push({
|
|
185
|
+
memoryId: id, from, suggestedTo: null,
|
|
186
|
+
reason: `PR #${pr.number} has changes requested${by ? ` by @${by}` : ""} — run \`open-memex promote ${id} --reject --note "..."\` yourself if you agree`,
|
|
187
|
+
approvedBy: null,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
mappings.push({
|
|
192
|
+
memoryId: id, from, suggestedTo: null,
|
|
193
|
+
reason: pr.state === "open" ? `PR #${pr.number} still awaiting review` : `PR #${pr.number} is closed without merge`,
|
|
194
|
+
approvedBy: null,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return { branch: current, pr, mappings };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface PrApplyResult {
|
|
201
|
+
memoryId: string;
|
|
202
|
+
from: ReviewState;
|
|
203
|
+
to: ReviewState;
|
|
204
|
+
by: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Apply the mapped transitions locally (no push — D27). Advances step by
|
|
208
|
+
* step through the ladder, so a merged PR moves proposed → approved →
|
|
209
|
+
* published with each step in the audit trail. */
|
|
210
|
+
export function applyPrStatus(st: PrStatus): PrApplyResult[] {
|
|
211
|
+
const out: PrApplyResult[] = [];
|
|
212
|
+
const actor = currentAuthor();
|
|
213
|
+
const order: ReviewState[] = ["draft", "proposed", "approved", "published"];
|
|
214
|
+
for (const m of st.mappings) {
|
|
215
|
+
if (!m.suggestedTo || order.indexOf(m.from) >= order.indexOf(m.suggestedTo)) continue;
|
|
216
|
+
const by = m.approvedBy ?? actor;
|
|
217
|
+
const note = m.suggestedTo === "published" && st.pr
|
|
218
|
+
? `PR #${st.pr.number} merged`
|
|
219
|
+
: m.reason;
|
|
220
|
+
let cur = m.from;
|
|
221
|
+
while (order.indexOf(cur) < order.indexOf(m.suggestedTo)) {
|
|
222
|
+
const r = promoteMemory(m.memoryId, { by, note });
|
|
223
|
+
cur = r.to;
|
|
224
|
+
out.push({ memoryId: m.memoryId, from: r.from, to: r.to, by });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function formatPrStatus(st: PrStatus): string {
|
|
231
|
+
const lines: string[] = [];
|
|
232
|
+
lines.push(`branch: ${st.branch}`);
|
|
233
|
+
if (!st.pr) {
|
|
234
|
+
lines.push(`PR: none yet — push the branch and open one, then re-run.`);
|
|
235
|
+
} else {
|
|
236
|
+
const p = st.pr;
|
|
237
|
+
lines.push(`PR #${p.number}: ${p.title}`);
|
|
238
|
+
lines.push(` ${p.url}`);
|
|
239
|
+
lines.push(` state: ${p.merged ? `merged${p.mergedAt ? ` at ${p.mergedAt}` : ""}` : p.state}` +
|
|
240
|
+
(p.reviewDecision ? `, reviews: ${p.reviewDecision}` : ""));
|
|
241
|
+
if (p.approvedBy.length > 0) lines.push(` approved by: ${p.approvedBy.map((l) => "@" + l).join(", ")}`);
|
|
242
|
+
if (p.changesRequestedBy.length > 0) lines.push(` changes requested by: ${p.changesRequestedBy.map((l) => "@" + l).join(", ")}`);
|
|
243
|
+
}
|
|
244
|
+
if (st.mappings.length === 0) {
|
|
245
|
+
lines.push(`no in-repo memories on this branch.`);
|
|
246
|
+
} else {
|
|
247
|
+
lines.push(`memories (${st.mappings.length}):`);
|
|
248
|
+
for (const m of st.mappings) {
|
|
249
|
+
const arrow = m.suggestedTo ? ` → [${m.suggestedTo}]` : "";
|
|
250
|
+
lines.push(` ${m.memoryId.slice(0, 8)} [${m.from}]${arrow} ${m.reason}`);
|
|
251
|
+
}
|
|
252
|
+
if (st.mappings.some((m) => m.suggestedTo)) {
|
|
253
|
+
lines.push(`run with --apply to perform the mapped transitions locally (no push).`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -93,6 +93,12 @@ const plugin: Plugin = async ({ worktree, directory }) => {
|
|
|
93
93
|
updated_at: rfc,
|
|
94
94
|
supersedes: null,
|
|
95
95
|
superseded_by: null,
|
|
96
|
+
review_state: "draft",
|
|
97
|
+
proposed_by: null,
|
|
98
|
+
approved_by: null,
|
|
99
|
+
derived_from: null,
|
|
100
|
+
review_note: null,
|
|
101
|
+
review_history: [],
|
|
96
102
|
};
|
|
97
103
|
try {
|
|
98
104
|
const { filePath } = writeMemoryFile(fm, content);
|
package/src/init.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { execFileSync } from "node:child_process";
|
|
7
6
|
import { createInterface } from "node:readline/promises";
|
|
8
7
|
import { DEFAULT_CONFIG, saveConfig } from "./config.ts";
|
|
8
|
+
import { projectRoot } from "./paths.ts";
|
|
9
9
|
|
|
10
10
|
const MARKER = "<!-- open-memex -->";
|
|
11
11
|
|
|
@@ -50,8 +50,10 @@ const INSTRUCTIONS = `${MARKER}
|
|
|
50
50
|
> Applies only when the \`open-memex\` MCP server is available in this session
|
|
51
51
|
> (the \`memory_*\` tools exist). Otherwise ignore this section.
|
|
52
52
|
|
|
53
|
-
You have a local memory MCP server (\`open-memex\`) with
|
|
54
|
-
\`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`, \`memory_forget
|
|
53
|
+
You have a local memory MCP server (\`open-memex\`) with eleven tools:
|
|
54
|
+
\`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`, \`memory_forget\`,
|
|
55
|
+
\`memory_status\`, \`memory_submit\`, \`memory_propose\`, \`memory_promote\`, \`memory_resolve\`,
|
|
56
|
+
\`memory_pr_status\`.
|
|
55
57
|
|
|
56
58
|
- BE PROACTIVE. When the user shares something worth remembering across sessions
|
|
57
59
|
(a decision, a preference, a project convention, a fix and its cause), call
|
|
@@ -62,21 +64,35 @@ You have a local memory MCP server (\`open-memex\`) with five tools:
|
|
|
62
64
|
- Memories default to this project's scope; use the \`personal\` scope for facts about
|
|
63
65
|
the user that hold across all projects. When a saved fact becomes outdated, call
|
|
64
66
|
\`memory_supersede\` instead of adding a duplicate.
|
|
65
|
-
`;
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
## Syncing project memories for review (D26)
|
|
69
|
+
|
|
70
|
+
Project memories you save land in a local outbox first — they are NOT in git yet.
|
|
71
|
+
Syncing them into the repo for review is an explicit, user-approved step:
|
|
72
|
+
|
|
73
|
+
- At session start, and when you finish a meaningful chunk of work, call
|
|
74
|
+
\`memory_status\`. If the outbox has drafts, summarize them (one line each) and ask
|
|
75
|
+
the user which ones to sync. Sync NOTHING the user did not name.
|
|
76
|
+
- When the user says "sync memory" (or "同步记忆"), treat it as a request to run
|
|
77
|
+
the sync flow above: call \`memory_status\`, summarize the outbox drafts, and ask
|
|
78
|
+
which ones to sync.
|
|
79
|
+
- When the user approves, ask ONE follow-up: a separate memory-only branch/PR
|
|
80
|
+
(recommended), or fold the memories into the current branch alongside code?
|
|
81
|
+
A "yes, you do it" answer covers the whole chain — branch, local commit,
|
|
82
|
+
push, PR creation — do NOT re-ask at each step. If the user says they will
|
|
83
|
+
do it themselves, hand them the printed push/PR commands and do nothing.
|
|
84
|
+
Then call \`memory_submit\` with the approved ids (\`onto\` = current branch name
|
|
85
|
+
only for the fold-into-code path). It creates the branch, copies the drafts in
|
|
86
|
+
as \`proposed\`, commits locally, and prints the push + PR commands.
|
|
87
|
+
- Base branch for the memory PR defaults to the branch you are on; the user may
|
|
88
|
+
redirect it to the integration branch (main) for branch-independent knowledge.
|
|
89
|
+
- If anything conflicts (same id with different content, push rejected), STOP and
|
|
90
|
+
let the user judge — never overwrite.
|
|
91
|
+
- After the PR merges, call \`memory_pr_status\` (with \`apply\` when the user
|
|
92
|
+
approves) to map the PR's review state back onto each memory — merged means
|
|
93
|
+
\`published\`, an approval means \`approved\` (credited to the reviewer).
|
|
94
|
+
- \`personal\` scope memories NEVER leave the machine.
|
|
95
|
+
`;
|
|
80
96
|
|
|
81
97
|
function writeMcpJson(root: string, client: string, force: boolean): string | null {
|
|
82
98
|
if (client === "opencode") return writeOpencodeMcpJson(root, force);
|
package/src/mcp.ts
CHANGED
|
@@ -30,17 +30,56 @@ import {
|
|
|
30
30
|
listMemories,
|
|
31
31
|
supersedeMemory,
|
|
32
32
|
forgetMemory,
|
|
33
|
+
statusMemories,
|
|
34
|
+
submitMemoriesOp,
|
|
35
|
+
proposeMemoriesOp,
|
|
36
|
+
promoteMemoryOp,
|
|
37
|
+
resolveMemoryOp,
|
|
38
|
+
prStatusOp,
|
|
33
39
|
memoryAddArgs,
|
|
34
40
|
memorySearchArgs,
|
|
35
41
|
memoryListArgs,
|
|
36
42
|
memorySupersedeArgs,
|
|
37
43
|
memoryForgetArgs,
|
|
44
|
+
memoryStatusArgs,
|
|
45
|
+
memorySubmitArgs,
|
|
46
|
+
memoryProposeArgs,
|
|
47
|
+
memoryPromoteArgs,
|
|
48
|
+
memoryResolveArgs,
|
|
49
|
+
memoryPrStatusArgs,
|
|
38
50
|
TOOL_DESCRIPTIONS,
|
|
39
51
|
type ToolResult,
|
|
40
52
|
} from "./tools/ops.ts";
|
|
41
53
|
|
|
42
54
|
const SERVER_VERSION = "0.2.0-alpha";
|
|
43
55
|
|
|
56
|
+
/**
|
|
57
|
+
* D26: session-start guidance delivered through the MCP handshake itself.
|
|
58
|
+
* The init-written instruction files only exist if the user ran
|
|
59
|
+
* `open-memex init --client`; the initialize `instructions` reach every MCP
|
|
60
|
+
* client at connect time. Still advisory — no MCP consumer offers a hard
|
|
61
|
+
* session-start hook — but it is the strongest signal available.
|
|
62
|
+
*/
|
|
63
|
+
const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
|
|
64
|
+
(eleven memory_* tools: add, search, list, supersede, forget, status, submit,
|
|
65
|
+
propose, promote, resolve, pr_status).
|
|
66
|
+
|
|
67
|
+
- At the START of this session, call memory_status. If the project outbox has
|
|
68
|
+
drafts waiting for review, summarize them (one line each) and ask the user
|
|
69
|
+
which ones to sync into the repo. Sync NOTHING the user did not name.
|
|
70
|
+
- When the user says "sync memory" (or "同步记忆"), treat it as a request to run
|
|
71
|
+
the sync flow: call memory_status, summarize the outbox drafts, and ask which
|
|
72
|
+
ones to sync.
|
|
73
|
+
- When you finish a meaningful chunk of work, call memory_status again as a checkpoint.
|
|
74
|
+
- BE PROACTIVE: when the user shares something worth remembering across sessions
|
|
75
|
+
(a decision, a preference, a project convention, a fix and its cause), call
|
|
76
|
+
memory_add without being asked. Keep each memory to one self-contained statement.
|
|
77
|
+
- Before asking the user about past decisions, conventions, or preferences they
|
|
78
|
+
may have told you before, call memory_search first.
|
|
79
|
+
- Memories default to this project's scope; use the personal scope for facts
|
|
80
|
+
about the user that hold across all projects.
|
|
81
|
+
- personal scope memories NEVER leave this machine.`;
|
|
82
|
+
|
|
44
83
|
/** Adapt a framework-agnostic op result to an MCP tool response. */
|
|
45
84
|
function toMcp(p: Promise<ToolResult>) {
|
|
46
85
|
return p.then(
|
|
@@ -64,11 +103,25 @@ export async function runMcpServer() {
|
|
|
64
103
|
|
|
65
104
|
// Init DB and one-shot sync of markdown -> index, mirroring the plugin.
|
|
66
105
|
db();
|
|
67
|
-
syncScope(scope.key);
|
|
68
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
106
|
+
syncScope(scope.key, "session");
|
|
107
|
+
syncScope(PERSONAL_SCOPE.key, "session");
|
|
69
108
|
console.error(`[open-memex] MCP server up. scope=${scope.key}`);
|
|
70
109
|
|
|
71
|
-
|
|
110
|
+
// D26: re-sync on every request, not just at startup. The in-repo dir
|
|
111
|
+
// follows the current git branch, so a branch switch mid-session would
|
|
112
|
+
// otherwise leave the index pointing at files that no longer exist.
|
|
113
|
+
const withSync = <A extends object, R>(fn: (args: A) => R) => {
|
|
114
|
+
return (args: A): R => {
|
|
115
|
+
syncScope(scope.key, "request");
|
|
116
|
+
syncScope(PERSONAL_SCOPE.key, "request");
|
|
117
|
+
return fn(args);
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const server = new McpServer(
|
|
122
|
+
{ name: "open-memex", version: SERVER_VERSION },
|
|
123
|
+
{ instructions: SERVER_INSTRUCTIONS },
|
|
124
|
+
);
|
|
72
125
|
|
|
73
126
|
server.registerTool(
|
|
74
127
|
"memory_add",
|
|
@@ -76,7 +129,7 @@ export async function runMcpServer() {
|
|
|
76
129
|
description: TOOL_DESCRIPTIONS.memory_add,
|
|
77
130
|
inputSchema: z.object(memoryAddArgs),
|
|
78
131
|
},
|
|
79
|
-
(args) => toMcp(addMemory(getScope, cfg, args)),
|
|
132
|
+
withSync((args) => toMcp(addMemory(getScope, cfg, args))),
|
|
80
133
|
);
|
|
81
134
|
|
|
82
135
|
server.registerTool(
|
|
@@ -86,7 +139,7 @@ export async function runMcpServer() {
|
|
|
86
139
|
inputSchema: z.object(memorySearchArgs),
|
|
87
140
|
annotations: { readOnlyHint: true },
|
|
88
141
|
},
|
|
89
|
-
(args) => toMcp(searchMemories(getScope, args)),
|
|
142
|
+
withSync((args) => toMcp(searchMemories(getScope, args))),
|
|
90
143
|
);
|
|
91
144
|
|
|
92
145
|
server.registerTool(
|
|
@@ -96,7 +149,7 @@ export async function runMcpServer() {
|
|
|
96
149
|
inputSchema: z.object(memoryListArgs),
|
|
97
150
|
annotations: { readOnlyHint: true },
|
|
98
151
|
},
|
|
99
|
-
(args) => toMcp(listMemories(getScope, args)),
|
|
152
|
+
withSync((args) => toMcp(listMemories(getScope, args))),
|
|
100
153
|
);
|
|
101
154
|
|
|
102
155
|
server.registerTool(
|
|
@@ -105,7 +158,7 @@ export async function runMcpServer() {
|
|
|
105
158
|
description: TOOL_DESCRIPTIONS.memory_supersede,
|
|
106
159
|
inputSchema: z.object(memorySupersedeArgs),
|
|
107
160
|
},
|
|
108
|
-
(args) => toMcp(supersedeMemory(cfg, args)),
|
|
161
|
+
withSync((args) => toMcp(supersedeMemory(cfg, args))),
|
|
109
162
|
);
|
|
110
163
|
|
|
111
164
|
server.registerTool(
|
|
@@ -115,7 +168,63 @@ export async function runMcpServer() {
|
|
|
115
168
|
inputSchema: z.object(memoryForgetArgs),
|
|
116
169
|
annotations: { destructiveHint: true },
|
|
117
170
|
},
|
|
118
|
-
(args) => toMcp(forgetMemory(args)),
|
|
171
|
+
withSync((args) => toMcp(forgetMemory(args))),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
server.registerTool(
|
|
175
|
+
"memory_status",
|
|
176
|
+
{
|
|
177
|
+
description: TOOL_DESCRIPTIONS.memory_status,
|
|
178
|
+
inputSchema: z.object(memoryStatusArgs),
|
|
179
|
+
annotations: { readOnlyHint: true },
|
|
180
|
+
},
|
|
181
|
+
withSync((_args) => toMcp(statusMemories())),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
server.registerTool(
|
|
185
|
+
"memory_submit",
|
|
186
|
+
{
|
|
187
|
+
description: TOOL_DESCRIPTIONS.memory_submit,
|
|
188
|
+
inputSchema: z.object(memorySubmitArgs),
|
|
189
|
+
},
|
|
190
|
+
withSync((args) => toMcp(submitMemoriesOp(args))),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
server.registerTool(
|
|
194
|
+
"memory_propose",
|
|
195
|
+
{
|
|
196
|
+
description: TOOL_DESCRIPTIONS.memory_propose,
|
|
197
|
+
inputSchema: z.object(memoryProposeArgs),
|
|
198
|
+
},
|
|
199
|
+
withSync((args) => toMcp(proposeMemoriesOp(args))),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
server.registerTool(
|
|
203
|
+
"memory_promote",
|
|
204
|
+
{
|
|
205
|
+
description: TOOL_DESCRIPTIONS.memory_promote,
|
|
206
|
+
inputSchema: z.object(memoryPromoteArgs),
|
|
207
|
+
},
|
|
208
|
+
withSync((args) => toMcp(promoteMemoryOp(args))),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
server.registerTool(
|
|
212
|
+
"memory_resolve",
|
|
213
|
+
{
|
|
214
|
+
description: TOOL_DESCRIPTIONS.memory_resolve,
|
|
215
|
+
inputSchema: z.object(memoryResolveArgs),
|
|
216
|
+
},
|
|
217
|
+
withSync((args) => toMcp(resolveMemoryOp(args))),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
server.registerTool(
|
|
221
|
+
"memory_pr_status",
|
|
222
|
+
{
|
|
223
|
+
description: TOOL_DESCRIPTIONS.memory_pr_status,
|
|
224
|
+
inputSchema: z.object(memoryPrStatusArgs),
|
|
225
|
+
annotations: { readOnlyHint: true },
|
|
226
|
+
},
|
|
227
|
+
withSync((args) => toMcp(prStatusOp(args))),
|
|
119
228
|
);
|
|
120
229
|
|
|
121
230
|
const transport = new StdioServerTransport();
|
package/src/paths.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
|
|
5
6
|
function dataRoot(): string {
|
|
6
7
|
if (process.env.MY_O_MEMORY_HOME) return path.resolve(process.env.MY_O_MEMORY_HOME);
|
|
@@ -43,3 +44,34 @@ export function memoriesDirPath(scopeKey: string): string {
|
|
|
43
44
|
const { memories } = paths();
|
|
44
45
|
return path.join(memories, scopeKey);
|
|
45
46
|
}
|
|
47
|
+
|
|
48
|
+
/** Project root: git top-level, falling back to cwd (2B: in-repo memory dir anchor). */
|
|
49
|
+
export function projectRoot(cwd: string = process.cwd()): string {
|
|
50
|
+
try {
|
|
51
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
55
|
+
}).trim();
|
|
56
|
+
if (top) return top;
|
|
57
|
+
} catch {
|
|
58
|
+
/* not a git repo — use cwd */
|
|
59
|
+
}
|
|
60
|
+
return path.resolve(cwd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* In-repo memory directory for a project scope (2B/D23): `<root>/<memoryDir>/`,
|
|
65
|
+
* default `<root>/.ai/open-memex/`. Created on demand. `memoryDir` must be a
|
|
66
|
+
* relative path without `..` segments (validated by `config set`).
|
|
67
|
+
*/
|
|
68
|
+
export function inRepoMemoriesDir(root: string, memoryDir: string): string {
|
|
69
|
+
const dir = path.join(root, memoryDir);
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
return dir;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Same as `inRepoMemoriesDir` but never creates the directory. */
|
|
75
|
+
export function inRepoMemoriesDirPath(root: string, memoryDir: string): string {
|
|
76
|
+
return path.join(root, memoryDir);
|
|
77
|
+
}
|
package/src/retrieve/inject.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { list } from "./search.ts";
|
|
1
|
+
import { list, hitStateLabel } from "./search.ts";
|
|
2
2
|
import type { Scope } from "../scope.ts";
|
|
3
3
|
import { PERSONAL_SCOPE } from "../scope.ts";
|
|
4
4
|
import type { MyOMemoryConfig } from "../config.ts";
|
|
@@ -23,7 +23,7 @@ export function buildContextBlock(scope: Scope, cfg: MyOMemoryConfig): string |
|
|
|
23
23
|
|
|
24
24
|
if (project.length > 0) {
|
|
25
25
|
lines.push("", `Project knowledge (${scope.projectName}):`);
|
|
26
|
-
for (const m of project) lines.push(`- [${m.type}] ${oneLine(m.snippet)}`);
|
|
26
|
+
for (const m of project) lines.push(`- [${m.type}]${hitStateLabel(m)} ${oneLine(m.snippet)}`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
lines.push(
|