open-memex 0.4.0-alpha.10 → 0.4.0-alpha.11
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/README.md +155 -20
- package/README.zh-CN.md +133 -16
- package/dist/cli.js +93 -20
- package/docs/TEST-PLAN.md +72 -0
- package/package.json +1 -1
- package/scripts/test-full.ts +345 -0
- package/src/cli.ts +93 -20
package/README.md
CHANGED
|
@@ -1,15 +1,77 @@
|
|
|
1
1
|
# open-memex
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/open-memex)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
3
6
|
[中文文档](./README.zh-CN.md)
|
|
4
7
|
|
|
5
8
|
Local-first persistent memory for AI coding agents: an [opencode](https://opencode.ai) plugin
|
|
6
9
|
plus a generic MCP server (VS Code Copilot, Cursor, Claude Code, Visual Studio, …).
|
|
7
10
|
|
|
11
|
+
## Why open-memex?
|
|
12
|
+
|
|
13
|
+
Engineering knowledge lives in two places: the code, and people's heads.
|
|
14
|
+
Every new AI coding session starts from zero — the same project context gets
|
|
15
|
+
explained again, the same gotchas get rediscovered, the same incident lessons
|
|
16
|
+
fade when the chat ends.
|
|
17
|
+
|
|
18
|
+
open-memex captures the part worth remembering — the decision, the constraint,
|
|
19
|
+
the lesson — as reviewable Markdown, and injects it back into the next session
|
|
20
|
+
automatically.
|
|
21
|
+
|
|
22
|
+
> No capture, nothing to inherit.
|
|
23
|
+
|
|
24
|
+
It also complements agentic development workflows (spec-driven development,
|
|
25
|
+
plan/implement/verify loops): plans produce decisions, verification produces
|
|
26
|
+
rules — open-memex is the memory layer that keeps them across sessions instead
|
|
27
|
+
of re-deriving them on every run.
|
|
28
|
+
|
|
29
|
+
## How it compares
|
|
30
|
+
|
|
31
|
+
| | open-memex | Cloud memory services | Wiki / docs portal | Chat history |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| Data location | Your machine + your repos | Vendor servers | Central server | Gone when the chat ends |
|
|
34
|
+
| Review before sharing | Yes — outbox + PR | Varies | Yes | No |
|
|
35
|
+
| Agent recall | Session-start injection + search | API calls | Manual lookup | No |
|
|
36
|
+
| Human-readable | Plain Markdown files | Dashboard / API | Yes | No |
|
|
37
|
+
|
|
8
38
|
- **Markdown files** as the source of truth (human-editable, git-friendly)
|
|
9
39
|
- **SQLite FTS5** as a rebuildable index (BM25 keyword search, via `better-sqlite3`)
|
|
10
40
|
- **Zero cloud**, zero account, zero third-party API
|
|
11
41
|
- Loads directly under opencode's embedded Bun runtime; CLI and MCP server run under Node — no build step in development (the published npm package ships pre-compiled JS), no Bun install
|
|
12
42
|
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
┌──────────────────┐
|
|
47
|
+
│ AI agent │
|
|
48
|
+
│ Copilot / Cursor │
|
|
49
|
+
│ Claude / opencode│
|
|
50
|
+
└────────┬─────────┘
|
|
51
|
+
│ MCP (stdio) — 11 tools
|
|
52
|
+
│ session-start injection
|
|
53
|
+
┌────────▼─────────┐
|
|
54
|
+
│ open-memex │
|
|
55
|
+
│ MCP server │
|
|
56
|
+
└──┬────────────┬──┘
|
|
57
|
+
│ │
|
|
58
|
+
┌────────────▼───┐ ┌─────▼──────────┐
|
|
59
|
+
│ Markdown files │ │ SQLite FTS5 │
|
|
60
|
+
│ source of truth│ │ rebuildable │
|
|
61
|
+
│ local-first │ │ index (BM25) │
|
|
62
|
+
└─────────────┬──┘ └────────────────┘
|
|
63
|
+
│ submit (explicit,
|
|
64
|
+
│ local commit)
|
|
65
|
+
┌───────▼────────┐
|
|
66
|
+
│ Git repo │
|
|
67
|
+
│ .ai/open-memex/│
|
|
68
|
+
│ PR-reviewed │
|
|
69
|
+
│ team memory │
|
|
70
|
+
└────────────────┘
|
|
71
|
+
|
|
72
|
+
personal scope: this machine only — never synced, never enters a repo.
|
|
73
|
+
```
|
|
74
|
+
|
|
13
75
|
## Installation
|
|
14
76
|
|
|
15
77
|
### Requirements
|
|
@@ -18,7 +80,9 @@ plus a generic MCP server (VS Code Copilot, Cursor, Claude Code, Visual Studio,
|
|
|
18
80
|
|
|
19
81
|
### Step 1 — Install the CLI
|
|
20
82
|
|
|
21
|
-
|
|
83
|
+
#### Stable vs alpha
|
|
84
|
+
|
|
85
|
+
**Stable** (recommended for most users) — the `latest` tag:
|
|
22
86
|
|
|
23
87
|
```sh
|
|
24
88
|
npm install -g open-memex
|
|
@@ -26,10 +90,26 @@ npm install -g open-memex
|
|
|
26
90
|
|
|
27
91
|
This installs the `0.3.0` stable release.
|
|
28
92
|
|
|
93
|
+
**Alpha** (bleeding edge, for testers) — the `alpha` tag:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npm install -g open-memex@alpha
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See what's published:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
npm view open-memex version # latest stable
|
|
103
|
+
npm view open-memex@alpha version # latest alpha
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Alpha builds may have rough edges — bug reports are welcome.
|
|
107
|
+
|
|
29
108
|
**No install — run via npx:**
|
|
30
109
|
|
|
31
110
|
```sh
|
|
32
|
-
npx -y open-memex <command>
|
|
111
|
+
npx -y open-memex <command> # e.g. npx -y open-memex init --client vscode
|
|
112
|
+
npx -y open-memex@alpha <command> # alpha line, no install
|
|
33
113
|
```
|
|
34
114
|
|
|
35
115
|
**From source** (bleeding edge, `V2-dev-p2` branch):
|
|
@@ -185,6 +265,62 @@ storage writability, then boots a real MCP server and runs `initialize` +
|
|
|
185
265
|
4 characters kept, the rest replaced with `x` — and the memory is saved.
|
|
186
266
|
Preview what a message would capture with `open-memex capture --dry-run "…"`.
|
|
187
267
|
|
|
268
|
+
## Memory types
|
|
269
|
+
|
|
270
|
+
Eleven types — `type` says what the memory is, `tags` say what it's about:
|
|
271
|
+
|
|
272
|
+
| Type | Captures |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `fact` | A stable true statement about the project or world |
|
|
275
|
+
| `preference` | How someone likes things done |
|
|
276
|
+
| `decision` | A choice that was made — the why and the trade-off |
|
|
277
|
+
| `constraint` | A rule that must not be violated |
|
|
278
|
+
| `todo` | A commitment to do something later |
|
|
279
|
+
| `knowledge` | Durable domain or architecture knowledge |
|
|
280
|
+
| `howto` | A procedure that worked |
|
|
281
|
+
| `gotcha` | A trap to avoid |
|
|
282
|
+
| `lesson` | What an incident or mistake taught us |
|
|
283
|
+
| `observation` | Something noticed, not yet a conclusion |
|
|
284
|
+
| `reference` | A pointer to the authoritative doc (no copying) |
|
|
285
|
+
|
|
286
|
+
## Team memory workflow
|
|
287
|
+
|
|
288
|
+
Personal notes stay private. Project knowledge follows an explicit, reviewable
|
|
289
|
+
pipeline — nothing is shared automatically:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
capture → outbox (draft, local) → submit → repo (.ai/open-memex/) → PR review → published → recall
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
1. **Capture** — save decisions, gotchas, lessons as drafts during normal work.
|
|
296
|
+
2. **Review** — drafts wait in a local outbox; `sync-status` (or saying
|
|
297
|
+
"sync memory" in chat) shows what's pending.
|
|
298
|
+
3. **Submit** — you name the memories; they move into `<repo>/.ai/open-memex/`
|
|
299
|
+
with a local commit. open-memex never auto-pushes.
|
|
300
|
+
4. **PR review** — memories are plain Markdown; reviewers approve, request
|
|
301
|
+
changes, or reject through the normal branch/PR process (`promote`,
|
|
302
|
+
`pr-status`, `resolve`).
|
|
303
|
+
5. **Recall** — published memories are injected at session start and searchable
|
|
304
|
+
on demand, for humans and agents alike.
|
|
305
|
+
|
|
306
|
+
Reviewer convention: [docs/CURATOR.md](./docs/CURATOR.md).
|
|
307
|
+
|
|
308
|
+
## Security & data
|
|
309
|
+
|
|
310
|
+
- **Local-first:** everything lives on your machine (`%APPDATA%\open-memex` /
|
|
311
|
+
`~/.local/share/open-memex`) plus the repos you choose. Zero cloud calls,
|
|
312
|
+
zero accounts, zero third-party APIs, zero telemetry.
|
|
313
|
+
- **Secrets stay out:** `<private>…</private>` spans are stripped; detected API
|
|
314
|
+
keys/tokens are masked in place before saving. Preview with
|
|
315
|
+
`open-memex capture --dry-run "…"`.
|
|
316
|
+
- **Personal never syncs:** the `personal` scope is this machine only — excluded
|
|
317
|
+
from export by default and can never enter a repo.
|
|
318
|
+
- **Auditable sharing:** team memories move only by explicit `submit`, travel
|
|
319
|
+
through branch/PR review, and every `promote` transition is appended to the
|
|
320
|
+
memory's `review_history` (who / when / why).
|
|
321
|
+
- **You own the files:** Markdown is the source of truth — inspect, edit, or
|
|
322
|
+
delete anything by hand; the SQLite index rebuilds from the files.
|
|
323
|
+
|
|
188
324
|
## Scopes
|
|
189
325
|
|
|
190
326
|
- **project** — scoped to the current repo (keyed off the git origin URL hash, or the cwd if no remote). Default for new memories.
|
|
@@ -401,28 +537,27 @@ server with cwd set to your project root (`init` handles this for you).
|
|
|
401
537
|
|
|
402
538
|
## Roadmap
|
|
403
539
|
|
|
404
|
-
**`0.3.0` (
|
|
540
|
+
**`0.3.0` (stable):** generic MCP server, `open-memex` bin/CLI, one-command
|
|
405
541
|
`init` setup, Chinese keyword capture with personal/project routing, `config` /
|
|
406
542
|
`capture --dry-run` / `doctor` helpers, Visual Studio support.
|
|
407
543
|
|
|
408
|
-
|
|
544
|
+
**`0.4.0` (in development):** team sync — shared memory via git: appdata draft
|
|
409
545
|
outbox → `sync-status` → `submit` (local branch+commit, push/PR on your Yes)
|
|
410
|
-
→ `promote` / `resolve` review workflow, in-repo `.ai/open-memex/` dir
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
Design details: [docs/V2-DESIGN.md](./docs/V2-DESIGN.md) (append-only decision log D1–D20).
|
|
546
|
+
→ `promote` / `resolve` review workflow, in-repo `.ai/open-memex/` dir;
|
|
547
|
+
`export` / `import` archive for user portability (Markdown + manifest, no walled
|
|
548
|
+
garden; private excluded by default, `-a` / `--all` for full migration);
|
|
549
|
+
distill-to-AGENTS.md assist (`distill-agents`, propose-only — you merge by hand);
|
|
550
|
+
§3.5 checkpoint distillation in the MCP handshake + init instructions (the agent
|
|
551
|
+
proposes 1–3 captures at checkpoints, the human decides); 1–2 colleague pilot.
|
|
552
|
+
|
|
553
|
+
**Future (signal-gated, no version committed):** org layer — org memory repo,
|
|
554
|
+
curator convention; native agent plugins (Claude Code / Codex hooks as
|
|
555
|
+
enhancement paths over the same MCP tools); local embeddings as a
|
|
556
|
+
benchmark-gated experiment (no embedding model is ever downloaded without
|
|
557
|
+
explicit opt-in); cloud `RemoteProvider` customization only if multi-repo
|
|
558
|
+
sharing, ACL, or compliance needs demand it.
|
|
559
|
+
|
|
560
|
+
Design details: [docs/V2-DESIGN.md](./docs/V2-DESIGN.md) (append-only decision log).
|
|
426
561
|
|
|
427
562
|
## License
|
|
428
563
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,15 +1,74 @@
|
|
|
1
1
|
# open-memex
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/open-memex)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
3
6
|
[English](./README.md)
|
|
4
7
|
|
|
5
8
|
给 AI 编程助手的本地优先持久记忆:一个 [opencode](https://opencode.ai) 插件,
|
|
6
9
|
加上一个通用 MCP server(VS Code Copilot、Cursor、Claude Code、Visual Studio 等)。
|
|
7
10
|
|
|
11
|
+
## 为什么需要 open-memex?
|
|
12
|
+
|
|
13
|
+
工程知识只存在于两个地方:代码里,和人的脑子里。
|
|
14
|
+
每个新的 AI 编程会话都从零开始——同样的项目背景要反复讲,同样的坑要反复踩,
|
|
15
|
+
同样的事故教训在聊天结束时就消失了。
|
|
16
|
+
|
|
17
|
+
open-memex 把值得记住的部分——决策、约束、教训——存成可 review 的 Markdown,
|
|
18
|
+
并在下一次会话开始时自动注入回去。
|
|
19
|
+
|
|
20
|
+
> No capture, nothing to inherit.(不记录,就无从传承。)
|
|
21
|
+
|
|
22
|
+
它也补齐了 agentic 开发工作流(spec 驱动开发、plan/implement/verify 循环)缺的那一块:
|
|
23
|
+
plan 产出决策,verify 产出规则——open-memex 是让它们跨会话留存的记忆层,
|
|
24
|
+
而不是每次从头重新推导。
|
|
25
|
+
|
|
26
|
+
## 和其它方案的对比
|
|
27
|
+
|
|
28
|
+
| | open-memex | 云端记忆服务 | Wiki / 文档平台 | 聊天记录 |
|
|
29
|
+
|---|---|---|---|---|
|
|
30
|
+
| 数据在哪 | 你的机器 + 你的仓库 | 服务商服务器 | 中心服务器 | 聊天结束就没了 |
|
|
31
|
+
| 分享前 review | 有——outbox + PR | 不一定 | 有 | 没有 |
|
|
32
|
+
| Agent 回忆 | 会话开始注入 + 搜索 | 调 API | 人工去查 | 没有 |
|
|
33
|
+
| 人类可读 | 纯 Markdown 文件 | 后台 / API | 有 | 没有 |
|
|
34
|
+
|
|
8
35
|
- **Markdown 文件**是 source of truth(人类可读、git 友好)
|
|
9
36
|
- **SQLite FTS5** 做可重建索引(BM25 关键词检索,`better-sqlite3`)
|
|
10
37
|
- **零云端**、零账号、零第三方 API
|
|
11
38
|
- 直接跑在 opencode 内嵌的 Bun 运行时里;CLI 和 MCP server 跑在 Node 下——开发时无需构建(发布的 npm 包带预编译好的 JS)、无需安装 Bun
|
|
12
39
|
|
|
40
|
+
## 架构
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
┌──────────────────┐
|
|
44
|
+
│ AI agent │
|
|
45
|
+
│ Copilot / Cursor │
|
|
46
|
+
│ Claude / opencode│
|
|
47
|
+
└────────┬─────────┘
|
|
48
|
+
│ MCP (stdio) — 11 tools
|
|
49
|
+
│ session-start injection
|
|
50
|
+
┌────────▼─────────┐
|
|
51
|
+
│ open-memex │
|
|
52
|
+
│ MCP server │
|
|
53
|
+
└──┬────────────┬──┘
|
|
54
|
+
│ │
|
|
55
|
+
┌────────────▼───┐ ┌─────▼──────────┐
|
|
56
|
+
│ Markdown files │ │ SQLite FTS5 │
|
|
57
|
+
│ source of truth│ │ rebuildable │
|
|
58
|
+
│ local-first │ │ index (BM25) │
|
|
59
|
+
└─────────────┬──┘ └────────────────┘
|
|
60
|
+
│ submit (explicit,
|
|
61
|
+
│ local commit)
|
|
62
|
+
┌───────▼────────┐
|
|
63
|
+
│ Git repo │
|
|
64
|
+
│ .ai/open-memex/│
|
|
65
|
+
│ PR-reviewed │
|
|
66
|
+
│ team memory │
|
|
67
|
+
└────────────────┘
|
|
68
|
+
|
|
69
|
+
personal scope:只属于这台机器——永不同步,永远进不了仓库。
|
|
70
|
+
```
|
|
71
|
+
|
|
13
72
|
## 安装
|
|
14
73
|
|
|
15
74
|
### 前置要求
|
|
@@ -18,7 +77,9 @@
|
|
|
18
77
|
|
|
19
78
|
### 第一步——安装 CLI
|
|
20
79
|
|
|
21
|
-
|
|
80
|
+
#### 稳定版 vs Alpha 版
|
|
81
|
+
|
|
82
|
+
**稳定版**(推荐大多数用户)——`latest` 标签:
|
|
22
83
|
|
|
23
84
|
```sh
|
|
24
85
|
npm install -g open-memex
|
|
@@ -26,10 +87,26 @@ npm install -g open-memex
|
|
|
26
87
|
|
|
27
88
|
安装的是 `0.3.0` 正式版。
|
|
28
89
|
|
|
90
|
+
**Alpha 版**(最新开发版,给测试者)——`alpha` 标签:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npm install -g open-memex@alpha
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
查看已发布版本:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
npm view open-memex version # 最新稳定版
|
|
100
|
+
npm view open-memex@alpha version # 最新 alpha 版
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Alpha 版可能有毛边——欢迎报 bug。
|
|
104
|
+
|
|
29
105
|
**免安装——用 npx 直接跑:**
|
|
30
106
|
|
|
31
107
|
```sh
|
|
32
|
-
npx -y open-memex <命令>
|
|
108
|
+
npx -y open-memex <命令> # 例如 npx -y open-memex init --client vscode
|
|
109
|
+
npx -y open-memex@alpha <命令> # alpha 线,免安装
|
|
33
110
|
```
|
|
34
111
|
|
|
35
112
|
**从源码安装**(最新开发版,`V2-dev-p2` 分支):
|
|
@@ -180,6 +257,48 @@ open-memex doctor
|
|
|
180
257
|
(API key、token、高熵凭据)就地打码——保留前 4 个字符,其余替换为 `x`——
|
|
181
258
|
然后照常保存。用 `open-memex capture --dry-run "…"` 预览一条消息会被如何捕获。
|
|
182
259
|
|
|
260
|
+
## 记忆类型(Memory types)
|
|
261
|
+
|
|
262
|
+
11 种类型——`type` 说明这条记忆是什么,`tags` 说明它和什么有关:
|
|
263
|
+
|
|
264
|
+
| 类型 | 记录什么 |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `fact` | 关于项目或世界的稳定事实 |
|
|
267
|
+
| `preference` | 某人做事的偏好 |
|
|
268
|
+
| `decision` | 做过的选择——为什么、权衡了什么 |
|
|
269
|
+
| `constraint` | 不能违反的规则 |
|
|
270
|
+
| `todo` | 以后要做的承诺 |
|
|
271
|
+
| `knowledge` | 持久的领域或架构知识 |
|
|
272
|
+
| `howto` | 验证过的做法 |
|
|
273
|
+
| `gotcha` | 要避开的坑 |
|
|
274
|
+
| `lesson` | 事故或错误教会我们的东西 |
|
|
275
|
+
| `observation` | 注意到的现象,还不是结论 |
|
|
276
|
+
| `reference` | 指向权威文档的指针(不复制内容) |
|
|
277
|
+
|
|
278
|
+
## 团队记忆工作流
|
|
279
|
+
|
|
280
|
+
个人笔记永远私有。项目知识走一条显式、可 review 的流水线——没有任何东西会自动分享:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
capture → outbox(本地草稿)→ submit → 仓库(.ai/open-memex/)→ PR review → published → recall
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
1. **Capture**——正常工作中把决策、坑、教训存成草稿。
|
|
287
|
+
2. **Review**——草稿在本地 outbox 里等着;`sync-status`(或在聊天里说"同步记忆")查看待处理项。
|
|
288
|
+
3. **Submit**——你点名要分享的记忆才会进 `<repo>/.ai/open-memex/`,并做本地 commit。open-memex 永远不会自动 push。
|
|
289
|
+
4. **PR review**——记忆就是纯 Markdown;reviewer 走正常的分支/PR 流程批准、要求修改或拒绝(`promote`、`pr-status`、`resolve`)。
|
|
290
|
+
5. **Recall**——已发布的记忆在会话开始时自动注入,也可随时搜索,人和 agent 都能用。
|
|
291
|
+
|
|
292
|
+
Reviewer 守则:[docs/CURATOR.md](./docs/CURATOR.md)。
|
|
293
|
+
|
|
294
|
+
## 安全与数据
|
|
295
|
+
|
|
296
|
+
- **本地优先:** 所有东西都在你的机器上(`%APPDATA%\open-memex` / `~/.local/share/open-memex`),加上你选择的仓库。零云端调用、零账号、零第三方 API、零遥测。
|
|
297
|
+
- **密钥进不来:** `<private>…</private>` 包裹的内容会被剥离;检测到的 API key / token 在保存前就地打码。先用 `open-memex capture --dry-run "…"` 预览。
|
|
298
|
+
- **个人 scope 永不同步:** `personal` 只属于这台机器——export 默认排除,也永远进不了仓库。
|
|
299
|
+
- **分享可审计:** 团队记忆只能靠显式的 `submit` 移动,走分支/PR review,每次 `promote` 状态流转都会追加到记忆的 `review_history`(谁/何时/为什么)。
|
|
300
|
+
- **文件是你的:** Markdown 是 source of truth——随手看、随手改、随手删;SQLite 索引可以从文件重建。
|
|
301
|
+
|
|
183
302
|
## Scope
|
|
184
303
|
|
|
185
304
|
- **project** — 绑定当前仓库(用 git origin URL 哈希做 key,无 remote 则用 cwd)。新记忆默认进这里。
|
|
@@ -387,28 +506,26 @@ project scope 从进程工作目录解析,所以配置 server 时 cwd 要指
|
|
|
387
506
|
|
|
388
507
|
## 路线图(Roadmap)
|
|
389
508
|
|
|
390
|
-
**`0.3.0
|
|
509
|
+
**`0.3.0`(稳定版):** 通用 MCP server、`open-memex` bin/CLI、
|
|
391
510
|
一键 `init` 配置、中文关键词捕获(含 personal/project 路由)、
|
|
392
511
|
`config` / `capture --dry-run` / `doctor` 助手命令、Visual Studio 支持。
|
|
393
512
|
|
|
394
|
-
|
|
513
|
+
**`0.4.0`(开发中):** 团队同步——用 git 做共享记忆:appdata 草稿箱 →
|
|
395
514
|
`sync-status` → `submit`(本地分支+commit,push/PR 拿你的 Yes 才做)
|
|
396
|
-
→ `promote` / `resolve` 评审工作流、仓库内 `.ai/open-memex/`
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
**未来(看信号再定,不承诺版本):** 原生 agent 插件
|
|
406
|
-
(Claude Code / Codex hooks,作为同一套 MCP tools 的增强路径);
|
|
515
|
+
→ `promote` / `resolve` 评审工作流、仓库内 `.ai/open-memex/` 目录;
|
|
516
|
+
`export` / `import` 归档做用户可携带(Markdown + manifest,不造围墙花园;
|
|
517
|
+
private 默认不导出,`-a` / `--all` 全量迁移);
|
|
518
|
+
distill-to-AGENTS.md 辅助(`distill-agents`,只提议不改写——人工合并);
|
|
519
|
+
§3.5 检查点蒸馏写进 MCP 握手指令和 init 指令文件
|
|
520
|
+
(agent 在检查点提议 1–3 条捕获,人来定);找 1–2 个同事做 pilot。
|
|
521
|
+
|
|
522
|
+
**未来(看信号再定,不承诺版本):** 组织层——组织记忆仓库、curator 约定;
|
|
523
|
+
原生 agent 插件(Claude Code / Codex hooks,作为同一套 MCP tools 的增强路径);
|
|
407
524
|
本地 embedding 做基准测试门控的实验(**未经明确 opt-in 绝不下载
|
|
408
525
|
embedding 模型**);云端 `RemoteProvider` 定制只在多仓库共享、
|
|
409
526
|
ACL 或合规需求出现时才做。
|
|
410
527
|
|
|
411
|
-
设计细节:[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)(append-only
|
|
528
|
+
设计细节:[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)(append-only 决策日志)。
|
|
412
529
|
|
|
413
530
|
## 许可证
|
|
414
531
|
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,10 @@ import { fileURLToPath } from "node:url";
|
|
|
23
23
|
const COMMAND_HELP = {
|
|
24
24
|
where: `Show which project scope the current directory resolves to, and where its data lives.
|
|
25
25
|
|
|
26
|
-
Usage: open-memex where
|
|
26
|
+
Usage: open-memex where
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
open-memex where`,
|
|
27
30
|
list: `List memories in a scope, newest first.
|
|
28
31
|
|
|
29
32
|
Usage: open-memex list [--scope project|personal] [--type T] [--limit N]
|
|
@@ -60,13 +63,22 @@ Example:
|
|
|
60
63
|
open-memex add "We deploy on Fridays" --scope project --tag process`,
|
|
61
64
|
supersede: `Replace a memory with a newer version. The old one is kept as history.
|
|
62
65
|
|
|
63
|
-
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
66
|
+
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
open-memex supersede 01ABC "We deploy on Thursdays now"`,
|
|
64
70
|
status: `Change a memory's lifecycle status.
|
|
65
71
|
|
|
66
|
-
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
72
|
+
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
open-memex status 01ABC deprecated`,
|
|
67
76
|
forget: `Delete a memory by id.
|
|
68
77
|
|
|
69
|
-
Usage: open-memex forget <id
|
|
78
|
+
Usage: open-memex forget <id>
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
open-memex forget 01ABC`,
|
|
70
82
|
propose: `Copy personal memories into the project outbox as review drafts.
|
|
71
83
|
The personal originals stay put. Nothing enters git at this step.
|
|
72
84
|
|
|
@@ -100,21 +112,34 @@ Usage: open-memex resolve [id-or-path]
|
|
|
100
112
|
|
|
101
113
|
With no argument, lists conflicts. With an id or file path, shows the
|
|
102
114
|
3-way merge (base / outbox / repo) so you can resolve it by hand.
|
|
103
|
-
Conflicts are never auto-resolved
|
|
115
|
+
Conflicts are never auto-resolved.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
open-memex resolve
|
|
119
|
+
open-memex resolve 01ABC`,
|
|
104
120
|
"sync-status": `Show the project memory sync pipeline: when the index last synced
|
|
105
121
|
and what triggered it, drafts waiting in the outbox (appdata), memories in the
|
|
106
122
|
repo awaiting review or published, and repo files not yet committed.
|
|
107
123
|
|
|
108
|
-
Usage: open-memex sync-status
|
|
124
|
+
Usage: open-memex sync-status
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
open-memex sync-status`,
|
|
109
128
|
pull: `Pull shared project memories from the git remote: fetch + fast-forward
|
|
110
129
|
only. Never auto-merges — a diverged branch fails with a clear message and is
|
|
111
130
|
left for you to resolve by hand. On success the local index re-syncs.
|
|
112
131
|
|
|
113
|
-
Usage: open-memex pull
|
|
132
|
+
Usage: open-memex pull
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
open-memex pull`,
|
|
114
136
|
push: `Push the current branch (with its submitted memories) to the git
|
|
115
137
|
remote. Explicit only — open-memex never pushes on its own.
|
|
116
138
|
|
|
117
|
-
Usage: open-memex push
|
|
139
|
+
Usage: open-memex push
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
open-memex push`,
|
|
118
143
|
export: `Export memories to a portable .tar.gz bundle (markdown source of
|
|
119
144
|
truth + manifest.json) for moving to another machine or another app.
|
|
120
145
|
Excludes visibility:private memories by default; --all includes everything.
|
|
@@ -126,19 +151,31 @@ Flags:
|
|
|
126
151
|
--type filter by memory type
|
|
127
152
|
--tag filter by tag
|
|
128
153
|
--all, -a include private memories (full migration)
|
|
129
|
-
-o output file (default: ./open-memex-export-<timestamp>.tar.gz)
|
|
154
|
+
-o output file (default: ./open-memex-export-<timestamp>.tar.gz)
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
open-memex export -o backup.tar.gz
|
|
158
|
+
open-memex export --scope both --all -o full-migration.tar.gz`,
|
|
130
159
|
import: `Import a bundle created by \`open-memex export\`. Personal memories
|
|
131
160
|
go to the personal dir; project memories are re-keyed to the current project
|
|
132
161
|
and land in the outbox as drafts. Existing identical memories are skipped;
|
|
133
162
|
conflicting ids are reported, never overwritten.
|
|
134
163
|
|
|
135
|
-
Usage: open-memex import <bundle.tar.gz> [--dry-run]
|
|
164
|
+
Usage: open-memex import <bundle.tar.gz> [--dry-run]
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
open-memex import backup.tar.gz --dry-run
|
|
168
|
+
open-memex import backup.tar.gz`,
|
|
136
169
|
"distill-agents": `Propose an AGENTS.md snippet distilled from project
|
|
137
170
|
memories (decisions, constraints, lessons, gotchas, howtos). Prints markdown
|
|
138
171
|
to stdout, or writes it with -o. Review and merge by hand — open-memex never
|
|
139
172
|
rewrites your AGENTS.md on its own.
|
|
140
173
|
|
|
141
|
-
Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
174
|
+
Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
open-memex distill-agents
|
|
178
|
+
open-memex distill-agents --type decision,gotcha -o agents-snippet.md`,
|
|
142
179
|
submit: `Move outbox drafts into the repo for review: copies the drafts into
|
|
143
180
|
the repo memory dir as proposed (a local-approved copy keeps its approval),
|
|
144
181
|
commits locally on the CURRENT branch, and moves the outbox originals out.
|
|
@@ -162,13 +199,23 @@ overwritten. Report-only by default.
|
|
|
162
199
|
Usage: open-memex pr-status [--apply]
|
|
163
200
|
|
|
164
201
|
Flags:
|
|
165
|
-
--apply write the transitions locally (still never pushes)
|
|
202
|
+
--apply write the transitions locally (still never pushes)
|
|
203
|
+
|
|
204
|
+
Examples:
|
|
205
|
+
open-memex pr-status
|
|
206
|
+
open-memex pr-status --apply`,
|
|
166
207
|
reindex: `Rebuild the SQLite index from the markdown files.
|
|
167
208
|
|
|
168
|
-
Usage: open-memex reindex
|
|
209
|
+
Usage: open-memex reindex
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
open-memex reindex`,
|
|
169
213
|
scopes: `List the known scopes (personal + project).
|
|
170
214
|
|
|
171
|
-
Usage: open-memex scopes
|
|
215
|
+
Usage: open-memex scopes
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
open-memex scopes`,
|
|
172
219
|
migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
|
|
173
220
|
|
|
174
221
|
Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict newer|overwrite|skip]
|
|
@@ -180,13 +227,21 @@ Flags:
|
|
|
180
227
|
--on-conflict newer (default), overwrite, or skip
|
|
181
228
|
--to-v2 convert a legacy my-o-memory data dir to the v2 layout
|
|
182
229
|
|
|
183
|
-
Always preview with --dry-run first; nothing moves without confirmation
|
|
230
|
+
Always preview with --dry-run first; nothing moves without confirmation.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
open-memex migrate --dry-run
|
|
234
|
+
open-memex migrate --from personal --to project --dry-run`,
|
|
184
235
|
mcp: `Start the stdio MCP server (the same server editors connect to).
|
|
185
236
|
|
|
186
237
|
Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
187
238
|
|
|
188
239
|
Flags:
|
|
189
|
-
--print-config print the MCP client config instead of starting the server
|
|
240
|
+
--print-config print the MCP client config instead of starting the server
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
open-memex mcp
|
|
244
|
+
open-memex mcp --print-config vscode`,
|
|
190
245
|
init: `One-command project setup: writes the MCP config for your editor and the
|
|
191
246
|
agent memory instructions. Existing files are merged, never clobbered.
|
|
192
247
|
|
|
@@ -197,21 +252,32 @@ Flags:
|
|
|
197
252
|
--client editor to configure (default: auto-detect)
|
|
198
253
|
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
199
254
|
--force overwrite existing config
|
|
200
|
-
--yes accept all defaults, never prompt
|
|
255
|
+
--yes accept all defaults, never prompt
|
|
256
|
+
|
|
257
|
+
Examples:
|
|
258
|
+
open-memex init
|
|
259
|
+
open-memex init --client cursor --yes`,
|
|
201
260
|
config: `Show config, or set a key.
|
|
202
261
|
|
|
203
262
|
Usage: open-memex config [set <key> <value>]
|
|
204
263
|
|
|
205
|
-
|
|
264
|
+
Examples:
|
|
265
|
+
open-memex config
|
|
206
266
|
open-memex config set sync.autoPull false`,
|
|
207
267
|
capture: `Preview what the keyword-capture watcher would extract from text.
|
|
208
268
|
|
|
209
|
-
Usage: open-memex capture --dry-run "text"
|
|
269
|
+
Usage: open-memex capture --dry-run "text"
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
open-memex capture --dry-run "remember: we deploy on Fridays"`,
|
|
210
273
|
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
211
274
|
storage writability, then boots a real MCP server and runs initialize +
|
|
212
275
|
tools/list against it — all eleven tools must show up.
|
|
213
276
|
|
|
214
|
-
Usage: open-memex doctor
|
|
277
|
+
Usage: open-memex doctor
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
open-memex doctor`,
|
|
215
281
|
};
|
|
216
282
|
function usage(exitCode = 1) {
|
|
217
283
|
console.log(`open-memex CLI
|
|
@@ -247,6 +313,9 @@ Usage:
|
|
|
247
313
|
open-memex capture --dry-run "text"
|
|
248
314
|
open-memex doctor
|
|
249
315
|
|
|
316
|
+
Every command has its own help with description and examples:
|
|
317
|
+
open-memex <command> --help (or -h)
|
|
318
|
+
|
|
250
319
|
One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) writes
|
|
251
320
|
the MCP config for your editor (\`.vscode/mcp.json\`, \`.cursor/mcp.json\`,
|
|
252
321
|
\`opencode.jsonc\`, or Visual Studio's solution-level \`.mcp.json\`) — no copy-paste
|
|
@@ -1044,6 +1113,10 @@ async function main() {
|
|
|
1044
1113
|
entries.push({ key: name, files, marker });
|
|
1045
1114
|
}
|
|
1046
1115
|
entries.sort((a, b) => b.files - a.files);
|
|
1116
|
+
if (entries.length === 0) {
|
|
1117
|
+
console.log("(no scopes with memories yet — add one with `open-memex add`)");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1047
1120
|
for (const e of entries) {
|
|
1048
1121
|
console.log(` ${e.files.toString().padStart(4)} ${e.key}${e.marker}`);
|
|
1049
1122
|
}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-memex",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.11",
|
|
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",
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Full feature test — exercises every CLI command and every MCP tool.
|
|
2
|
+
// Usage: node --experimental-strip-types scripts/test-full.ts
|
|
3
|
+
// Isolated: uses temp HOME + MY_O_MEMORY_HOME + temp git repos. Touches nothing real.
|
|
4
|
+
//
|
|
5
|
+
// NOTE on flakiness: better-sqlite3 11.x intermittently crashes at process
|
|
6
|
+
// exit on Node 24 (RemoveEnvironmentCleanupHook assertion, exit 134) — a
|
|
7
|
+
// known pre-existing issue. The work itself always completes; only the exit
|
|
8
|
+
// code/output flush is affected. Id-generating calls retry until an id is
|
|
9
|
+
// parsed, and crash-prone commands retry up to 5 times.
|
|
10
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const REPO = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
const CLI = path.join(REPO, "dist", "cli.js");
|
|
18
|
+
const MCP_TS = path.join(REPO, "src", "mcp.ts");
|
|
19
|
+
|
|
20
|
+
const T = fs.mkdtempSync(path.join(os.tmpdir(), "om-full-"));
|
|
21
|
+
const TESTENV = {
|
|
22
|
+
...process.env,
|
|
23
|
+
HOME: path.join(T, "home"),
|
|
24
|
+
MY_O_MEMORY_HOME: path.join(T, "data"),
|
|
25
|
+
};
|
|
26
|
+
fs.mkdirSync(TESTENV.HOME, { recursive: true });
|
|
27
|
+
|
|
28
|
+
let pass = 0, fail = 0;
|
|
29
|
+
const failures: string[] = [];
|
|
30
|
+
function ok(name: string, cond: boolean, info?: string) {
|
|
31
|
+
if (cond) { pass++; console.log(` ok ${name}`); }
|
|
32
|
+
else { fail++; failures.push(name); console.log(` FAIL ${name}${info ? " — " + info : ""}`); }
|
|
33
|
+
}
|
|
34
|
+
function isCrash(err: string): boolean {
|
|
35
|
+
return /RemoveEnvironmentCleanupHook|Assertion failed/.test(err);
|
|
36
|
+
}
|
|
37
|
+
// Base runner: transparently retries the known better-sqlite3 exit crash
|
|
38
|
+
// (work completes; only exit code/output flush is affected).
|
|
39
|
+
function cliRaw(args: string[], cwd: string, env: NodeJS.ProcessEnv): { out: string; err: string; code: number } {
|
|
40
|
+
let last = { out: "", err: "", code: 1 };
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
try {
|
|
43
|
+
const out = execFileSync("node", [CLI, ...args], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
44
|
+
return { out, err: "", code: 0 };
|
|
45
|
+
} catch (e: any) {
|
|
46
|
+
last = { out: e.stdout ?? "", err: e.stderr ?? "", code: e.status ?? 1 };
|
|
47
|
+
if (!isCrash(last.err)) break; // real error, not the flaky crash
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return last;
|
|
51
|
+
}
|
|
52
|
+
function cli(args: string[], cwd: string): { out: string; err: string; code: number } {
|
|
53
|
+
return cliRaw(args, cwd, TESTENV);
|
|
54
|
+
}
|
|
55
|
+
function git(args: string[], cwd: string) {
|
|
56
|
+
execFileSync("git", args, { cwd, env: TESTENV, stdio: "ignore" });
|
|
57
|
+
}
|
|
58
|
+
function mkproj(name: string): string {
|
|
59
|
+
const p = path.join(T, name);
|
|
60
|
+
fs.mkdirSync(p, { recursive: true });
|
|
61
|
+
git(["init", "-q"], p);
|
|
62
|
+
git(["config", "user.email", "test@example.com"], p);
|
|
63
|
+
git(["config", "user.name", "Test"], p);
|
|
64
|
+
fs.writeFileSync(path.join(p, "README.md"), "# test\n");
|
|
65
|
+
git(["add", "."], p);
|
|
66
|
+
git(["commit", "-qm", "init"], p);
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
const grabId = (out: string) => (out.match(/saved ([A-Z0-9]{26})/) || [])[1] ?? "";
|
|
70
|
+
const grabArrowId = (out: string) => (out.match(/→\s*([A-Z0-9]{26})/) || [])[1] ?? "";
|
|
71
|
+
function tarRead(tarfile: string, member: string): string {
|
|
72
|
+
try {
|
|
73
|
+
return execFileSync("tar", ["-xzOf", tarfile, member], { env: TESTENV, encoding: "utf8" });
|
|
74
|
+
} catch { return ""; }
|
|
75
|
+
}
|
|
76
|
+
function cliRetry(args: string[], cwd: string): { out: string; err: string; code: number } {
|
|
77
|
+
return cli(args, cwd); // retry is built into cliRaw
|
|
78
|
+
}
|
|
79
|
+
// add is the id factory — the known better-sqlite3 exit crash can eat the
|
|
80
|
+
// output, so retry until we actually get an id back.
|
|
81
|
+
function addMem(args: string[], cwd: string): string {
|
|
82
|
+
for (let i = 0; i < 5; i++) {
|
|
83
|
+
const id = grabId(cli(["add", ...args], cwd).out);
|
|
84
|
+
if (id) return id;
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
function proposeMem(id: string, cwd: string): string {
|
|
89
|
+
for (let i = 0; i < 5; i++) {
|
|
90
|
+
const d = grabArrowId(cli(["propose", id, "--to", "project"], cwd).out);
|
|
91
|
+
if (d) return d;
|
|
92
|
+
}
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
// Separate data dir = separate machine (the real export/import scenario).
|
|
96
|
+
const TESTENV2 = { ...TESTENV, MY_O_MEMORY_HOME: path.join(T, "data2") };
|
|
97
|
+
function cli2(args: string[], cwd: string): { out: string; err: string; code: number } {
|
|
98
|
+
return cliRaw(args, cwd, TESTENV2);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("== setup ==");
|
|
102
|
+
const PROJ = mkproj("proj");
|
|
103
|
+
const REMOTE = path.join(T, "remote.git");
|
|
104
|
+
execFileSync("git", ["init", "-q", "--bare", REMOTE], { env: TESTENV });
|
|
105
|
+
git(["remote", "add", "origin", REMOTE], PROJ);
|
|
106
|
+
ok("proj + bare remote ready", fs.existsSync(path.join(PROJ, ".git")));
|
|
107
|
+
|
|
108
|
+
// ---------- 1. where / scopes / config ----------
|
|
109
|
+
console.log("== where / scopes / config ==");
|
|
110
|
+
let r = cli(["where"], PROJ);
|
|
111
|
+
ok("where shows project scope", /project__/.test(r.out), r.out.slice(0, 120));
|
|
112
|
+
r = cli(["scopes"], PROJ);
|
|
113
|
+
ok("scopes empty → graceful message", /no scopes with memories yet/.test(r.out), r.out.slice(0, 120));
|
|
114
|
+
r = cli(["config", "set", "sync.autoPull", "true"], PROJ);
|
|
115
|
+
r = cli(["config"], PROJ);
|
|
116
|
+
ok("config set/get dotted key", /autoPull/.test(r.out) && /true/.test(r.out), r.out.slice(0, 200));
|
|
117
|
+
cli(["config", "set", "sync.autoPull", "false"], PROJ);
|
|
118
|
+
|
|
119
|
+
// ---------- 2. add / list / search ----------
|
|
120
|
+
console.log("== add / list / search ==");
|
|
121
|
+
const idFact = addMem(["fulltest fact about deploys", "--type", "fact", "--tag", "t1"], PROJ);
|
|
122
|
+
const idDecision = addMem(["fulltest decision to use ff merges", "--type", "decision", "--tag", "t2"], PROJ);
|
|
123
|
+
const idPersonal = addMem(["fulltest personal pref", "--scope", "personal"], PROJ);
|
|
124
|
+
ok("add returns ids", !!idFact && !!idDecision && !!idPersonal);
|
|
125
|
+
r = cli(["list"], PROJ);
|
|
126
|
+
ok("list default = project only", r.out.includes(idFact) && r.out.includes(idDecision) && !r.out.includes(idPersonal));
|
|
127
|
+
r = cli(["list", "--scope", "personal"], PROJ);
|
|
128
|
+
ok("list --scope personal", r.out.includes(idPersonal) && !r.out.includes(idFact));
|
|
129
|
+
r = cli(["scopes"], PROJ);
|
|
130
|
+
ok("scopes lists populated scopes", /personal/.test(r.out) && /project__/.test(r.out), r.out.slice(0, 160));
|
|
131
|
+
r = cli(["list", "--type", "decision"], PROJ);
|
|
132
|
+
ok("list --type filter", r.out.includes(idDecision) && !r.out.includes(idFact));
|
|
133
|
+
r = cli(["search", "ff merges"], PROJ);
|
|
134
|
+
ok("search finds decision", r.out.includes(idDecision), r.out.slice(0, 150));
|
|
135
|
+
|
|
136
|
+
// ---------- 3. supersede / status / forget ----------
|
|
137
|
+
console.log("== supersede / status / forget ==");
|
|
138
|
+
r = cli(["supersede", idFact, "fulltest fact about deploys v2"], PROJ);
|
|
139
|
+
const idV2 = grabArrowId(r.out);
|
|
140
|
+
ok("supersede creates new version", !!idV2 && idV2 !== idFact, r.out.slice(0, 150));
|
|
141
|
+
const idDep = addMem(["fulltest to deprecate", "--type", "fact"], PROJ);
|
|
142
|
+
r = cli(["status", idDep, "deprecated"], PROJ);
|
|
143
|
+
ok("status deprecated", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
144
|
+
r = cli(["status", idFact, "deprecated"], PROJ);
|
|
145
|
+
ok("status on superseded chain-member is refused", r.code !== 0 && /chain-managed|supersede/i.test(r.err + r.out), (r.err || r.out).slice(0, 120));
|
|
146
|
+
r = cli(["forget", idDecision], PROJ);
|
|
147
|
+
ok("forget deletes", r.code === 0 && /delet/i.test(r.out), r.out.slice(0, 120));
|
|
148
|
+
r = cli(["list"], PROJ);
|
|
149
|
+
ok("forgotten id gone from list", !r.out.includes(idDecision));
|
|
150
|
+
|
|
151
|
+
// ---------- 4. propose / submit / promote / sync-status ----------
|
|
152
|
+
console.log("== review workflow: propose / submit / promote ==");
|
|
153
|
+
const idProp = addMem(["fulltest proposal candidate", "--scope", "personal"], PROJ);
|
|
154
|
+
const idDraft = proposeMem(idProp, PROJ);
|
|
155
|
+
ok("propose stages outbox draft", !!idDraft);
|
|
156
|
+
r = cli(["sync-status"], PROJ);
|
|
157
|
+
ok("sync-status shows outbox draft", /outbox/.test(r.out) && r.out.includes(idDraft));
|
|
158
|
+
r = cli(["submit", idDraft], PROJ);
|
|
159
|
+
ok("submit commits locally", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
160
|
+
const log = execFileSync("git", ["log", "--oneline", "-1"], { cwd: PROJ, env: TESTENV, encoding: "utf8" });
|
|
161
|
+
ok("submit created a git commit", /submit|mem/i.test(log), log.trim());
|
|
162
|
+
r = cli(["promote", idDraft, "--note", "fulltest approval"], PROJ);
|
|
163
|
+
ok("promote → approved", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
164
|
+
r = cli(["promote", idDraft, "--note", "fulltest publish"], PROJ);
|
|
165
|
+
ok("promote → published", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
166
|
+
const idRej = addMem(["fulltest reject candidate", "--scope", "personal"], PROJ);
|
|
167
|
+
const idRejDraft = proposeMem(idRej, PROJ);
|
|
168
|
+
r = cliRetry(["submit", idRejDraft], PROJ);
|
|
169
|
+
ok("reject-path submit", r.code === 0, (r.err || r.out).slice(0, 120));
|
|
170
|
+
r = cli(["promote", idRejDraft, "--reject", "--note", "fulltest rejection"], PROJ);
|
|
171
|
+
ok("promote --reject with note", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
172
|
+
|
|
173
|
+
// ---------- 5. export / import ----------
|
|
174
|
+
console.log("== export / import ==");
|
|
175
|
+
const expFile = path.join(T, "bundle.tar.gz");
|
|
176
|
+
r = cliRetry(["export", "-o", expFile], PROJ);
|
|
177
|
+
ok("export creates bundle", r.code === 0 && fs.existsSync(expFile), (r.err || r.out).slice(0, 120));
|
|
178
|
+
const man = JSON.parse(tarRead(expFile, "manifest.json") || "{}");
|
|
179
|
+
ok("manifest format valid", man.format === "open-memex-export/1" && Array.isArray(man.memories) && man.memories.length > 0, JSON.stringify(man).slice(0, 120));
|
|
180
|
+
r = cliRetry(["export", "--scope", "personal", "--all", "-o", path.join(T, "p.tar.gz")], PROJ);
|
|
181
|
+
const manP = JSON.parse(tarRead(path.join(T, "p.tar.gz"), "manifest.json") || "{}");
|
|
182
|
+
ok("export --all includes private", manP.include_private === true && manP.memories.length >= 2);
|
|
183
|
+
const PROJ2 = mkproj("proj2");
|
|
184
|
+
function cli2Retry(args: string[], cwd: string): { out: string; err: string; code: number } {
|
|
185
|
+
return cli2(args, cwd); // retry is built into cliRaw
|
|
186
|
+
}
|
|
187
|
+
r = cli2Retry(["import", expFile, "--dry-run"], PROJ2);
|
|
188
|
+
ok("import --dry-run", /DRY RUN/.test(r.out), r.out.slice(0, 120));
|
|
189
|
+
r = cli2Retry(["import", expFile], PROJ2);
|
|
190
|
+
ok("import real run", /imported [1-9]/.test(r.out), r.out.slice(0, 120));
|
|
191
|
+
r = cli2(["list"], PROJ2);
|
|
192
|
+
ok("imported memories visible in target project", /fulltest/.test(r.out), r.out.slice(0, 160));
|
|
193
|
+
r = cli2Retry(["import", expFile], PROJ2);
|
|
194
|
+
ok("import identical → skipped", /skipped [1-9]+ identical/.test(r.out), r.out.slice(0, 120));
|
|
195
|
+
// conflict: craft bundle with same id, different content
|
|
196
|
+
const cid = man.memories[0].id;
|
|
197
|
+
const cstage = path.join(T, "cstage");
|
|
198
|
+
fs.mkdirSync(path.join(cstage, "memories", "project"), { recursive: true });
|
|
199
|
+
fs.writeFileSync(path.join(cstage, "memories", "project", cid + ".md"),
|
|
200
|
+
`---\nid: ${cid}\nscope: project\nscope_key: project__old__x\nvisibility: internal\ntype: fact\nstatus: active\ncreated_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\n---\n\nCONFLICT CONTENT\n`);
|
|
201
|
+
fs.writeFileSync(path.join(cstage, "manifest.json"), JSON.stringify({ format: "open-memex-export/1", exported_at: "2026-01-01T00:00:00Z", open_memex_version: "t", include_private: false, filters: {}, memories: [{ id: cid, scope: "project", file: `memories/project/${cid}.md` }] }));
|
|
202
|
+
const cfile = path.join(T, "conflict.tar.gz");
|
|
203
|
+
execFileSync("tar", ["-czf", cfile, "-C", cstage, "manifest.json", "memories"], { env: TESTENV });
|
|
204
|
+
r = cli2Retry(["import", cfile], PROJ2);
|
|
205
|
+
ok("import conflict reported, never overwritten", /conflict/.test(r.out) && /imported 0/.test(r.out), r.out.slice(0, 160));
|
|
206
|
+
|
|
207
|
+
// ---------- 6. distill-agents ----------
|
|
208
|
+
console.log("== distill-agents ==");
|
|
209
|
+
addMem(["fulltest distill decision: always fast-forward", "--type", "decision"], PROJ);
|
|
210
|
+
addMem(["fulltest distill gotcha: sqlite crashes on exit", "--type", "gotcha"], PROJ);
|
|
211
|
+
r = cliRetry(["distill-agents"], PROJ);
|
|
212
|
+
ok("distill-agents proposes snippet", /## Learned/.test(r.out), r.out.slice(0, 120));
|
|
213
|
+
r = cliRetry(["distill-agents", "--type", "decision", "-o", path.join(T, "snip.md")], PROJ);
|
|
214
|
+
ok("distill-agents -o writes file", fs.existsSync(path.join(T, "snip.md")));
|
|
215
|
+
|
|
216
|
+
// ---------- 7. pull / push ----------
|
|
217
|
+
console.log("== pull / push ==");
|
|
218
|
+
r = cliRetry(["push"], PROJ);
|
|
219
|
+
ok("push explicit", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
220
|
+
const PROJ3 = path.join(T, "proj3");
|
|
221
|
+
execFileSync("git", ["clone", "-q", REMOTE, PROJ3], { env: TESTENV });
|
|
222
|
+
git(["config", "user.email", "test@example.com"], PROJ3);
|
|
223
|
+
git(["config", "user.name", "Test"], PROJ3);
|
|
224
|
+
const idRemote = addMem(["fulltest remote memory", "--type", "fact"], PROJ3);
|
|
225
|
+
cliRetry(["submit", idRemote], PROJ3);
|
|
226
|
+
cliRetry(["push"], PROJ3);
|
|
227
|
+
r = cliRetry(["pull"], PROJ);
|
|
228
|
+
ok("pull fast-forward", r.code === 0 && /up.to.date|fast-forward|pulled/i.test(r.out + r.err), (r.out + r.err).slice(0, 150));
|
|
229
|
+
// diverged: commit on both sides
|
|
230
|
+
fs.writeFileSync(path.join(PROJ, "div1.txt"), "a");
|
|
231
|
+
git(["add", "."], PROJ); git(["commit", "-qm", "div1"], PROJ);
|
|
232
|
+
fs.writeFileSync(path.join(PROJ3, "div2.txt"), "b");
|
|
233
|
+
git(["add", "."], PROJ3); git(["commit", "-qm", "div2"], PROJ3);
|
|
234
|
+
cli(["push"], PROJ3);
|
|
235
|
+
r = cli(["pull"], PROJ);
|
|
236
|
+
ok("pull diverged → clear failure", r.code !== 0 && /diverg|behind|ahead/i.test(r.out + r.err), (r.out + r.err).slice(0, 160));
|
|
237
|
+
|
|
238
|
+
// ---------- 8. migrate / reindex ----------
|
|
239
|
+
console.log("== migrate / reindex ==");
|
|
240
|
+
r = cli(["migrate", "--dry-run"], PROJ);
|
|
241
|
+
ok("migrate bare → graceful no-op message", /no --from given/.test(r.out + r.err), (r.out + r.err).slice(0, 150));
|
|
242
|
+
const idMig = addMem(["fulltest migrate me", "--scope", "personal"], PROJ);
|
|
243
|
+
r = cli(["migrate", "--from", "personal", "--to", "project", "--dry-run"], PROJ);
|
|
244
|
+
ok("migrate personal→project dry-run", r.code === 0, (r.err || r.out).slice(0, 150));
|
|
245
|
+
const dbPath = path.join(TESTENV.MY_O_MEMORY_HOME as string, "index.db");
|
|
246
|
+
fs.rmSync(dbPath);
|
|
247
|
+
const beforeReindex = Date.now();
|
|
248
|
+
cli(["reindex"], PROJ); // exit code unreliable (known sqlite exit crash); verify by artifact
|
|
249
|
+
let dbOk = false;
|
|
250
|
+
try { dbOk = fs.existsSync(dbPath) && fs.statSync(dbPath).mtimeMs >= beforeReindex - 1000; } catch { /* no */ }
|
|
251
|
+
ok("reindex rebuilds", dbOk, `db exists: ${fs.existsSync(dbPath)}`);
|
|
252
|
+
r = cli(["list", "--scope", "personal"], PROJ);
|
|
253
|
+
ok("list works after reindex", r.out.includes(idMig) || r.out.includes(idPersonal));
|
|
254
|
+
|
|
255
|
+
// ---------- 9. capture / doctor / mcp --print-config / init ----------
|
|
256
|
+
console.log("== capture / doctor / mcp / init ==");
|
|
257
|
+
r = cli(["capture", "--dry-run", "remember: we deploy on Fridays and use ff merges"], PROJ);
|
|
258
|
+
ok("capture --dry-run", r.code === 0 && /deploy|friday/i.test(r.out), r.out.slice(0, 150));
|
|
259
|
+
for (const c of ["vscode", "cursor", "claude", "opencode", "visualstudio"]) {
|
|
260
|
+
r = cli(["mcp", "--print-config", c], PROJ);
|
|
261
|
+
ok(`mcp --print-config ${c}`, r.code === 0 && r.out.trim().length > 10, r.err.slice(0, 100));
|
|
262
|
+
}
|
|
263
|
+
r = cli(["init", "--client", "vscode", "--yes"], PROJ);
|
|
264
|
+
ok("init --client vscode --yes", r.code === 0 && fs.existsSync(path.join(PROJ, ".vscode", "mcp.json")), (r.err || r.out).slice(0, 150));
|
|
265
|
+
r = cliRetry(["doctor"], PROJ);
|
|
266
|
+
ok("doctor", r.code === 0 && /All checks passed/.test(r.out), (r.err || r.out).slice(0, 200));
|
|
267
|
+
|
|
268
|
+
// ---------- 10. MCP: all 11 tools + session-start instructions ----------
|
|
269
|
+
console.log("== MCP tools ==");
|
|
270
|
+
{
|
|
271
|
+
const child = spawn("node", ["--experimental-strip-types", MCP_TS], {
|
|
272
|
+
cwd: PROJ, env: TESTENV, stdio: ["pipe", "pipe", "pipe"],
|
|
273
|
+
});
|
|
274
|
+
let buf = "";
|
|
275
|
+
let id = 0;
|
|
276
|
+
const pending = new Map<number, (v: any) => void>();
|
|
277
|
+
child.stdout.on("data", (d: Buffer) => {
|
|
278
|
+
buf += d.toString();
|
|
279
|
+
let idx: number;
|
|
280
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
281
|
+
const line = buf.slice(0, idx).trim();
|
|
282
|
+
buf = buf.slice(idx + 1);
|
|
283
|
+
if (!line) continue;
|
|
284
|
+
try {
|
|
285
|
+
const msg = JSON.parse(line);
|
|
286
|
+
if (msg.id !== undefined && pending.has(msg.id)) { pending.get(msg.id)!(msg); pending.delete(msg.id); }
|
|
287
|
+
} catch { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
const req = (method: string, params: any) => new Promise<any>((resolve) => {
|
|
291
|
+
const i = ++id;
|
|
292
|
+
pending.set(i, resolve);
|
|
293
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: i, method, params }) + "\n");
|
|
294
|
+
setTimeout(() => { if (pending.has(i)) { pending.delete(i); resolve({ __timeout: true }); } }, 30000);
|
|
295
|
+
});
|
|
296
|
+
const call = async (tool: string, args: any) => {
|
|
297
|
+
const resp = await req("tools/call", { name: tool, arguments: args });
|
|
298
|
+
const text = (resp.result?.content || []).map((c: any) => c.text || "").join("\n");
|
|
299
|
+
return { resp, text };
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const init = await req("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "t", version: "1" } });
|
|
303
|
+
const instructions: string = init.result?.instructions || "";
|
|
304
|
+
ok("MCP initialize returns instructions", instructions.length > 200, instructions.slice(0, 100));
|
|
305
|
+
ok("instructions mention sync/memory_status", /memory_status|sync/i.test(instructions));
|
|
306
|
+
await req("notifications/initialized", {});
|
|
307
|
+
const tools = await req("tools/list", {});
|
|
308
|
+
const names: string[] = (tools.result?.tools || []).map((t: any) => t.name);
|
|
309
|
+
const expected = ["memory_add","memory_forget","memory_list","memory_pr_status","memory_promote","memory_propose","memory_resolve","memory_search","memory_status","memory_submit","memory_supersede"];
|
|
310
|
+
ok("11 MCP tools listed", expected.every((n) => names.includes(n)), names.join(","));
|
|
311
|
+
|
|
312
|
+
let m = await call("memory_add", { content: "mcptest fact via MCP", type: "fact" });
|
|
313
|
+
const mid = (m.text.match(/([A-Z0-9]{26})/) || [])[1] || "";
|
|
314
|
+
ok("memory_add", !!mid, m.text.slice(0, 120));
|
|
315
|
+
m = await call("memory_search", { query: "mcptest fact" });
|
|
316
|
+
ok("memory_search", m.text.includes(mid), m.text.slice(0, 120));
|
|
317
|
+
m = await call("memory_list", { limit: 5 });
|
|
318
|
+
ok("memory_list", m.text.includes(mid) || /fact/.test(m.text), m.text.slice(0, 120));
|
|
319
|
+
m = await call("memory_status", {});
|
|
320
|
+
ok("memory_status", /project|outbox|sync/i.test(m.text), m.text.slice(0, 120));
|
|
321
|
+
m = await call("memory_supersede", { id: mid, content: "mcptest fact via MCP v2" });
|
|
322
|
+
const mid2 = (m.text.match(/with ([A-Z0-9]{26})/) || [])[1] || "";
|
|
323
|
+
ok("memory_supersede", !!mid2 && mid2 !== mid, m.text.slice(0, 120));
|
|
324
|
+
// propose → submit → promote via MCP
|
|
325
|
+
const pid = ((await call("memory_add", { content: "mcptest proposal", scope: "personal" })).text.match(/([A-Z0-9]{26})/) || [])[1] || "";
|
|
326
|
+
m = await call("memory_propose", { ids: [pid] });
|
|
327
|
+
const pdraft = (m.text.match(/→\s*([A-Z0-9]{26})/) || m.text.match(/([A-Z0-9]{26})/) || [])[1] || "";
|
|
328
|
+
ok("memory_propose", !!pdraft && pdraft !== pid, m.text.slice(0, 120));
|
|
329
|
+
m = await call("memory_submit", { ids: [pdraft] });
|
|
330
|
+
ok("memory_submit", !/error/i.test(m.text) || /commit|submit/i.test(m.text), m.text.slice(0, 120));
|
|
331
|
+
m = await call("memory_promote", { id: pdraft, note: "mcptest approve" });
|
|
332
|
+
ok("memory_promote", !/__timeout/.test(JSON.stringify(m.resp)), m.text.slice(0, 120));
|
|
333
|
+
m = await call("memory_resolve", {});
|
|
334
|
+
ok("memory_resolve (list)", !/__timeout/.test(JSON.stringify(m.resp)), m.text.slice(0, 120));
|
|
335
|
+
m = await call("memory_pr_status", {});
|
|
336
|
+
ok("memory_pr_status (report)", !/__timeout/.test(JSON.stringify(m.resp)), m.text.slice(0, 120));
|
|
337
|
+
m = await call("memory_forget", { id: mid2 });
|
|
338
|
+
ok("memory_forget", /delet|forget/i.test(m.text), m.text.slice(0, 120));
|
|
339
|
+
child.kill();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log("\n================ SUMMARY ================");
|
|
343
|
+
console.log(`pass: ${pass}, fail: ${fail}`);
|
|
344
|
+
if (failures.length) { console.log("failed tests:"); for (const f of failures) console.log(" - " + f); }
|
|
345
|
+
process.exit(fail ? 1 : 0);
|
package/src/cli.ts
CHANGED
|
@@ -30,7 +30,10 @@ import { fileURLToPath } from "node:url";
|
|
|
30
30
|
const COMMAND_HELP: Record<string, string> = {
|
|
31
31
|
where: `Show which project scope the current directory resolves to, and where its data lives.
|
|
32
32
|
|
|
33
|
-
Usage: open-memex where
|
|
33
|
+
Usage: open-memex where
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
open-memex where`,
|
|
34
37
|
|
|
35
38
|
list: `List memories in a scope, newest first.
|
|
36
39
|
|
|
@@ -71,15 +74,24 @@ Example:
|
|
|
71
74
|
|
|
72
75
|
supersede: `Replace a memory with a newer version. The old one is kept as history.
|
|
73
76
|
|
|
74
|
-
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
77
|
+
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
open-memex supersede 01ABC "We deploy on Thursdays now"`,
|
|
75
81
|
|
|
76
82
|
status: `Change a memory's lifecycle status.
|
|
77
83
|
|
|
78
|
-
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
84
|
+
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
open-memex status 01ABC deprecated`,
|
|
79
88
|
|
|
80
89
|
forget: `Delete a memory by id.
|
|
81
90
|
|
|
82
|
-
Usage: open-memex forget <id
|
|
91
|
+
Usage: open-memex forget <id>
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
open-memex forget 01ABC`,
|
|
83
95
|
|
|
84
96
|
propose: `Copy personal memories into the project outbox as review drafts.
|
|
85
97
|
The personal originals stay put. Nothing enters git at this step.
|
|
@@ -116,24 +128,37 @@ Usage: open-memex resolve [id-or-path]
|
|
|
116
128
|
|
|
117
129
|
With no argument, lists conflicts. With an id or file path, shows the
|
|
118
130
|
3-way merge (base / outbox / repo) so you can resolve it by hand.
|
|
119
|
-
Conflicts are never auto-resolved
|
|
131
|
+
Conflicts are never auto-resolved.
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
open-memex resolve
|
|
135
|
+
open-memex resolve 01ABC`,
|
|
120
136
|
|
|
121
137
|
"sync-status": `Show the project memory sync pipeline: when the index last synced
|
|
122
138
|
and what triggered it, drafts waiting in the outbox (appdata), memories in the
|
|
123
139
|
repo awaiting review or published, and repo files not yet committed.
|
|
124
140
|
|
|
125
|
-
Usage: open-memex sync-status
|
|
141
|
+
Usage: open-memex sync-status
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
open-memex sync-status`,
|
|
126
145
|
|
|
127
146
|
pull: `Pull shared project memories from the git remote: fetch + fast-forward
|
|
128
147
|
only. Never auto-merges — a diverged branch fails with a clear message and is
|
|
129
148
|
left for you to resolve by hand. On success the local index re-syncs.
|
|
130
149
|
|
|
131
|
-
Usage: open-memex pull
|
|
150
|
+
Usage: open-memex pull
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
open-memex pull`,
|
|
132
154
|
|
|
133
155
|
push: `Push the current branch (with its submitted memories) to the git
|
|
134
156
|
remote. Explicit only — open-memex never pushes on its own.
|
|
135
157
|
|
|
136
|
-
Usage: open-memex push
|
|
158
|
+
Usage: open-memex push
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
open-memex push`,
|
|
137
162
|
|
|
138
163
|
export: `Export memories to a portable .tar.gz bundle (markdown source of
|
|
139
164
|
truth + manifest.json) for moving to another machine or another app.
|
|
@@ -146,21 +171,33 @@ Flags:
|
|
|
146
171
|
--type filter by memory type
|
|
147
172
|
--tag filter by tag
|
|
148
173
|
--all, -a include private memories (full migration)
|
|
149
|
-
-o output file (default: ./open-memex-export-<timestamp>.tar.gz)
|
|
174
|
+
-o output file (default: ./open-memex-export-<timestamp>.tar.gz)
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
open-memex export -o backup.tar.gz
|
|
178
|
+
open-memex export --scope both --all -o full-migration.tar.gz`,
|
|
150
179
|
|
|
151
180
|
import: `Import a bundle created by \`open-memex export\`. Personal memories
|
|
152
181
|
go to the personal dir; project memories are re-keyed to the current project
|
|
153
182
|
and land in the outbox as drafts. Existing identical memories are skipped;
|
|
154
183
|
conflicting ids are reported, never overwritten.
|
|
155
184
|
|
|
156
|
-
Usage: open-memex import <bundle.tar.gz> [--dry-run]
|
|
185
|
+
Usage: open-memex import <bundle.tar.gz> [--dry-run]
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
open-memex import backup.tar.gz --dry-run
|
|
189
|
+
open-memex import backup.tar.gz`,
|
|
157
190
|
|
|
158
191
|
"distill-agents": `Propose an AGENTS.md snippet distilled from project
|
|
159
192
|
memories (decisions, constraints, lessons, gotchas, howtos). Prints markdown
|
|
160
193
|
to stdout, or writes it with -o. Review and merge by hand — open-memex never
|
|
161
194
|
rewrites your AGENTS.md on its own.
|
|
162
195
|
|
|
163
|
-
Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
196
|
+
Usage: open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N] [-o <file>]
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
open-memex distill-agents
|
|
200
|
+
open-memex distill-agents --type decision,gotcha -o agents-snippet.md`,
|
|
164
201
|
|
|
165
202
|
submit: `Move outbox drafts into the repo for review: copies the drafts into
|
|
166
203
|
the repo memory dir as proposed (a local-approved copy keeps its approval),
|
|
@@ -186,15 +223,25 @@ overwritten. Report-only by default.
|
|
|
186
223
|
Usage: open-memex pr-status [--apply]
|
|
187
224
|
|
|
188
225
|
Flags:
|
|
189
|
-
--apply write the transitions locally (still never pushes)
|
|
226
|
+
--apply write the transitions locally (still never pushes)
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
open-memex pr-status
|
|
230
|
+
open-memex pr-status --apply`,
|
|
190
231
|
|
|
191
232
|
reindex: `Rebuild the SQLite index from the markdown files.
|
|
192
233
|
|
|
193
|
-
Usage: open-memex reindex
|
|
234
|
+
Usage: open-memex reindex
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
open-memex reindex`,
|
|
194
238
|
|
|
195
239
|
scopes: `List the known scopes (personal + project).
|
|
196
240
|
|
|
197
|
-
Usage: open-memex scopes
|
|
241
|
+
Usage: open-memex scopes
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
open-memex scopes`,
|
|
198
245
|
|
|
199
246
|
migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
|
|
200
247
|
|
|
@@ -207,14 +254,22 @@ Flags:
|
|
|
207
254
|
--on-conflict newer (default), overwrite, or skip
|
|
208
255
|
--to-v2 convert a legacy my-o-memory data dir to the v2 layout
|
|
209
256
|
|
|
210
|
-
Always preview with --dry-run first; nothing moves without confirmation
|
|
257
|
+
Always preview with --dry-run first; nothing moves without confirmation.
|
|
258
|
+
|
|
259
|
+
Examples:
|
|
260
|
+
open-memex migrate --dry-run
|
|
261
|
+
open-memex migrate --from personal --to project --dry-run`,
|
|
211
262
|
|
|
212
263
|
mcp: `Start the stdio MCP server (the same server editors connect to).
|
|
213
264
|
|
|
214
265
|
Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
215
266
|
|
|
216
267
|
Flags:
|
|
217
|
-
--print-config print the MCP client config instead of starting the server
|
|
268
|
+
--print-config print the MCP client config instead of starting the server
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
open-memex mcp
|
|
272
|
+
open-memex mcp --print-config vscode`,
|
|
218
273
|
|
|
219
274
|
init: `One-command project setup: writes the MCP config for your editor and the
|
|
220
275
|
agent memory instructions. Existing files are merged, never clobbered.
|
|
@@ -226,24 +281,35 @@ Flags:
|
|
|
226
281
|
--client editor to configure (default: auto-detect)
|
|
227
282
|
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
228
283
|
--force overwrite existing config
|
|
229
|
-
--yes accept all defaults, never prompt
|
|
284
|
+
--yes accept all defaults, never prompt
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
open-memex init
|
|
288
|
+
open-memex init --client cursor --yes`,
|
|
230
289
|
|
|
231
290
|
config: `Show config, or set a key.
|
|
232
291
|
|
|
233
292
|
Usage: open-memex config [set <key> <value>]
|
|
234
293
|
|
|
235
|
-
|
|
294
|
+
Examples:
|
|
295
|
+
open-memex config
|
|
236
296
|
open-memex config set sync.autoPull false`,
|
|
237
297
|
|
|
238
298
|
capture: `Preview what the keyword-capture watcher would extract from text.
|
|
239
299
|
|
|
240
|
-
Usage: open-memex capture --dry-run "text"
|
|
300
|
+
Usage: open-memex capture --dry-run "text"
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
open-memex capture --dry-run "remember: we deploy on Fridays"`,
|
|
241
304
|
|
|
242
305
|
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
243
306
|
storage writability, then boots a real MCP server and runs initialize +
|
|
244
307
|
tools/list against it — all eleven tools must show up.
|
|
245
308
|
|
|
246
|
-
Usage: open-memex doctor
|
|
309
|
+
Usage: open-memex doctor
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
open-memex doctor`,
|
|
247
313
|
};
|
|
248
314
|
|
|
249
315
|
function usage(exitCode = 1): never {
|
|
@@ -280,6 +346,9 @@ Usage:
|
|
|
280
346
|
open-memex capture --dry-run "text"
|
|
281
347
|
open-memex doctor
|
|
282
348
|
|
|
349
|
+
Every command has its own help with description and examples:
|
|
350
|
+
open-memex <command> --help (or -h)
|
|
351
|
+
|
|
283
352
|
One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) writes
|
|
284
353
|
the MCP config for your editor (\`.vscode/mcp.json\`, \`.cursor/mcp.json\`,
|
|
285
354
|
\`opencode.jsonc\`, or Visual Studio's solution-level \`.mcp.json\`) — no copy-paste
|
|
@@ -1128,6 +1197,10 @@ function positionalArgs(argv: string[]): string[] {
|
|
|
1128
1197
|
entries.push({ key: name, files, marker });
|
|
1129
1198
|
}
|
|
1130
1199
|
entries.sort((a, b) => b.files - a.files);
|
|
1200
|
+
if (entries.length === 0) {
|
|
1201
|
+
console.log("(no scopes with memories yet — add one with `open-memex add`)");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1131
1204
|
for (const e of entries) {
|
|
1132
1205
|
console.log(` ${e.files.toString().padStart(4)} ${e.key}${e.marker}`);
|
|
1133
1206
|
}
|