open-memex 0.7.2 → 0.7.3-alpha.2
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/.dockerignore +3 -0
- package/CHANGELOG.md +16 -0
- package/Dockerfile +18 -0
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/.build-stamp +1 -1
- package/dist/mcp.js +11 -5
- package/dist/tools/ops.js +22 -22
- package/docs/V2-DESIGN.md +12 -10
- package/docs/assets/open-memex-architecture-en.png +0 -0
- package/docs/assets/open-memex-architecture-zh.png +0 -0
- package/docs/release-runbook.md +37 -5
- package/glama.json +6 -0
- package/package.json +1 -1
- package/scripts/smoke-mcp.ts +3 -0
- package/src/mcp.ts +11 -5
- package/src/tools/ops.ts +23 -23
package/.dockerignore
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,22 @@ the `@alpha` tag are development snapshots and are not listed individually.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
- Tool-definition quality pass (Glama TDQS 4.0/5 on v0.7.2): fixed `memory_pr_status` declaring `readOnlyHint` while `apply=true` mutates local files; added explicit MCP annotations (read-only / destructive / open-world) to all 11 tools; descriptions now route between sibling tools (propose vs submit vs promote, resolve vs pr_status, forget vs supersede), state error behavior and real defaults (search limit 8, list limit 20), and the search/list `type` and `limit` parameters are documented. D76 records the rule; `scripts/smoke-mcp.ts` guards the pr_status annotation.
|
|
13
|
+
|
|
14
|
+
- Design doc maintenance (D75): pruned resolved Open Questions (decided in D56/D57/D61/D68, plus the shipped retrieval-robustness layers) and recorded four competitor-scan directions — retrieval evaluation gate, per-client trust levels, cold-start import, and a `doctor` mis-registration check.
|
|
15
|
+
|
|
16
|
+
- Dropped the deprecated `user` value from the MCP tools' advertised `scope` enums (`personal` is the only global-scope name now; the storage layer still migrates v1 `user` data automatically).
|
|
17
|
+
|
|
18
|
+
- Added a root `Dockerfile` (multi-stage, starts the stdio MCP server via `node dist/cli.js mcp`) so container-based directories such as Glama can build, start, and introspect the server.
|
|
19
|
+
|
|
20
|
+
- Both READMEs now open the Core concepts section with an architecture diagram (English and Chinese versions in `docs/assets/`).
|
|
21
|
+
|
|
22
|
+
## [0.7.2] - 2026-10-04
|
|
23
|
+
|
|
24
|
+
## Listed in the official MCP Registry
|
|
25
|
+
|
|
26
|
+
- **Added: open-memex is now published in the official MCP Registry** as `io.github.stoneskin/open-memex`, so MCP clients and directories can discover and install it from the registry. The npm package declares the matching `mcpName` in `package.json` (the registry verifies package ownership against it), and the repo carries `server.json` with the registry metadata; its version fields move in step with package releases.
|
|
27
|
+
- Release runbook: commands are now given one per step with explicit stop-and-check gates, including "wait for npm to propagate before mcp-publisher publish".
|
|
12
28
|
- Added `docs/release-runbook.md`: the maintainer release checklist (version sync points, preflight, npm alpha/stable publish, MCP Registry publish, Node 22.14 floor notes).
|
|
13
29
|
|
|
14
30
|
## [0.7.1] - 2026-10-04
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Container for MCP introspection (Glama) and containerized runs.
|
|
2
|
+
# Starts the stdio MCP server: `open-memex mcp`.
|
|
3
|
+
FROM node:22-slim AS build
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \
|
|
6
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
+
COPY package*.json ./
|
|
8
|
+
RUN npm ci
|
|
9
|
+
COPY . .
|
|
10
|
+
RUN npm run build
|
|
11
|
+
|
|
12
|
+
FROM node:22-slim
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
COPY --from=build /app/dist ./dist
|
|
15
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
16
|
+
COPY package.json ./
|
|
17
|
+
ENV NODE_ENV=production
|
|
18
|
+
CMD ["node", "dist/cli.js", "mcp"]
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/open-memex)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
+
[](https://glama.ai/mcp/servers/stoneskin/open-memex)
|
|
7
8
|
|
|
8
9
|
[中文文档](./README.zh-CN.md)
|
|
9
10
|
|
|
@@ -86,6 +87,8 @@ open-memex doctor
|
|
|
86
87
|
|
|
87
88
|
Three ideas explain almost everything open-memex does.
|
|
88
89
|
|
|
90
|
+

|
|
91
|
+
|
|
89
92
|
**1. Two scopes: `project` and `personal`.**
|
|
90
93
|
Every memory belongs to one of two places:
|
|
91
94
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/open-memex)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
+
[](https://glama.ai/mcp/servers/stoneskin/open-memex)
|
|
7
8
|
|
|
8
9
|
[English README](./README.md)
|
|
9
10
|
|
|
@@ -71,6 +72,8 @@ open-memex doctor
|
|
|
71
72
|
|
|
72
73
|
三个概念能解释 open-memex 几乎所有的事。
|
|
73
74
|
|
|
75
|
+

|
|
76
|
+
|
|
74
77
|
**1. 两种作用域:`project` 和 `personal`。**
|
|
75
78
|
每条记忆属于两个地方之一:
|
|
76
79
|
|
package/dist/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
c5a82fa9f711dea3b1e8787d551424e978d606435b08f83d51ba5aaf0996e9ff
|
package/dist/mcp.js
CHANGED
|
@@ -164,51 +164,57 @@ export async function runMcpServer() {
|
|
|
164
164
|
server.registerTool("memory_add", {
|
|
165
165
|
description: TOOL_DESCRIPTIONS.memory_add,
|
|
166
166
|
inputSchema: z.object(memoryAddArgs),
|
|
167
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
167
168
|
}, withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))));
|
|
168
169
|
server.registerTool("memory_search", {
|
|
169
170
|
description: TOOL_DESCRIPTIONS.memory_search,
|
|
170
171
|
inputSchema: z.object(memorySearchArgs),
|
|
171
|
-
annotations: { readOnlyHint: true },
|
|
172
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
172
173
|
}, withSync((args) => toMcp(searchMemories(getScope, args))));
|
|
173
174
|
server.registerTool("memory_list", {
|
|
174
175
|
description: TOOL_DESCRIPTIONS.memory_list,
|
|
175
176
|
inputSchema: z.object(memoryListArgs),
|
|
176
|
-
annotations: { readOnlyHint: true },
|
|
177
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
177
178
|
}, withSync((args) => toMcp(listMemories(getScope, args))));
|
|
178
179
|
server.registerTool("memory_supersede", {
|
|
179
180
|
description: TOOL_DESCRIPTIONS.memory_supersede,
|
|
180
181
|
inputSchema: z.object(memorySupersedeArgs),
|
|
182
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
181
183
|
}, withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))));
|
|
182
184
|
server.registerTool("memory_forget", {
|
|
183
185
|
description: TOOL_DESCRIPTIONS.memory_forget,
|
|
184
186
|
inputSchema: z.object(memoryForgetArgs),
|
|
185
|
-
annotations: { destructiveHint: true },
|
|
187
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
186
188
|
}, withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))));
|
|
187
189
|
server.registerTool("memory_status", {
|
|
188
190
|
description: TOOL_DESCRIPTIONS.memory_status,
|
|
189
191
|
inputSchema: z.object(memoryStatusArgs),
|
|
190
|
-
annotations: { readOnlyHint: true },
|
|
192
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
191
193
|
}, withSync((_args) => toMcp(statusMemories())));
|
|
192
194
|
server.registerTool("memory_submit", {
|
|
193
195
|
description: TOOL_DESCRIPTIONS.memory_submit,
|
|
194
196
|
inputSchema: z.object(memorySubmitArgs),
|
|
197
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
195
198
|
}, withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))));
|
|
196
199
|
server.registerTool("memory_propose", {
|
|
197
200
|
description: TOOL_DESCRIPTIONS.memory_propose,
|
|
198
201
|
inputSchema: z.object(memoryProposeArgs),
|
|
202
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
199
203
|
}, withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))));
|
|
200
204
|
server.registerTool("memory_promote", {
|
|
201
205
|
description: TOOL_DESCRIPTIONS.memory_promote,
|
|
202
206
|
inputSchema: z.object(memoryPromoteArgs),
|
|
207
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
203
208
|
}, withSync((args) => toMcp(promoteMemoryOp(args))));
|
|
204
209
|
server.registerTool("memory_resolve", {
|
|
205
210
|
description: TOOL_DESCRIPTIONS.memory_resolve,
|
|
206
211
|
inputSchema: z.object(memoryResolveArgs),
|
|
212
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
207
213
|
}, withSync((args) => toMcp(resolveMemoryOp(args))));
|
|
208
214
|
server.registerTool("memory_pr_status", {
|
|
209
215
|
description: TOOL_DESCRIPTIONS.memory_pr_status,
|
|
210
216
|
inputSchema: z.object(memoryPrStatusArgs),
|
|
211
|
-
annotations: { readOnlyHint:
|
|
217
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
212
218
|
}, withSync((args) => toMcp(prStatusOp(args))));
|
|
213
219
|
const transport = new StdioServerTransport();
|
|
214
220
|
await server.connect(transport);
|
package/dist/tools/ops.js
CHANGED
|
@@ -38,23 +38,23 @@ export async function withOutboxNote(scopeKey, p, reviewHint) {
|
|
|
38
38
|
}
|
|
39
39
|
/** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
|
|
40
40
|
export const TOOL_DESCRIPTIONS = {
|
|
41
|
-
memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
|
|
42
|
-
memory_search: "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
|
|
43
|
-
memory_list: "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown).",
|
|
44
|
-
memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
|
|
45
|
-
memory_forget: "Delete a memory by id. Use when the user asks to forget something. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
|
|
46
|
-
memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
|
|
47
|
-
memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.)",
|
|
48
|
-
memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
|
|
49
|
-
memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
|
|
50
|
-
memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
|
|
51
|
-
memory_pr_status: "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default; apply=true
|
|
41
|
+
memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects. Returns the new memory's id; an identical fact already stored returns that id instead of duplicating.",
|
|
42
|
+
memory_search: "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored. Results are ranked by BM25 within review tiers — reviewed (approved/published) project memories come first. `limit` caps how many come back (default 8) and `type` restricts to one memory type.",
|
|
43
|
+
memory_list: "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown). `type` filters to one memory type; `limit` caps how many entries come back.",
|
|
44
|
+
memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated. Only active memories can be superseded; the new memory inherits type, tags, and aliases unless you pass replacements. An unknown id is reported back as a failure; nothing is written.",
|
|
45
|
+
memory_forget: "Delete a memory by id. Use when the user asks to forget something. To replace an outdated fact with a corrected one instead of deleting it, use memory_supersede. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
|
|
46
|
+
memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. For the memories themselves use memory_list; for PR review state use memory_pr_status. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
|
|
47
|
+
memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.) Unknown ids or a failed local commit abort the submit with the reason; nothing is published automatically.",
|
|
48
|
+
memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put. Use this when personal knowledge should enter a project's review flow; memory_submit then moves the drafts into the repo, and memory_promote advances drafts already there. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
|
|
49
|
+
memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. One step per call; an unknown id or a memory already published errors. Pass `by` to record the reviewer when it should differ from the default (e.g. approving on the strength of someone else's review). For brand-new outbox drafts, memory_submit is the entry point. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
|
|
50
|
+
memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. Run it when a git pull or merge leaves memory files conflicted; the merged file is written in place (undo with git if the merge is wrong). For PR review state rather than file conflicts, use memory_pr_status. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
|
|
51
|
+
memory_pr_status: "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default — that mode changes nothing; apply=true writes the mapped transitions to the local files (still no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
|
|
52
52
|
};
|
|
53
53
|
/** Shared zod input shapes (raw shape, not z.object — hosts wrap as needed). */
|
|
54
54
|
export const scopeArg = z
|
|
55
|
-
.enum(["project", "personal"
|
|
55
|
+
.enum(["project", "personal"])
|
|
56
56
|
.optional()
|
|
57
|
-
.describe("Memory scope. `project` = tied to this repo. `personal` = global across all your projects.
|
|
57
|
+
.describe("Memory scope. `project` = tied to this repo. `personal` = global across all your projects. Default: project.");
|
|
58
58
|
export const aliasesArg = z
|
|
59
59
|
.array(z.string())
|
|
60
60
|
.optional()
|
|
@@ -79,19 +79,19 @@ export const memorySearchArgs = {
|
|
|
79
79
|
.min(1)
|
|
80
80
|
.describe("Free-text query. File paths, error strings, identifiers work well."),
|
|
81
81
|
scope: z
|
|
82
|
-
.enum(["project", "personal", "
|
|
82
|
+
.enum(["project", "personal", "both"])
|
|
83
83
|
.optional()
|
|
84
84
|
.describe("Which scope(s) to search. Default: both."),
|
|
85
|
-
type: z.string().optional().describe("Restrict to memories of this type."),
|
|
86
|
-
limit: z.number().int().min(1).max(50).optional(),
|
|
85
|
+
type: z.string().optional().describe("Restrict to memories of this type (e.g. decision, preference, gotcha)."),
|
|
86
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 8)."),
|
|
87
87
|
};
|
|
88
88
|
export const memoryListArgs = {
|
|
89
89
|
scope: z
|
|
90
|
-
.enum(["project", "personal", "
|
|
90
|
+
.enum(["project", "personal", "both"])
|
|
91
91
|
.optional()
|
|
92
92
|
.describe("Which scope(s) to list. `project` = tied to this repo. `personal` = global across all your projects. `both` lists each scope's newest. Default: project."),
|
|
93
|
-
type: z.string().optional(),
|
|
94
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
93
|
+
type: z.string().optional().describe("Only list memories of this type (e.g. decision, preference, gotcha)."),
|
|
94
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max entries to list, newest first (default 20)."),
|
|
95
95
|
include: z
|
|
96
96
|
.enum(["active", "all"])
|
|
97
97
|
.optional()
|
|
@@ -153,7 +153,7 @@ export const memoryPrStatusArgs = {
|
|
|
153
153
|
.describe("Perform the mapped review transitions locally (no push). Default: report only."),
|
|
154
154
|
};
|
|
155
155
|
function resolveScope(getScope, kind) {
|
|
156
|
-
return kind === "personal"
|
|
156
|
+
return kind === "personal" ? PERSONAL_SCOPE : getScope();
|
|
157
157
|
}
|
|
158
158
|
function buildFrontmatter(s, body) {
|
|
159
159
|
const rfc = msToRfc3339(Date.now());
|
|
@@ -242,7 +242,7 @@ export async function addMemory(getScope, cfg, args) {
|
|
|
242
242
|
}
|
|
243
243
|
export async function searchMemories(getScope, args) {
|
|
244
244
|
const project = getScope();
|
|
245
|
-
const scopeKeys = args.scope === "personal"
|
|
245
|
+
const scopeKeys = args.scope === "personal"
|
|
246
246
|
? [PERSONAL_SCOPE.key]
|
|
247
247
|
: args.scope === "project"
|
|
248
248
|
? [project.key]
|
|
@@ -294,7 +294,7 @@ export async function listMemories(getScope, args) {
|
|
|
294
294
|
}
|
|
295
295
|
return { title: "memory: list (both scopes)", output: sections.join("\n\n") };
|
|
296
296
|
}
|
|
297
|
-
const s = resolveScope(getScope, args.scope === "project" || args.scope === "personal"
|
|
297
|
+
const s = resolveScope(getScope, args.scope === "project" || args.scope === "personal"
|
|
298
298
|
? args.scope
|
|
299
299
|
: undefined);
|
|
300
300
|
const lines = render(s.key);
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenMemex — Design Document (protocol v0.2)
|
|
2
2
|
|
|
3
|
-
**Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–
|
|
3
|
+
**Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D76 are settled; open questions are tracked at the end of this document.
|
|
4
4
|
**Author:** Stone, with 小沐
|
|
5
5
|
**Changelog vs v1:** incorporates round-3 review from Perplexity, Grok, Gemini, ChatGPT, DeepSeek.
|
|
6
6
|
Key changes: Design Principles section; `role` separated from `type`; two iron rules;
|
|
@@ -562,6 +562,7 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
562
562
|
- **Phase 4 — Future, signal-gated.** Cloud `RemoteProvider` customization only on: multi-private-repo
|
|
563
563
|
sharing needs, fine-grained ACL, audit/compliance mandates · optional API-backed exporters/providers
|
|
564
564
|
for enterprise knowledge systems.
|
|
565
|
+
- **Phase 5 — Candidates recorded 2026-10-04 (not committed).** From the competitor scan (D75): an offline FTS5 retrieval eval with a published recall number (gates any semantic-layer decision) · per-client trust levels on the review ladder · cold-start Markdown import into outbox drafts · `doctor` check that each editor's MCP registration points at the intended data root (mis-rooted servers are a real failure mode elsewhere). Sequencing and scope TBD per candidate.
|
|
565
566
|
|
|
566
567
|
## 19. Migration v1 → v2
|
|
567
568
|
|
|
@@ -1589,26 +1590,27 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
1589
1590
|
equivalence. Deliberately NOT done, pending a decision: HTML i18n and a
|
|
1590
1591
|
size cap for very large stores.
|
|
1591
1592
|
|
|
1592
|
-
|
|
1593
|
+
- **D75 — closed questions leave the list; competitor-inspired directions enter it (2026-10-04).** The Open Questions section is a working list, not an archive: the entries decided in D56 (host parity), D57 (retrieval explainability / memory health), D61 (checkpoint proactivity, capture criteria, retrieval layers 2–3), and D68 (memory visibility), plus the retrieval-robustness entry whose three cheap layers all shipped, are removed; the decisions remain the record. A same-day scan of five Glama-listed memory servers (auxly-memory-cli, agent-memory, remnic, Agent-Memory-Bridge, memtomem, memryzed) confirmed the current wedge — proactive in-session capture + one memory shared across editors + PR-based team sharing — appears in none of them whole. Borrowable directions are recorded instead: four new open questions (retrieval evaluation gate, per-client trust levels, cold-start import, session working state) and one roadmap candidate (`doctor` mis-registration check). Nothing here commits implementation; each direction needs its own design pass.
|
|
1593
1594
|
|
|
1594
|
-
|
|
1595
|
-
- **Checkpoint proactivity for capture (open, 2026-10-02):** D53 retired checkpoint *polling* — the server pushes draft counts instead. But agent proactivity at task checkpoints genuinely helps knowledge capture (§3.5 distillation), so the open question is the degree: proactive enough to catch distillable moments, without nagging the user or burning tokens at every checkpoint. Candidate directions (server-pushed hints in tool results, client-side heuristics, or a middle setting) to be evaluated in a later version; default posture TBD. **Update (2026-10-03): decided in D61 — propose posture** (agent proposes 1–3 candidates at task checkpoints, saves only on approval; auto-draft deferred until memory visibility ships, see the MEMORY_VISIBILITY question below).
|
|
1595
|
+
- **D76 — tool annotations must tell the truth (2026-10-05).** `memory_pr_status` declared `readOnlyHint: true` while `apply=true` writes local review states (caught by Glama's TDQS evaluation of v0.7.2, 4.0/5). Structured hints are part of the protocol surface: all 11 tools now declare read-only / destructive / open-world hints that match their behavior, descriptions route between sibling tools instead of leaving agents to guess, and `scripts/smoke-mcp.ts` guards the pr_status combination. Description honesty is held to the same standard as behavior: ranking claims, defaults, and error semantics in tool text must match the code.
|
|
1596
1596
|
|
|
1597
|
-
|
|
1597
|
+
## Open Questions
|
|
1598
1598
|
|
|
1599
|
-
|
|
1599
|
+
_Pruned 2026-10-04 (D75): entries decided in D56/D57/D61/D68 and the shipped layers of the retrieval-robustness question were removed — the decisions are the record. What remains below is genuinely open._
|
|
1600
1600
|
|
|
1601
1601
|
- **Non-programmer users: PMs, BAs, and entry points (open, 2026-10-03):** the current experience assumes a programmer's toolchain — terminal, npm install, editor plugin, git branches, PR review. Project managers and BAs have none of that, and their most likely entry points are not the VS Code chat window: at most an agentic CLI (opencode, Claude Code), more probably Teams chat, MS Copilot, or another chat surface calling the same MCP server. The mechanics must hide behind two confirmations ("remember this?" / "share with the team?") — scope/promote/supersede/outbox vocabulary is a programmer dialect they should never have to learn. Three roles to design for, in increasing order of build cost: (1) **sources** — their meetings and decisions are the highest-value project memory, captured by someone else's agent with attribution in `source`; needs nothing new. (2) **curators** — "is this true? is it safe to share?" is business-truth review, which BAs are often better placed to do than programmers; GitHub's web PR review is the only existing no-CLI door, and CURATOR.md would need a plain-language path for reviewers who have never touched git. (3) **direct users** — capture sources shift from chat commands to meeting transcripts, email, and tickets; and without git there is no courier: a shared-folder transport is mechanically easy (memories are Markdown files) but has no PR-equivalent review step, so a substitute review flow has to be designed. Also weigh: stakeholder/customer facts carry PII and retention weight that code conventions never do, so "is it safe to share" becomes a compliance question, not a hygiene one. If a company ever gives every employee one unified agent entry point (a work buddy / AI team mate), most of this audience split dissolves — the question is what open-memex should assume until then: heterogeneous entry points, one memory service behind them.
|
|
1602
1602
|
|
|
1603
1603
|
- **Retrospective distillation: mining host transcripts after the fact (open, 2026-10-03):** open-memex captures only in-session (keyword triggers, explicit `memory_add`) and keeps **no transcript** — so anything not captured during the conversation is gone once it ends. Hosts with durable memory (e.g. Muse) avoid this loss with two layers open-memex skipped: full transcript retention plus background distillation. The gap is architectural, not a timing detail: the raw material for a second chance does not exist on our side. But the hosts themselves already retain it — opencode persists sessions and messages in its local store; Claude Code keeps JSONL transcripts under `~/.claude/projects/`; VS Code Copilot's chat state is locked in workspace storage (readable, fragile, not a first target). Candidate direction: a "replay distillation" flow that exports a past session's transcript and has an agent mine it in a dedicated review session, with every extracted candidate landing as an **outbox draft** for human review — never a direct save. Boundaries that keep this inside the existing design: open-memex builds no transcript store of its own (borrow the host's), mining judgment is the agent's LLM work (we provide the export and the queue, not an extractor), fully local, mined drafts carry their provenance (`source` marks them as distilled, from which session). Open sub-questions: host adapters per tool (opencode store format, Claude Code JSONL), how the user picks sessions to mine (by date? by project? "everything since last sync"?), and whether the proactive-capture criteria (open question above) should be the mining rubric verbatim — likely yes.
|
|
1604
1604
|
|
|
1605
|
-
- **
|
|
1605
|
+
- **Novice experience: the 60-second magic moment and the plain-language layer (open, 2026-10-03; trimmed 2026-10-04):** the privacy model is now stated in plain words at init (D56). Still open: init offering to save one sample memory and immediately having the user's agent answer a question with it — the user *sees* recall work before being asked to trust it — and a plain-language layer over the review ladder ("saved on this computer" / "shared with the team") with outbox/submit/promote vocabulary becoming the advanced view. This is the same vocabulary wall the PM/BA question above hits from the other side.
|
|
1606
|
+
|
|
1607
|
+
- **Retrieval evaluation: measure recall before choosing a semantic layer (open, 2026-10-04):** the retrieval-robustness question closed with all three cheap layers shipped (D56 query construction; D61 agent-side expansion + capture-time aliases), and embeddings stayed deferred *by judgment, without numbers*. A comparable project (agent-memory) publishes an offline recall@5 of 0.98 and wears it as a badge. open-memex should build the same kind of small, fixed eval — a query set over a seeded store, recall@k / MRR for the current FTS5 pipeline — and publish the number. The semantic-layer decision then gets a gate: if FTS5 + aliases is good enough, embeddings stay deferred; if a query class measurably fails, that class defines what an optional local-embedding layer must beat. Eval first, architecture second.
|
|
1606
1608
|
|
|
1607
|
-
- **
|
|
1609
|
+
- **Per-client trust levels (open, 2026-10-04):** the review ladder is currently global: every host's writes face the same gates. A comparable tool (auxly) sets a trust level per agent — one agent's writes land immediately, another's queue for approval, a third is read-only. That maps naturally onto open-memex, since the ladder already exists; the open questions are the config surface (per host? per MCP client name? how does the server learn the client's identity reliably?), the default for unknown clients, and whether trust should also gate *reads* of the personal scope. Signal to watch: one user running several agents with different autonomy expectations.
|
|
1608
1610
|
|
|
1609
|
-
- **
|
|
1611
|
+
- **Cold-start import: index what the user already has (open, 2026-10-04):** adoption today starts from an empty store. Comparable tools index the user's existing notes (memtomem) or import other memory systems' exports (remnic: mem0 / ChatGPT / Claude). Candidate first steps, cheapest first: (1) import a folder of plain Markdown notes as personal-scope outbox drafts — never direct saves, the review gate stays the front door; (2) named-export importers once (1) proves the pipeline. Open sub-questions: chunking policy for long notes (one file ≠ one memory), dedup against existing memories, and provenance marking (`source: import`).
|
|
1610
1612
|
|
|
1611
|
-
- **
|
|
1613
|
+
- **Session working state: resume where you left off (open, 2026-10-04):** open-memex stores durable knowledge, not the state of an interrupted task. memryzed's third layer — per-repository session state (open files, last commands, where the work stands) so a fresh session resumes mid-task — names the gap. Candidate shape, if pursued: an ephemeral per-project working-state note, agent-written at wrap-up and surfaced at session start alongside memory injection, *outside* the review ladder (it is scratch state, not knowledge) with a short TTL. Open questions: what belongs in it (task summary? file list? next step?), who writes it (agent at Stop? user command?), and the overlap with the retrospective-distillation and checkpoint-proactivity threads. Defer until there is signal that users lose work context, not just facts.
|
|
1612
1614
|
|
|
1613
1615
|
---
|
|
1614
1616
|
|
|
Binary file
|
|
Binary file
|
package/docs/release-runbook.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
发布时照着这一页走。npm publish 和 MCP Registry 发布都在维护者本机执行。
|
|
4
4
|
|
|
5
|
+
> 命令一条一条跑,不要整段一起贴:`publish` 之前有几道必须确认成功的关卡,顺序错了会把半成品发出去。
|
|
6
|
+
|
|
5
7
|
## 同步版本号的四个地方
|
|
6
8
|
|
|
7
9
|
- `package.json` 和 `package-lock.json`(同一个 commit 里一起改)
|
|
@@ -15,15 +17,27 @@
|
|
|
15
17
|
- 纯修 bug:patch 线递增
|
|
16
18
|
- 已经 push 过的版本号,内容再有变化必须 bump,不能原地改
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## 发布前检查(在仓库根目录执行,一条一条来)
|
|
21
|
+
|
|
22
|
+
先拉最新代码:
|
|
19
23
|
|
|
20
24
|
```powershell
|
|
21
25
|
git pull
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
再构建:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
22
31
|
npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
最后跑全量测试。`test-full` 之前必须先 `build`:测试里有 dist 内容指纹守卫,没 build 会误报失败。
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
23
37
|
npm run test-full
|
|
24
38
|
```
|
|
25
39
|
|
|
26
|
-
|
|
40
|
+
测试全绿再往下走,中间任何一步报错就停。
|
|
27
41
|
|
|
28
42
|
## 本机前提(Windows)
|
|
29
43
|
|
|
@@ -31,6 +45,11 @@ npm run test-full
|
|
|
31
45
|
|
|
32
46
|
```powershell
|
|
33
47
|
nvm use 22.23.3
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
确认版本已经切好:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
34
53
|
node -v
|
|
35
54
|
```
|
|
36
55
|
|
|
@@ -42,8 +61,15 @@ npm install -g open-memex
|
|
|
42
61
|
|
|
43
62
|
## 发 alpha
|
|
44
63
|
|
|
64
|
+
先确认 npm 登录态:
|
|
65
|
+
|
|
45
66
|
```powershell
|
|
46
67
|
npm whoami
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
输出的是自己的 npm 用户名再发布:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
47
73
|
npm publish --tag alpha
|
|
48
74
|
```
|
|
49
75
|
|
|
@@ -52,21 +78,27 @@ npm publish --tag alpha
|
|
|
52
78
|
## 发 stable
|
|
53
79
|
|
|
54
80
|
1. main 上的版本号落到 `X.Y.Z`(可让小沐代提交),`CHANGELOG.md` 完成分节,`server.json` 同步到同一版本。
|
|
55
|
-
2.
|
|
81
|
+
2. 一条一条执行:先确认登录态:
|
|
56
82
|
|
|
57
83
|
```powershell
|
|
58
84
|
npm whoami
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
再发布:
|
|
88
|
+
|
|
89
|
+
```powershell
|
|
59
90
|
npm publish
|
|
60
91
|
```
|
|
61
92
|
|
|
62
93
|
3. 把版本号告诉小沐(例如"0.7.2 published"):小沐核验 npm latest,然后打 git tag `vX.Y.Z` 并建 GitHub Release。
|
|
63
|
-
4. 同步 MCP Registry
|
|
94
|
+
4. 同步 MCP Registry。**必须等 npm 新版生效后再跑**:registry 会去 npm 抓新版本的实际包验 `mcpName` 和版本。`npm publish` 成功后先等 1–2 分钟,再执行发布(登录态过期时才需要先重跑 `login github`,也是一条一条来):
|
|
64
95
|
|
|
65
96
|
```powershell
|
|
66
|
-
& "$env:USERPROFILE\tools\mcp-publisher\mcp-publisher.exe" login github
|
|
67
97
|
& "$env:USERPROFILE\tools\mcp-publisher\mcp-publisher.exe" publish
|
|
68
98
|
```
|
|
69
99
|
|
|
100
|
+
在仓库根目录执行。万一报版本相关的错,多半是 npm 元数据还没传播完,再等两分钟重跑这一条即可。
|
|
101
|
+
|
|
70
102
|
## 出问题先看这里
|
|
71
103
|
|
|
72
104
|
- `npm publish` 失败:先确认 `npm whoami` 的登录态,再看报错第一行。
|
package/glama.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-memex",
|
|
3
|
-
"version": "0.7.2",
|
|
3
|
+
"version": "0.7.3-alpha.2",
|
|
4
4
|
"mcpName": "io.github.stoneskin/open-memex",
|
|
5
5
|
"description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
|
|
6
6
|
"keywords": [
|
package/scripts/smoke-mcp.ts
CHANGED
|
@@ -85,6 +85,9 @@ try {
|
|
|
85
85
|
JSON.stringify(["memory_add", "memory_forget", "memory_list", "memory_pr_status", "memory_promote", "memory_propose", "memory_resolve", "memory_search", "memory_status", "memory_submit", "memory_supersede"]),
|
|
86
86
|
names.join(","),
|
|
87
87
|
);
|
|
88
|
+
// D76: annotations must tell the truth — pr_status mutates with apply=true.
|
|
89
|
+
const prStatusTool = tools.result.tools.find((t: any) => t.name === "memory_pr_status");
|
|
90
|
+
check("pr_status is not read-only (D76)", prStatusTool?.annotations?.readOnlyHint === false, JSON.stringify(prStatusTool?.annotations));
|
|
88
91
|
const addSchema = tools.result.tools.find((t: any) => t.name === "memory_add").inputSchema;
|
|
89
92
|
check("memory_add schema has content+type+scope+tags", !!addSchema.properties?.content && !!addSchema.properties?.type, Object.keys(addSchema.properties ?? {}).join(","));
|
|
90
93
|
|
package/src/mcp.ts
CHANGED
|
@@ -209,6 +209,7 @@ export async function runMcpServer() {
|
|
|
209
209
|
{
|
|
210
210
|
description: TOOL_DESCRIPTIONS.memory_add,
|
|
211
211
|
inputSchema: z.object(memoryAddArgs),
|
|
212
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
212
213
|
},
|
|
213
214
|
withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))),
|
|
214
215
|
);
|
|
@@ -218,7 +219,7 @@ export async function runMcpServer() {
|
|
|
218
219
|
{
|
|
219
220
|
description: TOOL_DESCRIPTIONS.memory_search,
|
|
220
221
|
inputSchema: z.object(memorySearchArgs),
|
|
221
|
-
annotations: { readOnlyHint: true },
|
|
222
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
222
223
|
},
|
|
223
224
|
withSync((args) => toMcp(searchMemories(getScope, args))),
|
|
224
225
|
);
|
|
@@ -228,7 +229,7 @@ export async function runMcpServer() {
|
|
|
228
229
|
{
|
|
229
230
|
description: TOOL_DESCRIPTIONS.memory_list,
|
|
230
231
|
inputSchema: z.object(memoryListArgs),
|
|
231
|
-
annotations: { readOnlyHint: true },
|
|
232
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
232
233
|
},
|
|
233
234
|
withSync((args) => toMcp(listMemories(getScope, args))),
|
|
234
235
|
);
|
|
@@ -238,6 +239,7 @@ export async function runMcpServer() {
|
|
|
238
239
|
{
|
|
239
240
|
description: TOOL_DESCRIPTIONS.memory_supersede,
|
|
240
241
|
inputSchema: z.object(memorySupersedeArgs),
|
|
242
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
241
243
|
},
|
|
242
244
|
withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))),
|
|
243
245
|
);
|
|
@@ -247,7 +249,7 @@ export async function runMcpServer() {
|
|
|
247
249
|
{
|
|
248
250
|
description: TOOL_DESCRIPTIONS.memory_forget,
|
|
249
251
|
inputSchema: z.object(memoryForgetArgs),
|
|
250
|
-
annotations: { destructiveHint: true },
|
|
252
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
251
253
|
},
|
|
252
254
|
withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))),
|
|
253
255
|
);
|
|
@@ -257,7 +259,7 @@ export async function runMcpServer() {
|
|
|
257
259
|
{
|
|
258
260
|
description: TOOL_DESCRIPTIONS.memory_status,
|
|
259
261
|
inputSchema: z.object(memoryStatusArgs),
|
|
260
|
-
annotations: { readOnlyHint: true },
|
|
262
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
261
263
|
},
|
|
262
264
|
withSync((_args) => toMcp(statusMemories())),
|
|
263
265
|
);
|
|
@@ -267,6 +269,7 @@ export async function runMcpServer() {
|
|
|
267
269
|
{
|
|
268
270
|
description: TOOL_DESCRIPTIONS.memory_submit,
|
|
269
271
|
inputSchema: z.object(memorySubmitArgs),
|
|
272
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
270
273
|
},
|
|
271
274
|
withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))),
|
|
272
275
|
);
|
|
@@ -276,6 +279,7 @@ export async function runMcpServer() {
|
|
|
276
279
|
{
|
|
277
280
|
description: TOOL_DESCRIPTIONS.memory_propose,
|
|
278
281
|
inputSchema: z.object(memoryProposeArgs),
|
|
282
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
279
283
|
},
|
|
280
284
|
withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))),
|
|
281
285
|
);
|
|
@@ -285,6 +289,7 @@ export async function runMcpServer() {
|
|
|
285
289
|
{
|
|
286
290
|
description: TOOL_DESCRIPTIONS.memory_promote,
|
|
287
291
|
inputSchema: z.object(memoryPromoteArgs),
|
|
292
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
288
293
|
},
|
|
289
294
|
withSync((args) => toMcp(promoteMemoryOp(args))),
|
|
290
295
|
);
|
|
@@ -294,6 +299,7 @@ export async function runMcpServer() {
|
|
|
294
299
|
{
|
|
295
300
|
description: TOOL_DESCRIPTIONS.memory_resolve,
|
|
296
301
|
inputSchema: z.object(memoryResolveArgs),
|
|
302
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
297
303
|
},
|
|
298
304
|
withSync((args) => toMcp(resolveMemoryOp(args))),
|
|
299
305
|
);
|
|
@@ -303,7 +309,7 @@ export async function runMcpServer() {
|
|
|
303
309
|
{
|
|
304
310
|
description: TOOL_DESCRIPTIONS.memory_pr_status,
|
|
305
311
|
inputSchema: z.object(memoryPrStatusArgs),
|
|
306
|
-
annotations: { readOnlyHint:
|
|
312
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
307
313
|
},
|
|
308
314
|
withSync((args) => toMcp(prStatusOp(args))),
|
|
309
315
|
);
|
package/src/tools/ops.ts
CHANGED
|
@@ -66,35 +66,35 @@ export async function withOutboxNote(
|
|
|
66
66
|
/** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
|
|
67
67
|
export const TOOL_DESCRIPTIONS = {
|
|
68
68
|
memory_add:
|
|
69
|
-
"Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
|
|
69
|
+
"Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects. Returns the new memory's id; an identical fact already stored returns that id instead of duplicating.",
|
|
70
70
|
memory_search:
|
|
71
|
-
"Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
|
|
71
|
+
"Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored. Results are ranked by BM25 within review tiers — reviewed (approved/published) project memories come first. `limit` caps how many come back (default 8) and `type` restricts to one memory type.",
|
|
72
72
|
memory_list:
|
|
73
|
-
"List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown).",
|
|
73
|
+
"List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown). `type` filters to one memory type; `limit` caps how many entries come back.",
|
|
74
74
|
memory_supersede:
|
|
75
|
-
"Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
|
|
75
|
+
"Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated. Only active memories can be superseded; the new memory inherits type, tags, and aliases unless you pass replacements. An unknown id is reported back as a failure; nothing is written.",
|
|
76
76
|
memory_forget:
|
|
77
|
-
"Delete a memory by id. Use when the user asks to forget something. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
|
|
77
|
+
"Delete a memory by id. Use when the user asks to forget something. To replace an outdated fact with a corrected one instead of deleting it, use memory_supersede. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
|
|
78
78
|
memory_status:
|
|
79
|
-
"Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
|
|
79
|
+
"Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. For the memories themselves use memory_list; for PR review state use memory_pr_status. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
|
|
80
80
|
memory_submit:
|
|
81
|
-
"Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.)",
|
|
81
|
+
"Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.) Unknown ids or a failed local commit abort the submit with the reason; nothing is published automatically.",
|
|
82
82
|
memory_propose:
|
|
83
|
-
"Copy personal memories into the project outbox as review drafts. The personal originals stay put. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
|
|
83
|
+
"Copy personal memories into the project outbox as review drafts. The personal originals stay put. Use this when personal knowledge should enter a project's review flow; memory_submit then moves the drafts into the repo, and memory_promote advances drafts already there. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
|
|
84
84
|
memory_promote:
|
|
85
|
-
"Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
|
|
85
|
+
"Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. One step per call; an unknown id or a memory already published errors. Pass `by` to record the reviewer when it should differ from the default (e.g. approving on the strength of someone else's review). For brand-new outbox drafts, memory_submit is the entry point. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
|
|
86
86
|
memory_resolve:
|
|
87
|
-
"List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
|
|
87
|
+
"List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. Run it when a git pull or merge leaves memory files conflicted; the merged file is written in place (undo with git if the merge is wrong). For PR review state rather than file conflicts, use memory_pr_status. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
|
|
88
88
|
memory_pr_status:
|
|
89
|
-
"Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default; apply=true
|
|
89
|
+
"Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default — that mode changes nothing; apply=true writes the mapped transitions to the local files (still no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
|
|
90
90
|
} as const;
|
|
91
91
|
|
|
92
92
|
/** Shared zod input shapes (raw shape, not z.object — hosts wrap as needed). */
|
|
93
93
|
export const scopeArg = z
|
|
94
|
-
.enum(["project", "personal"
|
|
94
|
+
.enum(["project", "personal"])
|
|
95
95
|
.optional()
|
|
96
96
|
.describe(
|
|
97
|
-
"Memory scope. `project` = tied to this repo. `personal` = global across all your projects.
|
|
97
|
+
"Memory scope. `project` = tied to this repo. `personal` = global across all your projects. Default: project.",
|
|
98
98
|
);
|
|
99
99
|
|
|
100
100
|
export const aliasesArg = z
|
|
@@ -128,23 +128,23 @@ export const memorySearchArgs = {
|
|
|
128
128
|
.min(1)
|
|
129
129
|
.describe("Free-text query. File paths, error strings, identifiers work well."),
|
|
130
130
|
scope: z
|
|
131
|
-
.enum(["project", "personal", "
|
|
131
|
+
.enum(["project", "personal", "both"])
|
|
132
132
|
.optional()
|
|
133
133
|
.describe("Which scope(s) to search. Default: both."),
|
|
134
|
-
type: z.string().optional().describe("Restrict to memories of this type."),
|
|
135
|
-
limit: z.number().int().min(1).max(50).optional(),
|
|
134
|
+
type: z.string().optional().describe("Restrict to memories of this type (e.g. decision, preference, gotcha)."),
|
|
135
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 8)."),
|
|
136
136
|
};
|
|
137
137
|
export type MemorySearchArgs = z.infer<z.ZodObject<typeof memorySearchArgs>>;
|
|
138
138
|
|
|
139
139
|
export const memoryListArgs = {
|
|
140
140
|
scope: z
|
|
141
|
-
.enum(["project", "personal", "
|
|
141
|
+
.enum(["project", "personal", "both"])
|
|
142
142
|
.optional()
|
|
143
143
|
.describe(
|
|
144
144
|
"Which scope(s) to list. `project` = tied to this repo. `personal` = global across all your projects. `both` lists each scope's newest. Default: project.",
|
|
145
145
|
),
|
|
146
|
-
type: z.string().optional(),
|
|
147
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
146
|
+
type: z.string().optional().describe("Only list memories of this type (e.g. decision, preference, gotcha)."),
|
|
147
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max entries to list, newest first (default 20)."),
|
|
148
148
|
include: z
|
|
149
149
|
.enum(["active", "all"])
|
|
150
150
|
.optional()
|
|
@@ -231,9 +231,9 @@ export type MemoryPrStatusArgs = z.infer<z.ZodObject<typeof memoryPrStatusArgs>>
|
|
|
231
231
|
|
|
232
232
|
function resolveScope(
|
|
233
233
|
getScope: () => Scope,
|
|
234
|
-
kind?: "project" | "personal"
|
|
234
|
+
kind?: "project" | "personal",
|
|
235
235
|
): Scope {
|
|
236
|
-
return kind === "personal"
|
|
236
|
+
return kind === "personal" ? PERSONAL_SCOPE : getScope();
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function buildFrontmatter(
|
|
@@ -346,7 +346,7 @@ export async function searchMemories(
|
|
|
346
346
|
): Promise<ToolResult> {
|
|
347
347
|
const project = getScope();
|
|
348
348
|
const scopeKeys =
|
|
349
|
-
args.scope === "personal"
|
|
349
|
+
args.scope === "personal"
|
|
350
350
|
? [PERSONAL_SCOPE.key]
|
|
351
351
|
: args.scope === "project"
|
|
352
352
|
? [project.key]
|
|
@@ -407,7 +407,7 @@ export async function listMemories(
|
|
|
407
407
|
}
|
|
408
408
|
const s = resolveScope(
|
|
409
409
|
getScope,
|
|
410
|
-
args.scope === "project" || args.scope === "personal"
|
|
410
|
+
args.scope === "project" || args.scope === "personal"
|
|
411
411
|
? args.scope
|
|
412
412
|
: undefined,
|
|
413
413
|
);
|