open-memex 0.4.0-alpha.5 → 0.4.0
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 +8 -0
- package/README.md +191 -18
- package/README.zh-CN.md +164 -14
- package/dist/cli.js +282 -17
- package/dist/config.js +2 -0
- package/dist/distill-agents.js +62 -0
- package/dist/doctor.js +1 -1
- package/dist/export.js +157 -0
- package/dist/init.js +8 -0
- package/dist/mcp.js +42 -3
- package/dist/providers/git.js +142 -0
- package/dist/store/markdown.js +8 -6
- package/dist/tools/ops.js +6 -2
- package/docs/CURATOR.md +59 -0
- package/docs/TEST-PLAN.md +72 -0
- package/docs/V2-DESIGN.md +153 -7
- package/package.json +1 -1
- package/scripts/test-full.ts +345 -0
- package/src/cli.ts +295 -17
- package/src/config.ts +10 -0
- package/src/distill-agents.ts +85 -0
- package/src/doctor.ts +1 -1
- package/src/export.ts +206 -0
- package/src/init.ts +8 -0
- package/src/mcp.ts +43 -3
- package/src/providers/git.ts +191 -0
- package/src/store/markdown.ts +8 -6
- package/src/store/sync.ts +1 -1
- package/src/tools/ops.ts +8 -2
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* open-memex generic MCP server (stdio transport).
|
|
3
3
|
*
|
|
4
|
-
* Exposes the
|
|
4
|
+
* Exposes the memory tools as the opencode plugin
|
|
5
5
|
* (memory_add / memory_search / memory_list / memory_supersede /
|
|
6
|
-
* memory_forget
|
|
6
|
+
* memory_forget, plus memory_status / memory_submit / memory_propose /
|
|
7
|
+
* memory_promote / memory_resolve / memory_pr_status) over the Model Context Protocol, so any MCP client —
|
|
7
8
|
* VS Code Copilot Chat, Cursor, Claude Code, etc. — can use open-memex
|
|
8
9
|
* without a host-specific plugin.
|
|
9
10
|
*
|
|
@@ -19,13 +20,28 @@
|
|
|
19
20
|
*/
|
|
20
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
22
26
|
import { z } from "zod";
|
|
23
27
|
import { loadConfig } from "./config.js";
|
|
24
28
|
import { resolveProjectScope, PERSONAL_SCOPE } from "./scope.js";
|
|
25
29
|
import { db } from "./store/db.js";
|
|
26
30
|
import { syncScope } from "./store/sync.js";
|
|
27
31
|
import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, statusMemories, submitMemoriesOp, proposeMemoriesOp, promoteMemoryOp, resolveMemoryOp, prStatusOp, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, memoryStatusArgs, memorySubmitArgs, memoryProposeArgs, memoryPromoteArgs, memoryResolveArgs, memoryPrStatusArgs, TOOL_DESCRIPTIONS, } from "./tools/ops.js";
|
|
28
|
-
|
|
32
|
+
// Server version tracks package.json — never hardcode it here again.
|
|
33
|
+
// package.json sits two levels above this file in both layouts
|
|
34
|
+
// (src/mcp.ts and dist/mcp.js), same convention as cli.ts --version.
|
|
35
|
+
const SERVER_VERSION = (() => {
|
|
36
|
+
try {
|
|
37
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
39
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0-unknown";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "0.0.0-unknown";
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
29
45
|
/**
|
|
30
46
|
* D26: session-start guidance delivered through the MCP handshake itself.
|
|
31
47
|
* The init-written instruction files only exist if the user ran
|
|
@@ -53,6 +69,15 @@ propose, promote, resolve, pr_status).
|
|
|
53
69
|
- BE PROACTIVE: when the user shares something worth remembering across sessions
|
|
54
70
|
(a decision, a preference, a project convention, a fix and its cause), call
|
|
55
71
|
memory_add without being asked. Keep each memory to one self-contained statement.
|
|
72
|
+
- At the same checkpoints (session start, end of a work chunk, after the user
|
|
73
|
+
commits, after any memory_* action), DISTILL the session: propose 1–3 short
|
|
74
|
+
memories capturing the useful conclusion — what was learned or decided, how an
|
|
75
|
+
issue was resolved, what to avoid, where the authoritative doc lives — not the
|
|
76
|
+
raw transcript. Save NOTHING the user did not approve; on approval call
|
|
77
|
+
memory_add with source "inference" at the confirmed scope. If the knowledge
|
|
78
|
+
already lives in project docs, save a \`reference\` memory pointing at the doc
|
|
79
|
+
instead of copying it. Long-form notes are fine ONLY when the user explicitly
|
|
80
|
+
asks to save one.
|
|
56
81
|
- Before asking the user about past decisions, conventions, or preferences they
|
|
57
82
|
may have told you before, call memory_search first.
|
|
58
83
|
- Memories default to this project's scope; use the personal scope for facts
|
|
@@ -79,6 +104,20 @@ export async function runMcpServer() {
|
|
|
79
104
|
syncScope(scope.key, "session");
|
|
80
105
|
syncScope(PERSONAL_SCOPE.key, "session");
|
|
81
106
|
console.error(`[open-memex] MCP server up. scope=${scope.key}`);
|
|
107
|
+
// D12: pulls are explicit by default — session start never touches the
|
|
108
|
+
// network. With sync.autoPull, one best-effort pull; a failure never
|
|
109
|
+
// blocks the session, it just logs and continues.
|
|
110
|
+
if (cfg.sync?.autoPull) {
|
|
111
|
+
try {
|
|
112
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
113
|
+
const r = new GitProvider().pull(process.cwd());
|
|
114
|
+
syncScope(scope.key, "pull");
|
|
115
|
+
console.error(`[open-memex] auto-pull: ${r.branch} ${r.fastForwarded ? "fast-forwarded" : "already up to date"}`);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(`[open-memex] auto-pull skipped: ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
82
121
|
// D26: re-sync on every request, not just at startup. The in-repo dir
|
|
83
122
|
// follows the current git branch, so a branch switch mid-session would
|
|
84
123
|
// otherwise leave the index pointing at files that no longer exist.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitProvider — the git transport for repo-synced shared scopes (§9, D12).
|
|
3
|
+
*
|
|
4
|
+
* Git is a transport, not the product boundary: this provider moves the
|
|
5
|
+
* in-repo memory dir (`.ai/open-memex/`) between the local checkout and the
|
|
6
|
+
* remote. It never touches the appdata outbox, never force-pushes, never
|
|
7
|
+
* auto-merges a divergence — those are human decisions.
|
|
8
|
+
*
|
|
9
|
+
* Hard rules (from §9 / D12 / D36):
|
|
10
|
+
* - pull is explicit (`open-memex pull`); pull = fetch + fast-forward only.
|
|
11
|
+
* - push is explicit (`open-memex push`); the tool never pushes on its own.
|
|
12
|
+
* - a failed pull/push fails with a clear message and leaves no broken state.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
function fail(msg) {
|
|
16
|
+
throw new Error(`[open-memex] ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
/** Run git, raising a readable error. `timeoutMs` guards network calls. */
|
|
19
|
+
function git(root, args, timeoutMs = 0) {
|
|
20
|
+
try {
|
|
21
|
+
return execFileSync("git", args, {
|
|
22
|
+
cwd: root,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
...(timeoutMs > 0 ? { timeout: timeoutMs } : {}),
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const err = e;
|
|
30
|
+
if (err.code === "ETIMEDOUT")
|
|
31
|
+
fail(`git ${args.join(" ")} timed out — remote unreachable?`);
|
|
32
|
+
const detail = (err.stderr ?? err.message ?? "").trim().split("\n")[0];
|
|
33
|
+
fail(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
37
|
+
export class GitProvider {
|
|
38
|
+
name = "git";
|
|
39
|
+
capabilities = {
|
|
40
|
+
read: true,
|
|
41
|
+
write: true,
|
|
42
|
+
delete: false,
|
|
43
|
+
history: true,
|
|
44
|
+
sync: "bidirectional",
|
|
45
|
+
};
|
|
46
|
+
/** Current branch + upstream, or a clear failure when git can't answer. */
|
|
47
|
+
status(root) {
|
|
48
|
+
git(root, ["rev-parse", "--git-dir"]);
|
|
49
|
+
const branch = git(root, ["branch", "--show-current"]) ||
|
|
50
|
+
git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
51
|
+
let upstream = null;
|
|
52
|
+
try {
|
|
53
|
+
upstream = git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
upstream = null;
|
|
57
|
+
}
|
|
58
|
+
let ahead = 0;
|
|
59
|
+
let behind = 0;
|
|
60
|
+
if (upstream) {
|
|
61
|
+
const counts = git(root, ["rev-list", "--left-right", "--count", `HEAD...${upstream}`]);
|
|
62
|
+
const [a, b] = counts.split(/\s+/).map((n) => parseInt(n, 10));
|
|
63
|
+
ahead = Number.isFinite(a) ? a : 0;
|
|
64
|
+
behind = Number.isFinite(b) ? b : 0;
|
|
65
|
+
}
|
|
66
|
+
const remote = upstream ? upstream.split("/")[0] : null;
|
|
67
|
+
return { branch, upstream, remote, ahead, behind };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Explicit pull: fetch + fast-forward only (§9).
|
|
71
|
+
* Diverged branches are NOT merged — the user resolves them by hand.
|
|
72
|
+
*/
|
|
73
|
+
pull(root) {
|
|
74
|
+
const st = this.status(root);
|
|
75
|
+
if (!st.upstream || !st.remote) {
|
|
76
|
+
fail(`branch ${st.branch} has no upstream — set one with ` +
|
|
77
|
+
`\`git push -u <remote> ${st.branch}\`, then pull again`);
|
|
78
|
+
}
|
|
79
|
+
git(root, ["fetch", st.remote], FETCH_TIMEOUT_MS);
|
|
80
|
+
const before = git(root, ["rev-parse", "HEAD"]);
|
|
81
|
+
const remoteSha = git(root, ["rev-parse", st.upstream]);
|
|
82
|
+
if (before === remoteSha) {
|
|
83
|
+
return {
|
|
84
|
+
at: new Date().toISOString(),
|
|
85
|
+
branch: st.branch,
|
|
86
|
+
remote: st.remote,
|
|
87
|
+
before,
|
|
88
|
+
after: before,
|
|
89
|
+
fastForwarded: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Fast-forward is possible iff HEAD is an ancestor of the upstream.
|
|
93
|
+
let ffPossible = false;
|
|
94
|
+
try {
|
|
95
|
+
git(root, ["merge-base", "--is-ancestor", "HEAD", st.upstream]);
|
|
96
|
+
ffPossible = true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
ffPossible = false;
|
|
100
|
+
}
|
|
101
|
+
if (!ffPossible) {
|
|
102
|
+
fail(`branch ${st.branch} has diverged from ${st.upstream} — ` +
|
|
103
|
+
`open-memex never force-merges; resolve it by hand ` +
|
|
104
|
+
`(rebase or merge), then pull again`);
|
|
105
|
+
}
|
|
106
|
+
git(root, ["merge", "--ff-only", st.upstream]);
|
|
107
|
+
const after = git(root, ["rev-parse", "HEAD"]);
|
|
108
|
+
return {
|
|
109
|
+
at: new Date().toISOString(),
|
|
110
|
+
branch: st.branch,
|
|
111
|
+
remote: st.remote,
|
|
112
|
+
before,
|
|
113
|
+
after,
|
|
114
|
+
fastForwarded: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Explicit push of the current branch. Never called automatically. */
|
|
118
|
+
push(root) {
|
|
119
|
+
const st = this.status(root);
|
|
120
|
+
if (!st.remote) {
|
|
121
|
+
// No upstream yet: push explicitly sets it (-u), still user-invoked.
|
|
122
|
+
const remotes = git(root, ["remote"]);
|
|
123
|
+
const remote = remotes.split("\n").map((r) => r.trim()).filter(Boolean)[0];
|
|
124
|
+
if (!remote)
|
|
125
|
+
fail("no git remote configured — add one before pushing");
|
|
126
|
+
git(root, ["push", "-u", remote, st.branch], FETCH_TIMEOUT_MS);
|
|
127
|
+
return {
|
|
128
|
+
at: new Date().toISOString(),
|
|
129
|
+
branch: st.branch,
|
|
130
|
+
remote,
|
|
131
|
+
head: git(root, ["rev-parse", "HEAD"]),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
git(root, ["push", st.remote, st.branch], FETCH_TIMEOUT_MS);
|
|
135
|
+
return {
|
|
136
|
+
at: new Date().toISOString(),
|
|
137
|
+
branch: st.branch,
|
|
138
|
+
remote: st.remote,
|
|
139
|
+
head: git(root, ["rev-parse", "HEAD"]),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/store/markdown.js
CHANGED
|
@@ -3,19 +3,21 @@ import path from "node:path";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
5
|
import { memoriesDirFor, memoriesDirPath, inRepoMemoriesDirPath, } from "../paths.js";
|
|
6
|
-
/** v2 content-kind taxonomy (V2-DESIGN §3.1). `type` = what the memory IS
|
|
6
|
+
/** v2 content-kind taxonomy (V2-DESIGN §3.1). `type` = what the memory IS
|
|
7
|
+
* (single-valued, drives behavior). D41: 11 types; `warning`→`gotcha`,
|
|
8
|
+
* `workflow`→`howto`, `incident`→`lesson`, `architecture`→`knowledge`. */
|
|
7
9
|
export const MEMORY_TYPE_TAXONOMY = [
|
|
8
|
-
"preference",
|
|
9
10
|
"fact",
|
|
11
|
+
"preference",
|
|
10
12
|
"decision",
|
|
11
|
-
"lesson",
|
|
12
|
-
"warning",
|
|
13
|
-
"workflow",
|
|
14
|
-
"architecture",
|
|
15
13
|
"constraint",
|
|
16
14
|
"todo",
|
|
17
15
|
"knowledge",
|
|
16
|
+
"howto",
|
|
17
|
+
"gotcha",
|
|
18
|
+
"lesson",
|
|
18
19
|
"observation",
|
|
20
|
+
"reference",
|
|
19
21
|
];
|
|
20
22
|
const TAXONOMY = new Set(MEMORY_TYPE_TAXONOMY);
|
|
21
23
|
/** Defensive parse: malformed entries are dropped, never fatal. */
|
package/dist/tools/ops.js
CHANGED
|
@@ -42,9 +42,13 @@ export const memoryAddArgs = {
|
|
|
42
42
|
type: z
|
|
43
43
|
.enum(MEMORY_TYPE_TAXONOMY)
|
|
44
44
|
.optional()
|
|
45
|
-
.describe("Category of memory. Default:
|
|
45
|
+
.describe("Category of memory. Default: fact."),
|
|
46
46
|
scope: scopeArg,
|
|
47
47
|
tags: z.array(z.string()).optional().describe("Optional tags for filtering."),
|
|
48
|
+
source: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Where this memory came from. Default: tool. Pass 'inference' for agent-proposed captures at checkpoints (V2-DESIGN §3.5)."),
|
|
48
52
|
};
|
|
49
53
|
export const memorySearchArgs = {
|
|
50
54
|
query: z
|
|
@@ -161,7 +165,7 @@ export async function addMemory(getScope, cfg, args) {
|
|
|
161
165
|
const fm = buildFrontmatter(s, {
|
|
162
166
|
type: args.type ?? "fact",
|
|
163
167
|
tags: args.tags ?? [],
|
|
164
|
-
source: "tool",
|
|
168
|
+
source: args.source ?? "tool",
|
|
165
169
|
});
|
|
166
170
|
const { filePath } = writeMemoryFile(fm, redacted);
|
|
167
171
|
const mf = readMemoryFile(filePath);
|
package/docs/CURATOR.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Curator Convention
|
|
2
|
+
|
|
3
|
+
The **curator** is the human who tends a project's shared memory. It is a
|
|
4
|
+
documented convention, not a permission system — anyone on the team can act
|
|
5
|
+
as curator; the tool records *who* did *what* (the audit trail), it doesn't
|
|
6
|
+
decide who is *allowed* to.
|
|
7
|
+
|
|
8
|
+
## What the curator does
|
|
9
|
+
|
|
10
|
+
1. **Triage proposals.** `open-memex propose` puts memories up for review.
|
|
11
|
+
The curator reads them, then `open-memex promote <id>` to approve or
|
|
12
|
+
`open-memex promote <id> --reject` to send back, with `--note` saying why.
|
|
13
|
+
2. **Resolve conflicts.** `open-memex resolve` lists file-level and semantic
|
|
14
|
+
conflicts. The curator merges or picks a winner — Core never silently
|
|
15
|
+
resolves a semantic conflict; both sides stay `active` until a human
|
|
16
|
+
decides.
|
|
17
|
+
3. **Keep the garden.** Deprecate what's stale (`open-memex status <id>
|
|
18
|
+
deprecated`), supersede what's been replaced, forget what's noise.
|
|
19
|
+
Shared memory rots without pruning.
|
|
20
|
+
4. **Watch the pipeline.** `open-memex sync-status` shows the outbox, review
|
|
21
|
+
states, and uncommitted files; `open-memex pr-status --apply` maps the
|
|
22
|
+
GitHub PR state back onto `review_state`.
|
|
23
|
+
|
|
24
|
+
## Admission bar
|
|
25
|
+
|
|
26
|
+
Approve a memory when it is:
|
|
27
|
+
|
|
28
|
+
- **True** — you believe it, or it cites something verifiable.
|
|
29
|
+
- **Durable** — it will still matter in a month. Chat logs are not memories.
|
|
30
|
+
- **Scoped right** — project knowledge in project scope; personal stuff stays
|
|
31
|
+
personal (personal memories are never the curator's business).
|
|
32
|
+
- **Well-typed** — `type` says what it IS (`decision`, `gotcha`, `lesson`…),
|
|
33
|
+
`tags` say what it's ABOUT.
|
|
34
|
+
|
|
35
|
+
Send back (don't silently fix) when it's vague, duplicated, or belongs in
|
|
36
|
+
`docs/` as formal documentation instead — memory is the fast-changing long
|
|
37
|
+
tail, `docs/` is the slow-changing core.
|
|
38
|
+
|
|
39
|
+
## What the curator does NOT do
|
|
40
|
+
|
|
41
|
+
- **Never rewrite someone else's memory in place.** Propose a superseding
|
|
42
|
+
memory instead — the chain (`supersedes` / `superseded_by`) is the audit
|
|
43
|
+
trail.
|
|
44
|
+
- **Never approve their own proposals silently in team mode.** That's what
|
|
45
|
+
`--local-approve` is for — solo projects only.
|
|
46
|
+
- **Never pull rank with the tool.** If the team disagrees with a call, the
|
|
47
|
+
disagreement itself is worth a memory.
|
|
48
|
+
|
|
49
|
+
## Cadence
|
|
50
|
+
|
|
51
|
+
There is no required cadence. A workable default: triage proposals at the
|
|
52
|
+
end of each work chunk (the same checkpoint where §3.5 distillation runs),
|
|
53
|
+
and do a pruning pass when `sync-status` starts feeling noisy.
|
|
54
|
+
|
|
55
|
+
## Solo mode
|
|
56
|
+
|
|
57
|
+
No team, no curator needed. `open-memex propose --local-approve` records
|
|
58
|
+
`approved_by: self` and skips the PR. You are the curator, the proposer,
|
|
59
|
+
and the gardener — the same hygiene rules apply, just faster.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# OpenMemex 测试计划(v0.4.0-alpha.10)
|
|
2
|
+
|
|
3
|
+
> 自动化部分:`node --experimental-strip-types scripts/test-full.ts`
|
|
4
|
+
> 62 项全过(26 个 CLI 命令 + 11 个 MCP tool),隔离环境运行,不碰真实数据。
|
|
5
|
+
> 下面是机器/账号相关的部分,需要 Stone 在真机上过一遍。
|
|
6
|
+
|
|
7
|
+
## A. Windows 真机 + VS Code Copilot
|
|
8
|
+
|
|
9
|
+
- [ ] `npm i -g open-memex@alpha` 全局安装,`open-memex --version` 显示正确版本
|
|
10
|
+
- [ ] 在一个真实项目目录跑 `open-memex init`(不加 `--yes`,走一遍交互)
|
|
11
|
+
- 确认 `.vscode/mcp.json` 生成,`~/.copilot/copilot-instructions.md` 合并写入(不覆盖已有内容)
|
|
12
|
+
- [ ] 重启 VS Code,Copilot Chat 里问 "what do you remember about this project?"
|
|
13
|
+
- 预期:MCP 连接成功,能调用 memory_search
|
|
14
|
+
- [ ] `open-memex add "windows 真机测试" --type fact`,再让 Copilot 搜出来
|
|
15
|
+
- [ ] 中文路径项目、中文记忆内容各试一条(CJK 索引)
|
|
16
|
+
|
|
17
|
+
## B. 真实 GitHub PR 全流程(review 工作流)
|
|
18
|
+
|
|
19
|
+
在一个真实 repo 里:
|
|
20
|
+
|
|
21
|
+
- [ ] `open-memex add "PR流程测试" --scope personal` → `propose --to project` → `sync-status` 看到 outbox draft
|
|
22
|
+
- [ ] `open-memex submit <id>`(留在当前分支,本地 commit)
|
|
23
|
+
- [ ] 手动 `git push` + 开 PR
|
|
24
|
+
- [ ] 在 PR 里点 Approve → 回来跑 `open-memex pr-status`(先看 report),再 `pr-status --apply`
|
|
25
|
+
- 预期:memory 变成 approved,`approved_by` 是 reviewer
|
|
26
|
+
- [ ] 找一条让 reviewer 点 "Request changes" → `pr-status --apply`
|
|
27
|
+
- 预期:只给 suggestion,**不**自动 reject(D32)
|
|
28
|
+
- [ ] Merge PR → `pr-status --apply`
|
|
29
|
+
- 预期:memory 变成 published
|
|
30
|
+
- [ ] `open-memex resolve` 无冲突时输出 "(no conflicted memory files)"
|
|
31
|
+
|
|
32
|
+
## C. 其他编辑器 MCP 集成
|
|
33
|
+
|
|
34
|
+
- [ ] Cursor:`open-memex mcp --print-config cursor` → 贴到 Cursor MCP 配置 → 能连上
|
|
35
|
+
- [ ] opencode:`open-memex init --client opencode` → `opencode.jsonc` 生效
|
|
36
|
+
- [ ] Claude Code:`open-memex mcp --print-config claude` 给出的 `claude mcp add` 命令能跑通
|
|
37
|
+
|
|
38
|
+
## D. 跨机迁移(export/import 真实场景)
|
|
39
|
+
|
|
40
|
+
- [ ] 本机:`open-memex export --all -o migration.tar.gz`(含 private 的全量)
|
|
41
|
+
- [ ] 本机:`open-memex export -o share.tar.gz`(默认排除 private)→ 解包检查 manifest,确认没有 visibility:private 的条目
|
|
42
|
+
- [ ] 另一台机器:`open-memex import migration.tar.gz --dry-run` 先看预览,再正式 import
|
|
43
|
+
- 预期:project memory re-key 到新机器的 project scope,进 outbox 当 draft;personal 进 personal
|
|
44
|
+
- [ ] 同一个 bundle 导两次 → 第二次 "skipped N identical"
|
|
45
|
+
|
|
46
|
+
## E. Agent 会话行为(D42 / §3.5)
|
|
47
|
+
|
|
48
|
+
- [ ] 新开一个 agent 会话(MCP 已接),看 initialize 返回的 instructions 里有没有 session-start 同步指引
|
|
49
|
+
- [ ] 对 agent 说 "sync memory" / "同步记忆"
|
|
50
|
+
- 预期:agent 走 memory_status → 摘要 → 问你要同步哪条(而不是直接翻 appdata)
|
|
51
|
+
- [ ] 长对话中 agent 是否在检查点提议蒸馏(§3.5),提议后是否等你批准才保存(D42)
|
|
52
|
+
|
|
53
|
+
## F. 冲突解决(3-way merge)
|
|
54
|
+
|
|
55
|
+
- [ ] 两台机器(或两个 clone)同时改同一条 project memory,各自 submit + push,一边 pull 制造 diverged
|
|
56
|
+
- 预期:`open-memex pull` 明确报错退出,不自动 merge
|
|
57
|
+
- [ ] 手动 merge 后 `open-memex resolve <id>` 看 3-way 展示(base/outbox/repo),手动解决
|
|
58
|
+
|
|
59
|
+
## G. 同事 pilot(1–2 人,Stone 私下选)
|
|
60
|
+
|
|
61
|
+
- [ ] 对方 `npx open-memex@alpha init` 走通
|
|
62
|
+
- [ ] 对方能 propose → 你这边能看到 PR → promote 流程走通
|
|
63
|
+
- [ ] 收集反馈:哪里卡、哪里不符合直觉
|
|
64
|
+
|
|
65
|
+
## H. 已知问题观察
|
|
66
|
+
|
|
67
|
+
- [ ] better-sqlite3 在 Node 24 退出时偶发 crash(exit 134):注意是否丢数据(预期:不丢,只影响退出码)
|
|
68
|
+
- [ ] `memory_list` 默认只列 project scope 是否符合预期(Stone 已定保持现状)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
测试中发现的 bug 直接记到 GitHub issue;改完后更新本文档的复选框。
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -25,6 +25,12 @@ Decisions log; Prior Art; solo-dev adoption path.
|
|
|
25
25
|
6. **Adapters translate; they never implement memory logic.** All memory logic lives in Core.
|
|
26
26
|
7. **Memory is the entrance of knowledge, not its final form.** Terminal states are docs / ADRs /
|
|
27
27
|
AGENTS.md instructions — memory is how knowledge gets captured and found.
|
|
28
|
+
8. **Distill conversations; do not archive them.** The default unit of memory is the useful
|
|
29
|
+
conclusion from a conversation, not the full transcript: what was learned, how it was resolved,
|
|
30
|
+
and where the authoritative source lives.
|
|
31
|
+
9. **Portable second brain, not a walled garden.** Long-form personal notes are valid memories when
|
|
32
|
+
the user explicitly saves them; Markdown keeps them readable, exportable, and movable to other
|
|
33
|
+
tools or machines.
|
|
28
34
|
|
|
29
35
|
---
|
|
30
36
|
|
|
@@ -40,6 +46,9 @@ Decisions log; Prior Art; solo-dev adoption path.
|
|
|
40
46
|
- **MCP is an interface, not the identity.** MCP / CLI / REST / SDK are access layers over the protocol,
|
|
41
47
|
so the project is never locked to one transport or one agent tool (opencode, VS Code Copilot, Cursor,
|
|
42
48
|
Claude Code, Windsurf, …).
|
|
49
|
+
- **Second-brain lens:** for individuals, OpenMemex can also be a local Markdown second brain for
|
|
50
|
+
AI-assisted work — similar in spirit to users asking AI to save notes into Obsidian or Notion, but
|
|
51
|
+
with agent recall, scopes, review, and redaction built in from the start.
|
|
43
52
|
- **Company lens:** at organizational scale the same pain is tribal knowledge — senior engineers'
|
|
44
53
|
hard-won experience evaporates when they move on, and every incident gets re-debugged by someone
|
|
45
54
|
new. The current phase therefore prioritizes *capture*: valuable knowledge must land in memory
|
|
@@ -132,13 +141,37 @@ canonical_ref: docs/adr-003.md # memory holds a SUMMARY; the doc is canoni
|
|
|
132
141
|
|
|
133
142
|
### 3.1 Type taxonomy (content kind)
|
|
134
143
|
|
|
135
|
-
`
|
|
136
|
-
`
|
|
144
|
+
`fact` `preference` `decision` `constraint` `todo` `knowledge` `howto` `gotcha`
|
|
145
|
+
`lesson` `observation` `reference`
|
|
137
146
|
|
|
138
|
-
`type` describes **what the content is
|
|
147
|
+
`type` describes **what the content is** — single-valued, and it drives behavior
|
|
148
|
+
(lifecycle, review, rendering, retrieval). `role` describes **how it may be used**.
|
|
139
149
|
A `decision` with `role: knowledge` is retrievable history. Only `role: instruction` may enter
|
|
140
150
|
instruction context. (Separation adopted from review: mixing usage semantics into `type` was a design smell.)
|
|
141
151
|
|
|
152
|
+
One-line definitions:
|
|
153
|
+
|
|
154
|
+
- `fact` — a verifiable atomic statement (timezone, version number, path, account).
|
|
155
|
+
- `preference` — user likes, dislikes, working style.
|
|
156
|
+
- `decision` — a choice made + why; participates in supersede chains.
|
|
157
|
+
- `constraint` — a hard rule that must not be violated (release process, permission boundaries).
|
|
158
|
+
- `todo` — an actionable item with completion state.
|
|
159
|
+
- `knowledge` — declarative knowledge (how a system works, concept explanations).
|
|
160
|
+
- `howto` — steps to accomplish X.
|
|
161
|
+
- `gotcha` — a pitfall: don't do X because Y.
|
|
162
|
+
- `lesson` — a takeaway from experience, including incident postmortems (the incident id goes in `tags`).
|
|
163
|
+
- `observation` — noticed but not yet distilled.
|
|
164
|
+
- `reference` — a pointer to the authoritative doc via `canonical_ref`; the memory holds the summary.
|
|
165
|
+
|
|
166
|
+
A type earns its place only if the system treats it differently. If two candidates
|
|
167
|
+
share lifecycle, retrieval, and rendering, the loser becomes a tag. (D41 merged
|
|
168
|
+
`warning`→`gotcha`, `workflow`→`howto`, `incident`→`lesson`, `architecture`→`knowledge`.)
|
|
169
|
+
|
|
170
|
+
`tags` are retrieval hints, not a second type system: `type` says what the memory is, while tags say
|
|
171
|
+
which topics, tools, subsystems, paths, or incidents it relates to. Write paths should preserve
|
|
172
|
+
human-provided tags and may suggest simple normalized tags (for example `vscode`, `mcp`, `windows`,
|
|
173
|
+
`auth`, `onboarding`) to improve search without changing memory semantics.
|
|
174
|
+
|
|
142
175
|
### 3.2 Iron rules
|
|
143
176
|
|
|
144
177
|
- **Iron rule 1 — the instruction gate:** only memories with `role: instruction`
|
|
@@ -170,6 +203,34 @@ Content hash + fuzzy match against existing memories. A superseding write does *
|
|
|
170
203
|
the old memory becomes `status: superseded` with `superseded_by` pointing forward. History preserved;
|
|
171
204
|
queries rank `active` first.
|
|
172
205
|
|
|
206
|
+
### 3.5 Conversation distillation
|
|
207
|
+
|
|
208
|
+
OpenMemex does **not** store raw AI chat transcripts by default. A captured memory should usually be
|
|
209
|
+
a short, human-approved artifact distilled from the conversation: the final conclusion, important
|
|
210
|
+
facts, why the answer matters later, and how the issue was resolved. Good distilled memory candidates
|
|
211
|
+
answer some combination of:
|
|
212
|
+
|
|
213
|
+
- What did we learn or decide?
|
|
214
|
+
- What solved the problem, including key commands, files, links, or steps?
|
|
215
|
+
- What mistake or gotcha should the next developer avoid?
|
|
216
|
+
- Which scope owns it: `personal`, `project`, or future `org`?
|
|
217
|
+
- If the information already exists in project docs, where is the authoritative doc?
|
|
218
|
+
|
|
219
|
+
If stable knowledge already lives in a project document, prefer a `type: reference` memory with
|
|
220
|
+
`canonical_ref` pointing at that document over duplicating the full content. Memory should help the
|
|
221
|
+
agent find and apply authoritative docs, not become an outdated second copy of them.
|
|
222
|
+
|
|
223
|
+
Model-suggested captures use `source: inference` and should default to reviewable draft state. The
|
|
224
|
+
agent may propose 1-3 distilled memories at checkpoints or task end, but humans decide whether to
|
|
225
|
+
save them and which scope they belong to. The product goal is remembering the right conclusion at
|
|
226
|
+
the right scope, not remembering everything.
|
|
227
|
+
|
|
228
|
+
Long-form notes are still valid when the user explicitly asks to save a long note, write-up, meeting
|
|
229
|
+
summary, research log, or troubleshooting record. In that case OpenMemex behaves like a Markdown
|
|
230
|
+
second-brain target: store the note as user-authored content, preserve the body, tag it for retrieval,
|
|
231
|
+
and keep the same scope/privacy rules. The distinction is intent: implicit/model-suggested capture
|
|
232
|
+
distills; explicit "save this note" may preserve long-form text.
|
|
233
|
+
|
|
173
234
|
---
|
|
174
235
|
|
|
175
236
|
## 4. Scopes, Visibility & Namespaces
|
|
@@ -294,11 +355,21 @@ instructions are never candidates.)
|
|
|
294
355
|
| Implicit (opt-in) | end-of-session "should I remember X?"; implicit captures default to `confidence: low` and appear in a separate list view for batch cleanup (regret window). |
|
|
295
356
|
| Redaction (hard) | `<private>…</private>` stripped; secret patterns are **masked in place** (first 4 chars kept, rest → `x`) and the write proceeds (D14); pre-commit hook scans shared scopes. |
|
|
296
357
|
|
|
358
|
+
**Scope routing on capture:** when the user says "save", "remember", or "note this" without an
|
|
359
|
+
explicit scope, the agent should infer ownership from content and current context. Project-specific
|
|
360
|
+
knowledge (repo commands, architecture, code paths, product decisions, team conventions) routes to
|
|
361
|
+
`project`; personal preferences, private working notes, cross-project habits, or content unrelated to
|
|
362
|
+
the current repo routes to `personal`. Ambiguous captures should ask one short clarification instead
|
|
363
|
+
of guessing. `org` capture remains future/curated; a single user's chat never writes org memory
|
|
364
|
+
directly.
|
|
365
|
+
|
|
297
366
|
---
|
|
298
367
|
|
|
299
368
|
## 9. Sync
|
|
300
369
|
|
|
301
|
-
- **Git is transport; the local SQLite index is the query layer.**
|
|
370
|
+
- **Git is a transport, not the product boundary; the local SQLite index is the query layer.**
|
|
371
|
+
Retrieval never walks git. Git/GitHub is the default sharing provider because it gives teams branch,
|
|
372
|
+
PR, review, and audit semantics they already trust, but Core must remain provider-agnostic.
|
|
302
373
|
- one-memory-one-file ⇒ concurrent edits almost never conflict.
|
|
303
374
|
- **Pull is explicit** (`open-memex pull`), never automatic on session start — no surprise context changes.
|
|
304
375
|
`pull` = `fetch` + fast-forward only; never auto-commit/push (hard rule for enterprise environments).
|
|
@@ -314,6 +385,13 @@ instructions are never candidates.)
|
|
|
314
385
|
- **Git unavailable:** projects without git (or with unreachable remotes) remain fully usable.
|
|
315
386
|
`personal` scope works everywhere; `project` scope degrades to local-only and `open-memex status`
|
|
316
387
|
annotates it as such. Sync commands fail with a clear message, never with a broken state.
|
|
388
|
+
- **Portability:** because Markdown is the source of truth, memories should be exportable without
|
|
389
|
+
depending on git. A future `open-memex export` command can archive selected scopes/types/tags into a
|
|
390
|
+
portable zip/tar bundle (markdown + manifest) for moving to another computer or importing into
|
|
391
|
+
another application. Export excludes `visibility: private` memories by default; `--all` / `-a`
|
|
392
|
+
includes everything, for a full personal migration to a new machine. API-backed exporters/providers (Notion, Obsidian-compatible vaults, internal
|
|
393
|
+
knowledge systems, enterprise stores) are extension paths over the same memory files and admission
|
|
394
|
+
rules, not separate products.
|
|
317
395
|
|
|
318
396
|
---
|
|
319
397
|
|
|
@@ -456,10 +534,13 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
456
534
|
| atlaso-labs/codex | Codex marketplace | long-term memory plugin for Codex (hooks + MCP + cloud-sync upsell) | **direct comparable** for a future Codex plugin; their cloud upsell vs our local-first |
|
|
457
535
|
|
|
458
536
|
(Star counts / funding as of Sep 2026 — re-verify before quoting publicly.)
|
|
459
|
-
- **Phase 3 — Org layer.** Org memory repo ·
|
|
460
|
-
|
|
537
|
+
- **Phase 3 — Org layer.** Org memory repo · `examples/remote-server/` ·
|
|
538
|
+
curator convention ✅ `docs/CURATOR.md` (2026-09-29, pulled forward) ·
|
|
539
|
+
distill-to-AGENTS.md assist ✅ `open-memex distill-agents` (2026-09-29, pulled forward) ·
|
|
540
|
+
export/import archive command ✅ `open-memex export` / `import` (2026-09-29, pulled forward, D40).
|
|
461
541
|
- **Phase 4 — Future, signal-gated.** Cloud `RemoteProvider` customization only on: multi-private-repo
|
|
462
|
-
sharing needs, fine-grained ACL, audit/compliance mandates
|
|
542
|
+
sharing needs, fine-grained ACL, audit/compliance mandates · optional API-backed exporters/providers
|
|
543
|
+
for enterprise knowledge systems.
|
|
463
544
|
|
|
464
545
|
## 19. Migration v1 → v2
|
|
465
546
|
|
|
@@ -732,6 +813,71 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
732
813
|
fall back to the global usage. *Rationale: AI assistants discover the CLI
|
|
733
814
|
through --help first — a command that silently swallows --help as a flag
|
|
734
815
|
teaches the agent nothing. Approved 2026-09-28.*
|
|
816
|
+
- **D37** — Conversation distillation is the memory unit; transcript storage is
|
|
817
|
+
not the default. Capture should preserve the useful conclusion from a human/AI
|
|
818
|
+
session — what was learned, what resolved the issue, what should be avoided,
|
|
819
|
+
and where the authoritative doc lives — as a short reviewable memory. If the
|
|
820
|
+
knowledge already exists in docs, store a `reference` memory with
|
|
821
|
+
`canonical_ref` instead of duplicating the doc. Tags are retrieval hints, not
|
|
822
|
+
content kinds: `type` classifies the memory, tags describe topics/tools/paths.
|
|
823
|
+
*Rationale: full chat logs are noisy, harder to review, and riskier for
|
|
824
|
+
privacy; the value is remembering the right conclusion at the right scope,
|
|
825
|
+
with enough context for the next human or AI session. Approved 2026-09-28.*
|
|
826
|
+
- **D38** — OpenMemex supports a Markdown second-brain use case while keeping
|
|
827
|
+
distillation as the default for implicit/model-suggested capture. If the user
|
|
828
|
+
explicitly asks to save a long note, meeting summary, troubleshooting record,
|
|
829
|
+
or write-up, preserve it as user-authored Markdown with tags and normal
|
|
830
|
+
scope/privacy rules. Git/GitHub sync is the default team-sharing provider, not
|
|
831
|
+
the product boundary: future export/import archives and API-backed providers
|
|
832
|
+
may move the same markdown memories to other computers, applications, or
|
|
833
|
+
enterprise systems. For generic "save/remember/note this" requests, the agent
|
|
834
|
+
routes project-specific knowledge to `project`, personal or repo-unrelated
|
|
835
|
+
knowledge to `personal`, and asks one clarification if ambiguous; `org` remains
|
|
836
|
+
curated/future, never a direct single-user chat write. *Rationale: users also
|
|
837
|
+
want a local AI-assisted second brain, but portability and ownership must stay
|
|
838
|
+
explicit; sharing mechanisms should be provider choices over the same memory
|
|
839
|
+
model, not the identity of the product. Approved 2026-09-28.*
|
|
840
|
+
- **D39** — `type` and `tags` are complementary axes, not alternatives. `type` is
|
|
841
|
+
single-valued: what the memory IS — it drives behavior (lifecycle, review,
|
|
842
|
+
rendering, retrieval: a `todo` can be completed, a `reference` resolves
|
|
843
|
+
`canonical_ref`, a `decision` participates in supersede chains). `tags` are
|
|
844
|
+
multi-valued: what the memory is ABOUT — pure retrieval hints
|
|
845
|
+
(topics, tools, subsystems, paths, incidents). *Rationale: without type the
|
|
846
|
+
system cannot tell "a todo I must do" from "a fact I must know" even when both
|
|
847
|
+
are tagged `auth`; without tags, cross-cutting retrieval ("everything about
|
|
848
|
+
onboarding") would need a combinatorial type explosion. Type answers "how do I
|
|
849
|
+
handle this?", tags answer "how do I find this?". Approved 2026-09-28.*
|
|
850
|
+
- **D40** — `open-memex export` excludes `visibility: private` memories by
|
|
851
|
+
default; `--all` / `-a` includes everything, for a full personal migration to
|
|
852
|
+
a new machine. *Rationale: the safe default protects privacy; the escape hatch
|
|
853
|
+
keeps the "no walled garden" promise — the user can always take everything
|
|
854
|
+
with them. Approved 2026-09-28.*
|
|
855
|
+
- **D42** — §3.5 checkpoint distillation is taught in the MCP handshake
|
|
856
|
+
instructions (`src/mcp.ts` `SERVER_INSTRUCTIONS`) and the `init`-written
|
|
857
|
+
instruction files (`src/init.ts` `INSTRUCTIONS`): at checkpoints the agent
|
|
858
|
+
DISTILLs the session and proposes 1–3 short memories (conclusion, not
|
|
859
|
+
transcript); nothing is saved without user approval. Approved captures are
|
|
860
|
+
saved via `memory_add` with the new optional `source` param set to
|
|
861
|
+
`"inference"` (default `"tool"`). *Rationale: closes the 0.4.0 TODO from D41 —
|
|
862
|
+
the design's capture loop now reaches the agent. Implemented 2026-09-29.*
|
|
863
|
+
- **D43** — `distill-agents` output gains a "Memory hygiene (open-memex)"
|
|
864
|
+
footer carrying the §3.5 checkpoint guidance (propose 1–3 distilled captures
|
|
865
|
+
at checkpoints; save nothing without approval; prefer `reference` over
|
|
866
|
+
copying). *Rationale: double insurance for opencode users, who never see the
|
|
867
|
+
MCP handshake instructions or the `init`-written instruction files — but
|
|
868
|
+
opencode reads AGENTS.md natively, so the distilled snippet teaches the
|
|
869
|
+
checkpoint habit wherever it lands. Approved 2026-09-29.*
|
|
870
|
+
- **D41** — The type taxonomy is reconciled to 11 types with one-line definitions
|
|
871
|
+
(§3.1): `fact` `preference` `decision` `constraint` `todo` `knowledge` `howto`
|
|
872
|
+
`gotcha` `lesson` `observation` `reference`. Merged away: `warning`→`gotcha`,
|
|
873
|
+
`workflow`→`howto`, `incident`→`lesson` (incident id goes in `tags`),
|
|
874
|
+
`architecture`→`knowledge` (tag `architecture`). *Rationale: a type earns its
|
|
875
|
+
place only if the system treats it differently (lifecycle, retrieval,
|
|
876
|
+
rendering); otherwise it is a tag. Fifteen types blur classification and hurt
|
|
877
|
+
agent accuracy — eleven keeps each type's behavioral slot distinct. `lesson`
|
|
878
|
+
is deliberately broader than `incident`: a postmortem's shape (timeline, root
|
|
879
|
+
cause, actions) is a template concern, not a type. Code
|
|
880
|
+
`MEMORY_TYPE_TAXONOMY` updated to match. Approved 2026-09-28.*
|
|
735
881
|
|
|
736
882
|
## Open Questions
|
|
737
883
|
|
package/package.json
CHANGED