open-memex 0.3.0 → 0.4.0-alpha.1
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 +11 -1
- package/README.md +67 -6
- package/README.zh-CN.md +58 -6
- package/dist/cli.js +214 -11
- package/dist/config.js +10 -0
- package/dist/github.js +215 -0
- package/dist/index.js +6 -0
- package/dist/init.js +30 -18
- package/dist/mcp.js +44 -8
- 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 +108 -6
- package/package.json +1 -1
- package/scripts/smoke-mcp.ts +2 -2
- package/src/cli.ts +204 -12
- package/src/config.ts +16 -0
- package/src/github.ts +257 -0
- package/src/index.ts +6 -0
- package/src/init.ts +30 -17
- package/src/mcp.ts +86 -7
- 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/mcp.ts
CHANGED
|
@@ -30,11 +30,23 @@ 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";
|
|
@@ -64,10 +76,21 @@ export async function runMcpServer() {
|
|
|
64
76
|
|
|
65
77
|
// Init DB and one-shot sync of markdown -> index, mirroring the plugin.
|
|
66
78
|
db();
|
|
67
|
-
syncScope(scope.key);
|
|
68
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
79
|
+
syncScope(scope.key, "session");
|
|
80
|
+
syncScope(PERSONAL_SCOPE.key, "session");
|
|
69
81
|
console.error(`[open-memex] MCP server up. scope=${scope.key}`);
|
|
70
82
|
|
|
83
|
+
// D26: re-sync on every request, not just at startup. The in-repo dir
|
|
84
|
+
// follows the current git branch, so a branch switch mid-session would
|
|
85
|
+
// otherwise leave the index pointing at files that no longer exist.
|
|
86
|
+
const withSync = <A extends object, R>(fn: (args: A) => R) => {
|
|
87
|
+
return (args: A): R => {
|
|
88
|
+
syncScope(scope.key, "request");
|
|
89
|
+
syncScope(PERSONAL_SCOPE.key, "request");
|
|
90
|
+
return fn(args);
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
71
94
|
const server = new McpServer({ name: "open-memex", version: SERVER_VERSION });
|
|
72
95
|
|
|
73
96
|
server.registerTool(
|
|
@@ -76,7 +99,7 @@ export async function runMcpServer() {
|
|
|
76
99
|
description: TOOL_DESCRIPTIONS.memory_add,
|
|
77
100
|
inputSchema: z.object(memoryAddArgs),
|
|
78
101
|
},
|
|
79
|
-
(args) => toMcp(addMemory(getScope, cfg, args)),
|
|
102
|
+
withSync((args) => toMcp(addMemory(getScope, cfg, args))),
|
|
80
103
|
);
|
|
81
104
|
|
|
82
105
|
server.registerTool(
|
|
@@ -86,7 +109,7 @@ export async function runMcpServer() {
|
|
|
86
109
|
inputSchema: z.object(memorySearchArgs),
|
|
87
110
|
annotations: { readOnlyHint: true },
|
|
88
111
|
},
|
|
89
|
-
(args) => toMcp(searchMemories(getScope, args)),
|
|
112
|
+
withSync((args) => toMcp(searchMemories(getScope, args))),
|
|
90
113
|
);
|
|
91
114
|
|
|
92
115
|
server.registerTool(
|
|
@@ -96,7 +119,7 @@ export async function runMcpServer() {
|
|
|
96
119
|
inputSchema: z.object(memoryListArgs),
|
|
97
120
|
annotations: { readOnlyHint: true },
|
|
98
121
|
},
|
|
99
|
-
(args) => toMcp(listMemories(getScope, args)),
|
|
122
|
+
withSync((args) => toMcp(listMemories(getScope, args))),
|
|
100
123
|
);
|
|
101
124
|
|
|
102
125
|
server.registerTool(
|
|
@@ -105,7 +128,7 @@ export async function runMcpServer() {
|
|
|
105
128
|
description: TOOL_DESCRIPTIONS.memory_supersede,
|
|
106
129
|
inputSchema: z.object(memorySupersedeArgs),
|
|
107
130
|
},
|
|
108
|
-
(args) => toMcp(supersedeMemory(cfg, args)),
|
|
131
|
+
withSync((args) => toMcp(supersedeMemory(cfg, args))),
|
|
109
132
|
);
|
|
110
133
|
|
|
111
134
|
server.registerTool(
|
|
@@ -115,7 +138,63 @@ export async function runMcpServer() {
|
|
|
115
138
|
inputSchema: z.object(memoryForgetArgs),
|
|
116
139
|
annotations: { destructiveHint: true },
|
|
117
140
|
},
|
|
118
|
-
(args) => toMcp(forgetMemory(args)),
|
|
141
|
+
withSync((args) => toMcp(forgetMemory(args))),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
server.registerTool(
|
|
145
|
+
"memory_status",
|
|
146
|
+
{
|
|
147
|
+
description: TOOL_DESCRIPTIONS.memory_status,
|
|
148
|
+
inputSchema: z.object(memoryStatusArgs),
|
|
149
|
+
annotations: { readOnlyHint: true },
|
|
150
|
+
},
|
|
151
|
+
withSync((_args) => toMcp(statusMemories())),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
server.registerTool(
|
|
155
|
+
"memory_submit",
|
|
156
|
+
{
|
|
157
|
+
description: TOOL_DESCRIPTIONS.memory_submit,
|
|
158
|
+
inputSchema: z.object(memorySubmitArgs),
|
|
159
|
+
},
|
|
160
|
+
withSync((args) => toMcp(submitMemoriesOp(args))),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
server.registerTool(
|
|
164
|
+
"memory_propose",
|
|
165
|
+
{
|
|
166
|
+
description: TOOL_DESCRIPTIONS.memory_propose,
|
|
167
|
+
inputSchema: z.object(memoryProposeArgs),
|
|
168
|
+
},
|
|
169
|
+
withSync((args) => toMcp(proposeMemoriesOp(args))),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
server.registerTool(
|
|
173
|
+
"memory_promote",
|
|
174
|
+
{
|
|
175
|
+
description: TOOL_DESCRIPTIONS.memory_promote,
|
|
176
|
+
inputSchema: z.object(memoryPromoteArgs),
|
|
177
|
+
},
|
|
178
|
+
withSync((args) => toMcp(promoteMemoryOp(args))),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
server.registerTool(
|
|
182
|
+
"memory_resolve",
|
|
183
|
+
{
|
|
184
|
+
description: TOOL_DESCRIPTIONS.memory_resolve,
|
|
185
|
+
inputSchema: z.object(memoryResolveArgs),
|
|
186
|
+
},
|
|
187
|
+
withSync((args) => toMcp(resolveMemoryOp(args))),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
server.registerTool(
|
|
191
|
+
"memory_pr_status",
|
|
192
|
+
{
|
|
193
|
+
description: TOOL_DESCRIPTIONS.memory_pr_status,
|
|
194
|
+
inputSchema: z.object(memoryPrStatusArgs),
|
|
195
|
+
annotations: { readOnlyHint: true },
|
|
196
|
+
},
|
|
197
|
+
withSync((args) => toMcp(prStatusOp(args))),
|
|
119
198
|
);
|
|
120
199
|
|
|
121
200
|
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(
|
package/src/retrieve/search.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface SearchHit {
|
|
|
11
11
|
score: number;
|
|
12
12
|
updated_at: number;
|
|
13
13
|
status: string;
|
|
14
|
+
review_state: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
interface RawRow {
|
|
@@ -24,6 +25,7 @@ interface RawRow {
|
|
|
24
25
|
score: number;
|
|
25
26
|
status: string;
|
|
26
27
|
superseded_by: string | null;
|
|
28
|
+
review_state: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
function toHit(r: RawRow): SearchHit {
|
|
@@ -37,13 +39,36 @@ function toHit(r: RawRow): SearchHit {
|
|
|
37
39
|
score: r.score,
|
|
38
40
|
updated_at: r.updated_at,
|
|
39
41
|
status: r.status,
|
|
42
|
+
review_state: r.review_state ?? "draft",
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* D30: review-state ranking tiers for shared (project) memories.
|
|
48
|
+
* Reviewed knowledge (approved/published) outranks unreviewed outbox drafts;
|
|
49
|
+
* personal memories are always `draft` by design and are NOT demoted.
|
|
50
|
+
* Stable: order within a tier keeps the incoming score/recency order.
|
|
51
|
+
*/
|
|
52
|
+
function reviewTier(r: RawRow): number {
|
|
53
|
+
if (r.scope_key === "personal") return 1;
|
|
54
|
+
if (r.review_state === "approved" || r.review_state === "published") return 0;
|
|
55
|
+
if (r.review_state === "draft" || r.review_state === "rejected") return 2;
|
|
56
|
+
return 1; // proposed and anything unexpected
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** D30: `[draft]` / `[proposed]` / … marker for project memories.
|
|
60
|
+
* Personal memories stay unmarked (always draft — the marker would be noise). */
|
|
61
|
+
export function hitStateLabel(h: Pick<SearchHit, "scope_key" | "review_state">): string {
|
|
62
|
+
if (h.scope_key === "personal") return "";
|
|
63
|
+
return ` [${h.review_state || "draft"}]`;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
/**
|
|
44
67
|
* Lifecycle-aware post-processing (§3.3):
|
|
45
68
|
* - retracted / archived are excluded from retrieval (kept for audit);
|
|
46
69
|
* - a superseded memory resolves to the newest of its chain (cycle-safe);
|
|
70
|
+
* - D30: approved/published project memories rank first, project
|
|
71
|
+
* drafts/rejected rank after unreviewed content;
|
|
47
72
|
* - deprecated stays visible as a warning but ranks after active.
|
|
48
73
|
*/
|
|
49
74
|
function resolveVisible(rows: RawRow[], limit: number): SearchHit[] {
|
|
@@ -54,7 +79,7 @@ function resolveVisible(rows: RawRow[], limit: number): SearchHit[] {
|
|
|
54
79
|
const r = db()
|
|
55
80
|
.prepare(
|
|
56
81
|
`SELECT id, scope_key, project_name, type, tags, updated_at, status,
|
|
57
|
-
superseded_by, substr(content, 1, 240) AS snippet
|
|
82
|
+
superseded_by, review_state, substr(content, 1, 240) AS snippet
|
|
58
83
|
FROM memories WHERE id = ?`,
|
|
59
84
|
)
|
|
60
85
|
.get(id) as
|
|
@@ -67,7 +92,7 @@ function resolveVisible(rows: RawRow[], limit: number): SearchHit[] {
|
|
|
67
92
|
};
|
|
68
93
|
|
|
69
94
|
const seen = new Set<string>();
|
|
70
|
-
const
|
|
95
|
+
const tiers: SearchHit[][] = [[], [], [], []];
|
|
71
96
|
const deprecated: SearchHit[] = [];
|
|
72
97
|
|
|
73
98
|
for (const r of rows) {
|
|
@@ -90,9 +115,9 @@ function resolveVisible(rows: RawRow[], limit: number): SearchHit[] {
|
|
|
90
115
|
seen.add(target.id);
|
|
91
116
|
const hit = toHit(target);
|
|
92
117
|
if (target.status === "deprecated") deprecated.push(hit);
|
|
93
|
-
else
|
|
118
|
+
else tiers[reviewTier(target)].push(hit);
|
|
94
119
|
}
|
|
95
|
-
return [...
|
|
120
|
+
return [...tiers[0], ...tiers[1], ...tiers[2], ...deprecated].slice(0, limit);
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
/**
|
|
@@ -130,7 +155,7 @@ export function search(
|
|
|
130
155
|
|
|
131
156
|
const sql = `
|
|
132
157
|
SELECT m.id, m.scope_key, m.project_name, m.type, m.tags, m.updated_at,
|
|
133
|
-
m.status, m.superseded_by,
|
|
158
|
+
m.status, m.superseded_by, m.review_state,
|
|
134
159
|
snippet(memories_fts, 0, '[', ']', ' ... ', 12) AS snippet,
|
|
135
160
|
bm25(memories_fts) AS score
|
|
136
161
|
FROM memories_fts
|
|
@@ -155,6 +180,7 @@ export function search(
|
|
|
155
180
|
updated_at: number;
|
|
156
181
|
status: string;
|
|
157
182
|
superseded_by: string | null;
|
|
183
|
+
review_state: string;
|
|
158
184
|
snippet: string;
|
|
159
185
|
score: number;
|
|
160
186
|
}>;
|
|
@@ -173,7 +199,7 @@ export function list(
|
|
|
173
199
|
const typeFilter = opts.type ? ` AND type = ?` : "";
|
|
174
200
|
const sql = `
|
|
175
201
|
SELECT id, scope_key, project_name, type, tags, updated_at, status,
|
|
176
|
-
superseded_by, substr(content, 1, 240) AS snippet
|
|
202
|
+
superseded_by, review_state, substr(content, 1, 240) AS snippet
|
|
177
203
|
FROM memories
|
|
178
204
|
WHERE scope_key = ?${typeFilter}
|
|
179
205
|
ORDER BY updated_at DESC
|
|
@@ -194,6 +220,7 @@ export function list(
|
|
|
194
220
|
updated_at: number;
|
|
195
221
|
status: string;
|
|
196
222
|
superseded_by: string | null;
|
|
223
|
+
review_state: string;
|
|
197
224
|
snippet: string;
|
|
198
225
|
}>;
|
|
199
226
|
|