open-memex 0.2.0-alpha → 0.3.0-alpha
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 +32 -6
- package/README.md +239 -37
- package/README.zh-CN.md +307 -0
- package/bin/open-memex.js +28 -0
- package/docs/V2-DESIGN.md +107 -3
- package/package.json +12 -3
- package/scripts/smoke-mcp.ts +135 -0
- package/scripts/smoke-pure.ts +42 -1
- package/src/capture/keywords.ts +28 -15
- package/src/cli.ts +214 -9
- package/src/config.ts +89 -4
- package/src/doctor.ts +161 -0
- package/src/index.ts +19 -10
- package/src/init.ts +304 -0
- package/src/mcp.ts +133 -0
- package/src/redact.ts +121 -17
- package/src/tools/memory.ts +27 -202
- package/src/tools/ops.ts +259 -0
- package/PLAN.md +0 -168
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# open-memex
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
给 AI 编程助手的本地优先持久记忆:一个 [opencode](https://opencode.ai) 插件,
|
|
6
|
+
加上一个通用 MCP server(VS Code Copilot、Cursor、Claude Code、Visual Studio 等)。
|
|
7
|
+
|
|
8
|
+
- **Markdown 文件**是 source of truth(人类可读、git 友好)
|
|
9
|
+
- **SQLite FTS5** 做可重建索引(BM25 关键词检索,`better-sqlite3`)
|
|
10
|
+
- **零云端**、零账号、零第三方 API
|
|
11
|
+
- 直接跑在 opencode 内嵌的 Bun 运行时里;CLI 和 MCP server 跑在 Node 下——无需构建、无需安装 Bun
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
### 前置要求
|
|
16
|
+
|
|
17
|
+
- **Node.js ≥ 22.6**(`open-memex doctor` 会帮你检查)
|
|
18
|
+
|
|
19
|
+
### 第一步——安装 CLI
|
|
20
|
+
|
|
21
|
+
**npm(推荐):**
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install -g open-memex@alpha
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
安装的是 `0.3.0-alpha` 预览通道。(`latest` 仍指向旧的 `0.1.0` 稳定版。)
|
|
28
|
+
|
|
29
|
+
**免安装——用 npx 直接跑:**
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx -y open-memex@alpha <命令> # 例如 npx -y open-memex@alpha init --client vscode
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**从源码安装**(最新开发版,`V2-dev-p2` 分支):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
git clone -b V2-dev-p2 https://github.com/stoneskin/open-memex.git
|
|
39
|
+
cd open-memex
|
|
40
|
+
npm install
|
|
41
|
+
node --experimental-strip-types src/cli.ts <命令>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> `0.3.0-alpha` 的 npm 发布从该分支切出——如果 npx 还解析到旧的 alpha 版,
|
|
45
|
+
> 请先用源码安装,等发布落地。
|
|
46
|
+
|
|
47
|
+
#### 提示 "'open-memex' 不是内部命令"?——PATH 设置
|
|
48
|
+
|
|
49
|
+
`npm install -g` 会把 `open-memex` 启动器放到 npm 的全局 bin 目录。
|
|
50
|
+
如果终端找不到它,说明该目录不在你的 `PATH` 里:
|
|
51
|
+
|
|
52
|
+
1. 先找到这个目录:`npm config get prefix`
|
|
53
|
+
- **Windows:** 启动器(`open-memex.cmd`)就在该目录下,例如
|
|
54
|
+
`C:\Users\<你>\AppData\Roaming\npm`
|
|
55
|
+
- **macOS / Linux:** 在 `<prefix>/bin` 下,例如 `/usr/local/bin`
|
|
56
|
+
或 `~/.nvm/versions/node/v22.x.x/bin`
|
|
57
|
+
2. 把它加进 `PATH`:
|
|
58
|
+
- **Windows:** 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 →
|
|
59
|
+
把该目录加到*用户*的 `Path` 里 → **重启终端**。用 `where open-memex` 验证。
|
|
60
|
+
- **macOS / Linux:** 在 `~/.zshrc`(或 `~/.bashrc`)里加一行
|
|
61
|
+
`export PATH="$(npm prefix -g)/bin:$PATH"`,重启 shell,
|
|
62
|
+
用 `command -v open-memex` 验证。
|
|
63
|
+
3. 没有管理员权限 / 不想动 `PATH`?用上面的 npx 形式——npx 自己解析包,
|
|
64
|
+
不需要改 `PATH`。
|
|
65
|
+
|
|
66
|
+
### 第二步——给你的编辑器一键配置
|
|
67
|
+
|
|
68
|
+
在**项目根目录**下运行(这样 project scope 会解析到这个仓库):
|
|
69
|
+
|
|
70
|
+
**VS Code**(Copilot):
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
open-memex init --client vscode
|
|
74
|
+
# ……没装全局包的话:
|
|
75
|
+
npx -y open-memex@alpha init --client vscode
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
自动写 `.vscode/mcp.json` 和 `.github/copilot-instructions.md`,然后重新加载窗口,
|
|
79
|
+
在 Copilot Chat 的 MCP 面板里确认 `open-memex` server 已启动。
|
|
80
|
+
|
|
81
|
+
**Cursor:**
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
open-memex init --client cursor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
自动写 `.cursor/mcp.json` 和 `.github/copilot-instructions.md`。
|
|
88
|
+
|
|
89
|
+
**opencode**(作为普通 MCP 客户端):
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
open-memex init --client opencode
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
写项目级 `opencode.jsonc`(`type: "local"`)。想用原生插件?
|
|
96
|
+
在 `~/.config/opencode/opencode.jsonc` 里加
|
|
97
|
+
`"plugin": ["file:///absolute/path/to/open-memex/src/index.ts"]`——
|
|
98
|
+
在 tools 之外还能获得关键词自动捕获和首轮上下文注入。
|
|
99
|
+
|
|
100
|
+
**Claude Code**(在项目根目录运行):
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
claude mcp add open-memex -- open-memex mcp
|
|
104
|
+
# ……或打印配置片段:open-memex mcp --print-config claude
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Visual Studio**(在 solution 目录运行):
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
open-memex init --client visualstudio
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
写 solution 级 `.mcp.json` 和 `.github/copilot-instructions.md`。需要
|
|
114
|
+
Visual Studio 2022 17.14+ 或 Visual Studio 2026(**仅 Windows**)。
|
|
115
|
+
Visual Studio 也会自动发现 `.vscode/mcp.json` 和 `.cursor/mcp.json`,
|
|
116
|
+
所以上面的 VS Code 配置同样可用。
|
|
117
|
+
|
|
118
|
+
**Codex:** 暂无 `init` 客户端——以 `open-memex mcp --print-config` 为起点手动添加
|
|
119
|
+
(`config.toml` 的 `[mcp_servers]`,或 `codex mcp add`)。
|
|
120
|
+
|
|
121
|
+
`init` 说明:
|
|
122
|
+
|
|
123
|
+
- 在终端里会交互式询问:配哪个编辑器、是否开启关键词自动捕获、
|
|
124
|
+
是否在首轮注入记忆。`--yes` 全用默认值;脚本 / 非 TTY 环境不提问
|
|
125
|
+
(编辑器默认 VS Code)。
|
|
126
|
+
- 已有配置文件会被**合并,不会被覆盖**——重复运行是安全的。
|
|
127
|
+
`--force` 强制覆盖。
|
|
128
|
+
- 如果 `PATH` 上没有可用的 `open-memex`(比如一次性 npx),`init` 会把
|
|
129
|
+
`npx -y open-memex@alpha mcp` 写进配置,配置照样能用。
|
|
130
|
+
以后 `npm i -g open-memex@alpha` + `open-memex init --force` 可切换到更快
|
|
131
|
+
的直接调用。
|
|
132
|
+
|
|
133
|
+
### 第三步——验证
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
open-memex doctor
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
检查:Node 版本、配置来源、当前目录的 scope 解析、存储可写性,
|
|
140
|
+
然后启动一个真实的 MCP server 做 `initialize` + `tools/list`——
|
|
141
|
+
五个 tools 都必须出现。
|
|
142
|
+
|
|
143
|
+
## Agent 可用的 tools
|
|
144
|
+
|
|
145
|
+
| Tool | 作用 |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `memory_add` | 保存事实、偏好、决定、笔记 |
|
|
148
|
+
| `memory_search` | BM25 关键词检索,跨 project + personal 记忆 |
|
|
149
|
+
| `memory_list` | 按 scope 列出记忆,最新的在前 |
|
|
150
|
+
| `memory_supersede` | 用新版本替换一条记忆(保留替换链) |
|
|
151
|
+
| `memory_forget` | 按 id 删除一条记忆 |
|
|
152
|
+
|
|
153
|
+
## 捕获(Capture)
|
|
154
|
+
|
|
155
|
+
- **关键词触发**(opencode 原生插件,扫描用户消息):中文 `记住…` /
|
|
156
|
+
`记一下` / `记录一下` / `别忘了…`,英文 `remember …` / `note that …` /
|
|
157
|
+
`don't forget …` / `TIL …` / `save this …`。
|
|
158
|
+
Scope 路由:第一人称单数进 **personal**(`记住我…`、`替我记…`、
|
|
159
|
+
`我觉得…`、`我喜欢…`、`remember for me`);第一人称复数进当前
|
|
160
|
+
**project** scope(`我们认为…`、`我们决定…`、`帮我们记住…`)。
|
|
161
|
+
- **Agent 主动调用** `memory_add`
|
|
162
|
+
- **脱敏**:`<private>…</private>` 标签内的内容会被剥离;检测到的密钥
|
|
163
|
+
(API key、token、高熵凭据)就地打码——保留前 4 个字符,其余替换为 `x`——
|
|
164
|
+
然后照常保存。用 `open-memex capture --dry-run "…"` 预览一条消息会被如何捕获。
|
|
165
|
+
|
|
166
|
+
## Scope
|
|
167
|
+
|
|
168
|
+
- **project** — 绑定当前仓库(用 git origin URL 哈希做 key,无 remote 则用 cwd)。新记忆默认进这里。
|
|
169
|
+
- **personal** — 跨所有项目全局,**仅本机,永不上传/同步**。放个人偏好。(v1 叫 `user`,`migrate --to-v2` 会自动改名。)
|
|
170
|
+
|
|
171
|
+
完整 scope 模型(key 推导、迁移、visibility、保留名)见
|
|
172
|
+
[docs/SCOPES.md](./docs/SCOPES.md)。
|
|
173
|
+
|
|
174
|
+
## 检索(Retrieval)
|
|
175
|
+
|
|
176
|
+
每个会话的首轮,`open-memex` 会往 system prompt 里注入一个 `[OPEN-MEMEX]` 块,
|
|
177
|
+
包含 top-N 最新 project 记忆 + top-N 个人偏好。Agent 也可以随时调用
|
|
178
|
+
`memory_search` 按需检索。
|
|
179
|
+
|
|
180
|
+
## 存储布局
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
%APPDATA%\open-memex\ (Windows)
|
|
184
|
+
$XDG_DATA_HOME/open-memex/ (Linux/macOS)
|
|
185
|
+
├── index.db # SQLite FTS5 索引(可重建)
|
|
186
|
+
└── memories/
|
|
187
|
+
├── personal/
|
|
188
|
+
│ └── <id>.md
|
|
189
|
+
└── project__<name>__<hash12>/
|
|
190
|
+
└── <id>.md
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
每个 `.md` 文件是 v2 YAML frontmatter(`id, scope, scope_key, visibility, role,
|
|
194
|
+
type, importance, status, tags, created_at, updated_at, schema_version` 等)+
|
|
195
|
+
记忆正文。可以手工编辑——插件启动时按文件 mtime 重新同步。
|
|
196
|
+
Markdown 是 source of truth,SQLite 索引是派生的、可重建的
|
|
197
|
+
(`open-memex reindex`)。
|
|
198
|
+
|
|
199
|
+
## 配置(Config)
|
|
200
|
+
|
|
201
|
+
可选文件 `~/.config/opencode/open-memex.jsonc`(可用 `MY_O_MEMORY_CONFIG`
|
|
202
|
+
改路径;`MY_O_MEMORY_HOME` 改存储根目录)。
|
|
203
|
+
|
|
204
|
+
默认值:
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
{
|
|
208
|
+
"maxProjectMemories": 8, // 首轮注入的 project 记忆条数
|
|
209
|
+
"maxProfileItems": 5, // 首轮注入的个人偏好条数
|
|
210
|
+
"injectOnFirstTurn": true, // [OPEN-MEMEX] system-prompt 块
|
|
211
|
+
"keywordCaptureEnabled": true,
|
|
212
|
+
"logLevel": "info" // info | debug
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`open-memex config` 打印生效配置(默认值 + 文件)。
|
|
217
|
+
安装后改设置:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
open-memex config set keywordCaptureEnabled false
|
|
221
|
+
open-memex config set maxProjectMemories 12
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
可设置的 key:`maxProjectMemories`、`maxProfileItems`、`injectOnFirstTurn`、
|
|
225
|
+
`keywordCaptureEnabled`、`logLevel`。完整设计见
|
|
226
|
+
[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)。
|
|
227
|
+
|
|
228
|
+
## CLI 参考
|
|
229
|
+
|
|
230
|
+
安装与健康检查:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
open-memex init [--client vscode|cursor|opencode|visualstudio] [--force] [--yes]
|
|
234
|
+
open-memex config # 打印生效配置
|
|
235
|
+
open-memex config set <key> <value> # 改设置
|
|
236
|
+
open-memex doctor # 环境健康检查
|
|
237
|
+
open-memex capture --dry-run "记住我喜欢简洁的回答" # 预览关键词捕获
|
|
238
|
+
open-memex mcp --print-config vscode|cursor|claude|opencode|visualstudio
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
记忆操作:
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
open-memex add "This repo uses better-sqlite3" --type project-config
|
|
245
|
+
open-memex search "auth flow"
|
|
246
|
+
open-memex list --scope project
|
|
247
|
+
open-memex supersede <id> "Updated content"
|
|
248
|
+
open-memex status <id> deprecated
|
|
249
|
+
open-memex forget <id>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
维护:
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
open-memex where # 显示存储与配置文件路径
|
|
256
|
+
open-memex scopes # 列出 project scope 及记忆条数
|
|
257
|
+
open-memex reindex # 从 markdown 重建 SQLite 索引
|
|
258
|
+
open-memex migrate --to-v2 [--dry-run] # v1 数据 → v2
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
CLI 跑在 Node 22 内置的实验性 TypeScript loader 下(无需构建)。
|
|
262
|
+
从源码 checkout 使用时,每条命令前加
|
|
263
|
+
`node --experimental-strip-types src/cli.ts`(简单场景也可用
|
|
264
|
+
`npm run cli -- <命令>`——但 npm 会吞掉未知的 `--flag` 参数,
|
|
265
|
+
所以推荐直接用 `node`)。
|
|
266
|
+
|
|
267
|
+
## MCP server
|
|
268
|
+
|
|
269
|
+
同一个五个 memory tools,走 Model Context Protocol 的 stdio server——
|
|
270
|
+
不需要宿主专属插件,任何 MCP 客户端都能用 open-memex。
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
open-memex mcp # 全局安装后
|
|
274
|
+
npx -y open-memex@alpha mcp # 免安装
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
project scope 从进程工作目录解析,所以配置 server 时 cwd 要指向项目根目录
|
|
278
|
+
(`init` 会帮你处理好)。
|
|
279
|
+
|
|
280
|
+
> **注意:** MCP 是请求/响应式的——它给 agent 提供 tools,但没有 opencode
|
|
281
|
+
> 插件的关键词自动捕获和首轮上下文注入。想让 agent 主动用记忆,
|
|
282
|
+
> 靠的是 agent 的 instructions(`init` 写的 `.github/copilot-instructions.md`)。
|
|
283
|
+
|
|
284
|
+
## 路线图(Roadmap)
|
|
285
|
+
|
|
286
|
+
**`0.3.0-alpha`(本版):** 通用 MCP server、`open-memex` bin/CLI、
|
|
287
|
+
一键 `init` 配置、中文关键词捕获(含 personal/project 路由)、
|
|
288
|
+
`config` / `capture --dry-run` / `doctor` 助手命令、Visual Studio 支持。
|
|
289
|
+
|
|
290
|
+
**Coming —— `0.3.0-beta`:** 团队同步——用 git 做共享记忆
|
|
291
|
+
(`propose` / `promote` / `resolve` 工作流、仓库内记忆目录),
|
|
292
|
+
找 1–2 个同事做 pilot。
|
|
293
|
+
|
|
294
|
+
**Coming —— `0.3.0`(稳定版):** 组织层——组织记忆仓库、
|
|
295
|
+
curator 约定、distill-to-AGENTS.md 辅助。
|
|
296
|
+
|
|
297
|
+
**未来(看信号再定,不承诺版本):** 原生 agent 插件
|
|
298
|
+
(Claude Code / Codex hooks,作为同一套 MCP tools 的增强路径);
|
|
299
|
+
本地 embedding 做基准测试门控的实验(**未经明确 opt-in 绝不下载
|
|
300
|
+
embedding 模型**);云端 `RemoteProvider` 定制只在多仓库共享、
|
|
301
|
+
ACL 或合规需求出现时才做。
|
|
302
|
+
|
|
303
|
+
设计细节:[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)(append-only 决策日志 D1–D20)。
|
|
304
|
+
|
|
305
|
+
## 许可证
|
|
306
|
+
|
|
307
|
+
[Apache-2.0](./LICENSE)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `open-memex` bin launcher: re-execs src/cli.ts with TypeScript type-stripping
|
|
3
|
+
// enabled, so the command works on Node 22.6+ without the user passing
|
|
4
|
+
// --experimental-strip-types themselves. (Plain JS — no build step.)
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const entry = path.join(
|
|
10
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
11
|
+
"..",
|
|
12
|
+
"src",
|
|
13
|
+
"cli.ts",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const child = spawn(
|
|
17
|
+
process.execPath,
|
|
18
|
+
["--experimental-strip-types", entry, ...process.argv.slice(2)],
|
|
19
|
+
{ stdio: "inherit" },
|
|
20
|
+
);
|
|
21
|
+
child.on("error", (err) => {
|
|
22
|
+
console.error(`open-memex: failed to start: ${err.message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
child.on("exit", (code, signal) => {
|
|
26
|
+
if (signal) process.kill(process.pid, signal);
|
|
27
|
+
else process.exit(code ?? 0);
|
|
28
|
+
});
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -40,6 +40,11 @@ Decisions log; Prior Art; solo-dev adoption path.
|
|
|
40
40
|
- **MCP is an interface, not the identity.** MCP / CLI / REST / SDK are access layers over the protocol,
|
|
41
41
|
so the project is never locked to one transport or one agent tool (opencode, VS Code Copilot, Cursor,
|
|
42
42
|
Claude Code, Windsurf, …).
|
|
43
|
+
- **Company lens:** at organizational scale the same pain is tribal knowledge — senior engineers'
|
|
44
|
+
hard-won experience evaporates when they move on, and every incident gets re-debugged by someone
|
|
45
|
+
new. The current phase therefore prioritizes *capture*: valuable knowledge must land in memory
|
|
46
|
+
first, because team/org sharing, onboarding, and incident learning all build on that foundation.
|
|
47
|
+
(No capture, nothing to inherit.)
|
|
43
48
|
|
|
44
49
|
### Non-goals
|
|
45
50
|
|
|
@@ -287,7 +292,7 @@ instructions are never candidates.)
|
|
|
287
292
|
| Explicit tools | `memory_add/update/forget` (soft delete) · `search/get/list/status` · `propose/promote` · `resolve`. Write tools confirm with the user; read tools are open. |
|
|
288
293
|
| Keyword triggers | `remember …`, `note that …`, `TIL …`, `save this: …` + Chinese `记住` `记得` `保存一下` … |
|
|
289
294
|
| 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). |
|
|
290
|
-
| Redaction (hard) | `<private>…</private>` stripped; secret patterns
|
|
295
|
+
| 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. |
|
|
291
296
|
|
|
292
297
|
---
|
|
293
298
|
|
|
@@ -410,11 +415,47 @@ Zero-config is survival for an open-source project. The opencode plugin remains
|
|
|
410
415
|
VS Code MCP-server support; corporate Copilot local-tool support. **Hard gate before Phase 2.**
|
|
411
416
|
- **Phase 1 — Local hardening (1–2 wks).** CJK default (bigram+FTS5) · v1→v2 migration · dedup +
|
|
412
417
|
lifecycle · redaction hardening · scope docs. No external dependencies.
|
|
413
|
-
- **Phase 2A —
|
|
414
|
-
|
|
418
|
+
- **Phase 2A — MCP server (shipped 2026-09-27, D15).** Core/adapters split
|
|
419
|
+
(`src/tools/ops.ts`) · MCP server (`src/mcp.ts`, stdio) exposing all five memory tools —
|
|
420
|
+
read-only-first phasing dropped per D15 · query-aware injection stays host-side.
|
|
421
|
+
Ships in **`0.3.0-alpha`** (with bin/npx user-friendliness polish per §17 adoption path).
|
|
415
422
|
- **Phase 2B — Team sync.** GitProvider · `propose/promote/resolve` · in-repo dir · 1–2 colleague pilot
|
|
416
423
|
(pilot project selection is maintainer-private, not tracked in this doc).
|
|
424
|
+
Ships in **`0.3.0-beta`**.
|
|
417
425
|
Embeddings/rerank run as a **parallel benchmark-gated experiment**, not on the critical path.
|
|
426
|
+
- **Phase 2C — Native agent plugins (candidates, not committed).** Claude Code plugin and/or
|
|
427
|
+
Codex plugin as hook-enhanced paths over the same MCP tool surface (`SessionStart` →
|
|
428
|
+
context injection, `UserPromptSubmit` → keyword-triggered search, `Stop`/`PostToolUse` →
|
|
429
|
+
capture); per D16, no host-specific extraction intelligence — opencode is likewise
|
|
430
|
+
supported as a plain MCP consumer. Gated on real-world signal from 0.3.0-alpha MCP
|
|
431
|
+
dogfooding.
|
|
432
|
+
|
|
433
|
+
### Agent integration matrix
|
|
434
|
+
|
|
435
|
+
| Agent | Integration path | Native hooks? | Status |
|
|
436
|
+
|---|---|---|---|
|
|
437
|
+
| opencode | native plugin (`src/index.ts`) | ✅ keyword capture + first-turn injection | shipped (Phase 1) |
|
|
438
|
+
| VS Code Copilot | MCP server + `.github/copilot-instructions.md` | ❌ — VS Code extension API cannot intercept Copilot Chat (researched 2026-09-27); an extension would add no hook capability, so not worth building | ships `0.3.0-alpha` |
|
|
439
|
+
| Cursor | MCP server + rules | ❌ no chat plugin API | ships `0.3.0-alpha` |
|
|
440
|
+
| Claude Code | MCP server today; plugin + hooks candidate | ✅ `SessionStart` / `UserPromptSubmit` / `PostToolUse` | Phase 2C candidate |
|
|
441
|
+
| Codex (CLI/IDE) | MCP server (`[mcp_servers]` in config.toml / `codex mcp add`) today; plugin + hooks + marketplace candidate | ✅ hooks mirror Claude Code's | Phase 2C candidate |
|
|
442
|
+
|
|
443
|
+
### Competitive landscape (for future positioning)
|
|
444
|
+
|
|
445
|
+
Coding-agent memory is crowded; open-memex's wedge is **zero-cloud, zero-account,
|
|
446
|
+
zero-embedding-download**, with repo-native markdown as source of truth (maintainer
|
|
447
|
+
requirement: personal data never touches third-party services). Benchmarks to track:
|
|
448
|
+
|
|
449
|
+
| Product | Scale / backing (Sep 2026) | Shape | Gap vs open-memex |
|
|
450
|
+
|---|---|---|---|
|
|
451
|
+
| Mem0 | ~50k+★, $24M Series A (YC) | universal memory SDK/API, vector+graph, cloud-first | cloud dependency; not repo-native for coding agents |
|
|
452
|
+
| Letta (ex-MemGPT) | ~24k★, $10M seed | stateful agent platform, memory blocks | agent runtime, not a drop-in coding-agent memory |
|
|
453
|
+
| Zep / Graphiti | ~20–30k★, $12M seed | temporal knowledge graph, enterprise | heavy infra; overkill as a coding vault |
|
|
454
|
+
| Cognee | ~15–30k★, $7.5M seed | graph ECL pipelines | ingest-oriented, no coding-agent hooks |
|
|
455
|
+
| Supermemory | ~15k★, $2.6M seed | consumer second-brain + SaaS API | cloud SaaS |
|
|
456
|
+
| 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
|
+
|
|
458
|
+
(Star counts / funding as of Sep 2026 — re-verify before quoting publicly.)
|
|
418
459
|
- **Phase 3 — Org layer.** Org memory repo · curator convention · `examples/remote-server/` ·
|
|
419
460
|
distill-to-AGENTS.md assist.
|
|
420
461
|
- **Phase 4 — Future, signal-gated.** Cloud `RemoteProvider` customization only on: multi-private-repo
|
|
@@ -474,6 +515,69 @@ Zero-config is survival for an open-source project. The opencode plugin remains
|
|
|
474
515
|
`autoPull: true`, a failed pull never blocks the session and every pull emits a receipt.
|
|
475
516
|
*Rationale: a memory pull can change agent behavior, so it must be a deliberate, visible act —
|
|
476
517
|
predictable offline-first beats silent freshness. Unanimous 5/5 in round-4 AI review, 2026-09-26.*
|
|
518
|
+
- **D14** — Secret detection masks instead of refusing. A write-path secret hit is **masked in
|
|
519
|
+
place** (first 4 characters kept, the rest replaced with `x`, length-preserving) and the write
|
|
520
|
+
proceeds with a notice; `<private>…</private>` spans are still stripped to `[REDACTED]`.
|
|
521
|
+
Amends the v0.2 rule "secret patterns refuse the write" (§8, §11). *Rationale: a refused write
|
|
522
|
+
loses the surrounding context the user asked to remember; a prefix-masked secret stays
|
|
523
|
+
recognizable (which key it was) while the credential itself is not recoverable from the file.
|
|
524
|
+
Supersedes the refusal behavior; applies to every write path (tools, keyword capture, CLI).
|
|
525
|
+
2026-09-27.*
|
|
526
|
+
- **D15** — Phase 2A MCP server ships with all five tools, not read-only first. The MCP server
|
|
527
|
+
(`src/mcp.ts`, stdio) exposes `memory_add` / `memory_search` / `memory_list` /
|
|
528
|
+
`memory_supersede` / `memory_forget` — amends the §18 roadmap's "Read-only MCP" phasing.
|
|
529
|
+
*Rationale: the write path is the same Core (redact/D14, dedup, lifecycle) already shipped and
|
|
530
|
+
dogfooded in the opencode plugin, so a separate read-only stage adds process cost without
|
|
531
|
+
reducing risk. Core/adapters split implemented as `src/tools/ops.ts` (host-agnostic logic +
|
|
532
|
+
shared zod schemas); the opencode plugin and the MCP server are thin adapters over it.
|
|
533
|
+
Query-aware injection stays host-side: MCP is request/response and offers no hooks, so
|
|
534
|
+
proactive memory use depends on the host's agent instructions. 2026-09-27.*
|
|
535
|
+
- **D16** — No separate LLM extraction pass; memory intelligence lives in model-driven tool
|
|
536
|
+
calls. A dedicated post-session extraction (opencode `session.idle` hook → hidden session
|
|
537
|
+
→ host model) was evaluated and rejected: the model's own decision to call `memory_add`
|
|
538
|
+
*is* the LLM judgment of "worth remembering", so a second pass is redundant and
|
|
539
|
+
host-specific. Investment goes into the shared layer instead — `TOOL_DESCRIPTIONS` in
|
|
540
|
+
`src/tools/ops.ts` and each host's agent instructions — so every host benefits at once.
|
|
541
|
+
Native plugins (opencode now; Claude Code / Codex as Phase 2C candidates) remain as
|
|
542
|
+
hook-enhanced paths, but opencode is also supported as a plain MCP consumer of
|
|
543
|
+
`open-memex mcp`, keeping one unified tool surface. 2026-09-27.
|
|
544
|
+
- **D17** — `open-memex init` resolves the MCP server command at init time. A durable
|
|
545
|
+
`open-memex` on PATH (outside npm's ephemeral `_npx` cache) → `command: "open-memex"`;
|
|
546
|
+
otherwise (one-shot `npx open-memex@alpha init`) → `command: "npx", args: ["-y",
|
|
547
|
+
"open-memex@alpha", "mcp"]` plus a hint to `npm i -g` + re-run `init --force`.
|
|
548
|
+
`mcp --print-config` uses the same resolution. *Rationale: a one-shot npx run leaves
|
|
549
|
+
no bin behind, so writing `command: "open-memex"` would produce a dead MCP server on
|
|
550
|
+
the next editor launch; the npx fallback keeps the one-command setup actually
|
|
551
|
+
one-command. 2026-09-27.*
|
|
552
|
+
|
|
553
|
+
- **D18** — Keyword scope routing: 我 → personal, 我们 → project. Chinese capture
|
|
554
|
+
keywords are split into two pattern lists: personal patterns (`记住我`/`替我记`/
|
|
555
|
+
`我觉得`/`我喜欢`, plus legacy `remember for me`/`记住(个人)`) route to the personal
|
|
556
|
+
scope, while project patterns (`我们认为`/`我们决定`/`帮我们记住`, and the generic
|
|
557
|
+
`记住…` for `记住我们的…`) route to the current project scope. Personal patterns are
|
|
558
|
+
scanned first and *claim* the line so the generic `记住…` pattern cannot double-fire;
|
|
559
|
+
`记住我` uses a `(?!们)` guard so it never swallows `记住我们…`. *Rationale: the
|
|
560
|
+
user's own rule — "我" is personal, "我们" is the current project — stated 2026-09-27;
|
|
561
|
+
scanning user messages (never assistant output) with personal-first claim keeps one
|
|
562
|
+
utterance to one memory. README + repo AGENTS.md keyword sections updated in the same
|
|
563
|
+
commit. 2026-09-27.*
|
|
564
|
+
- **D19** — `open-memex init` asks setup questions; `open-memex config set` edits settings
|
|
565
|
+
after install. `init` prompts on a TTY (editor: vscode/cursor/opencode; keyword
|
|
566
|
+
auto-capture on/off; first-turn injection on/off), `--yes` accepts all defaults, and
|
|
567
|
+
non-terminal runs never prompt (scripts keep the historical vscode default).
|
|
568
|
+
Non-default answers persist to the JSONC config file; `open-memex config set <key>
|
|
569
|
+
<value>` changes them later (validated keys: `maxProjectMemories`, `maxProfileItems`,
|
|
570
|
+
`injectOnFirstTurn`, `keywordCaptureEnabled`, `logLevel`). `init --client opencode`
|
|
571
|
+
merges a `type: "local"` MCP entry into project-level `opencode.jsonc` (v1 format).
|
|
572
|
+
*Rationale: install time is the only moment the user's attention is guaranteed, and a
|
|
573
|
+
print-only `config` left no path to change settings afterwards. 2026-09-27.*
|
|
574
|
+
- **D20** — `init` / `mcp --print-config` support Visual Studio. Writes solution-level
|
|
575
|
+
`.mcp.json` with the `"servers"` section (`{ "type": "stdio", "command", "args" }`),
|
|
576
|
+
per Microsoft Learn (VS 2022 17.14+ / VS 2026, Windows-only). `.github/copilot-
|
|
577
|
+
instructions.md` is still written — VS's Copilot reads it too. Note VS also
|
|
578
|
+
auto-discovers `.vscode/mcp.json` and `.cursor/mcp.json`, so repos already set up for
|
|
579
|
+
VS Code get VS support for free; the explicit `.mcp.json` is the source-controllable
|
|
580
|
+
option. 2026-09-27.*
|
|
477
581
|
|
|
478
582
|
## Open Questions
|
|
479
583
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-memex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha",
|
|
4
4
|
"description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
"main": "src/index.ts",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
|
-
"cli": "node --experimental-strip-types src/cli.ts"
|
|
18
|
+
"cli": "node --experimental-strip-types src/cli.ts",
|
|
19
|
+
"mcp": "node --experimental-strip-types src/mcp.ts"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
21
23
|
"better-sqlite3": "^11.7.0",
|
|
22
|
-
"js-yaml": "^4.1.0"
|
|
24
|
+
"js-yaml": "^4.1.0",
|
|
25
|
+
"zod": "^4.1.8"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"@opencode-ai/plugin": "^1.18.30",
|
|
@@ -27,5 +30,11 @@
|
|
|
27
30
|
"@types/js-yaml": "^4.0.9",
|
|
28
31
|
"@types/node": "^22.0.0",
|
|
29
32
|
"typescript": "^5.6.0"
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"open-memex": "bin/open-memex.js"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.6"
|
|
30
39
|
}
|
|
31
40
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// MCP server smoke test: handshake + tools/list + add/search/forget over stdio.
|
|
2
|
+
// Usage: node --experimental-strip-types scripts/smoke-mcp.ts
|
|
3
|
+
// Uses temp dirs for MY_O_MEMORY_HOME and the project cwd — no real data touched.
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-home-"));
|
|
11
|
+
const PROJ = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-proj-"));
|
|
12
|
+
|
|
13
|
+
const MCP_TS = fileURLToPath(new URL("../src/mcp.ts", import.meta.url));
|
|
14
|
+
|
|
15
|
+
const child = spawn(
|
|
16
|
+
"node",
|
|
17
|
+
["--experimental-strip-types", MCP_TS],
|
|
18
|
+
{
|
|
19
|
+
cwd: PROJ,
|
|
20
|
+
env: { ...process.env, MY_O_MEMORY_HOME: HOME },
|
|
21
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
let buf = "";
|
|
26
|
+
let id = 0;
|
|
27
|
+
const pending = new Map<number, (v: any) => void>();
|
|
28
|
+
const stdoutLines: string[] = [];
|
|
29
|
+
|
|
30
|
+
child.stdout.on("data", (d) => {
|
|
31
|
+
buf += d.toString();
|
|
32
|
+
let idx;
|
|
33
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
34
|
+
const line = buf.slice(0, idx).trim();
|
|
35
|
+
buf = buf.slice(idx + 1);
|
|
36
|
+
if (!line) continue;
|
|
37
|
+
stdoutLines.push(line);
|
|
38
|
+
let msg;
|
|
39
|
+
try {
|
|
40
|
+
msg = JSON.parse(line);
|
|
41
|
+
} catch {
|
|
42
|
+
console.error("NON-JSON on stdout:", line);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
47
|
+
pending.get(msg.id)!(msg);
|
|
48
|
+
pending.delete(msg.id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
child.stderr.on("data", (d) => process.stderr.write("[srv:err] " + d.toString()));
|
|
53
|
+
|
|
54
|
+
function req(method: string, params?: any): Promise<any> {
|
|
55
|
+
const myId = ++id;
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
pending.set(myId, resolve);
|
|
58
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: myId, method, params }) + "\n");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function notify(method: string, params?: any) {
|
|
62
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results: string[] = [];
|
|
66
|
+
function check(name: string, ok: boolean, detail = "") {
|
|
67
|
+
results.push(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
|
|
68
|
+
if (!ok) process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const init = await req("initialize", {
|
|
73
|
+
protocolVersion: "2024-11-05",
|
|
74
|
+
capabilities: {},
|
|
75
|
+
clientInfo: { name: "mcp-e2e", version: "0.0.1" },
|
|
76
|
+
});
|
|
77
|
+
check("initialize", !!init.result?.serverInfo, JSON.stringify(init.result?.serverInfo));
|
|
78
|
+
notify("notifications/initialized");
|
|
79
|
+
|
|
80
|
+
const tools = await req("tools/list", {});
|
|
81
|
+
const names = (tools.result?.tools ?? []).map((t: any) => t.name).sort();
|
|
82
|
+
check(
|
|
83
|
+
"tools/list has 5 memory tools",
|
|
84
|
+
JSON.stringify(names) ===
|
|
85
|
+
JSON.stringify(["memory_add", "memory_forget", "memory_list", "memory_search", "memory_supersede"]),
|
|
86
|
+
names.join(","),
|
|
87
|
+
);
|
|
88
|
+
const addSchema = tools.result.tools.find((t: any) => t.name === "memory_add").inputSchema;
|
|
89
|
+
check("memory_add schema has content+type+scope+tags", !!addSchema.properties?.content && !!addSchema.properties?.type, Object.keys(addSchema.properties ?? {}).join(","));
|
|
90
|
+
|
|
91
|
+
// D14 through MCP: a fake secret must be masked, not refused.
|
|
92
|
+
const add = await req("tools/call", {
|
|
93
|
+
name: "memory_add",
|
|
94
|
+
arguments: { content: "mcp e2e probe: deploy key sk-test-FAKESECRET1234567890abcdef", type: "fact" },
|
|
95
|
+
});
|
|
96
|
+
const addText: string = add.result?.content?.[0]?.text ?? "";
|
|
97
|
+
const savedId = (addText.match(/id=([A-Za-z0-9_]+)/) ?? [])[1];
|
|
98
|
+
check("memory_add saves (not refuses)", !add.result?.isError && !!savedId, addText.slice(0, 80));
|
|
99
|
+
check("memory_add notes masking (D14)", addText.includes("masked"), addText.slice(0, 120));
|
|
100
|
+
|
|
101
|
+
// The raw secret must not be on disk.
|
|
102
|
+
const mdFiles: string[] = [];
|
|
103
|
+
const walk = (d: string) => {
|
|
104
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
105
|
+
const p = path.join(d, e.name);
|
|
106
|
+
if (e.isDirectory()) walk(p);
|
|
107
|
+
else if (e.name.endsWith(".md")) mdFiles.push(p);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
walk(HOME);
|
|
111
|
+
const leaked = mdFiles.filter((f) => fs.readFileSync(f, "utf8").includes("sk-test-FAKESECRET1234567890abcdef"));
|
|
112
|
+
check("secret not leaked to disk", leaked.length === 0, leaked.join(","));
|
|
113
|
+
|
|
114
|
+
const search = await req("tools/call", {
|
|
115
|
+
name: "memory_search",
|
|
116
|
+
arguments: { query: "mcp e2e probe" },
|
|
117
|
+
});
|
|
118
|
+
const searchText: string = search.result?.content?.[0]?.text ?? "";
|
|
119
|
+
check("memory_search finds it", searchText.includes(savedId), searchText.slice(0, 80));
|
|
120
|
+
check("memory_search output masked", !searchText.includes("sk-test-FAKESECRET1234567890abcdef"));
|
|
121
|
+
|
|
122
|
+
const list = await req("tools/call", { name: "memory_list", arguments: {} });
|
|
123
|
+
check("memory_list works", (list.result?.content?.[0]?.text ?? "").includes(savedId));
|
|
124
|
+
|
|
125
|
+
const forget = await req("tools/call", { name: "memory_forget", arguments: { id: savedId } });
|
|
126
|
+
check("memory_forget works", (forget.result?.content?.[0]?.text ?? "").includes("Deleted"));
|
|
127
|
+
|
|
128
|
+
const search2 = await req("tools/call", { name: "memory_search", arguments: { query: "mcp e2e probe" } });
|
|
129
|
+
check("memory gone after forget", !(search2.result?.content?.[0]?.text ?? "").includes(savedId));
|
|
130
|
+
} finally {
|
|
131
|
+
child.kill();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(results.join("\n"));
|
|
135
|
+
console.log(process.exitCode ? "MCP E2E: FAILURES" : "MCP E2E: ALL PASS");
|