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 CHANGED
@@ -1,15 +1,77 @@
1
1
  # open-memex
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/open-memex.svg)](https://www.npmjs.com/package/open-memex)
4
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./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
- **npm (recommended):**
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> # e.g. npx -y open-memex init --client vscode
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` (this release):** generic MCP server, `open-memex` bin/CLI, one-command
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
- **In progress — `0.4.0`:** team sync — shared memory via git: appdata draft
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, 1–2
411
- colleague pilot; capture — §3.5 checkpoint distillation in MCP handshake +
412
- init instructions (agent proposes 1–3 captures, human decides).
413
-
414
- **Coming — `0.3.0` (stable):** org layer — org memory repo, curator convention,
415
- distill-to-AGENTS.md assist, `export`/`import` archive for user portability
416
- (Markdown + manifest, no walled garden; private excluded by default,
417
- `-a`/`--all` for full migration).
418
-
419
- **Future (signal-gated, no version committed):** native agent plugins (Claude Code /
420
- Codex hooks as enhancement paths over the same MCP tools); local embeddings as a
421
- benchmark-gated experiment (no embedding model is ever downloaded without explicit
422
- opt-in); cloud `RemoteProvider` customization only if multi-repo sharing, ACL, or
423
- compliance needs demand it.
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
+ [![npm version](https://img.shields.io/npm/v/open-memex.svg)](https://www.npmjs.com/package/open-memex)
4
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./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
- **npm(推荐):**
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 <命令> # 例如 npx -y open-memex init --client vscode
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`(本版):** 通用 MCP server、`open-memex` bin/CLI、
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
- **进行中 —— `0.4.0`:** 团队同步——用 git 做共享记忆:appdata 草稿箱 →
513
+ **`0.4.0`(开发中):** 团队同步——用 git 做共享记忆:appdata 草稿箱 →
395
514
  `sync-status` → `submit`(本地分支+commit,push/PR 拿你的 Yes 才做)
396
- → `promote` / `resolve` 评审工作流、仓库内 `.ai/open-memex/` 目录,
397
- 找 1–2 个同事做 pilot;捕获——§3.5 检查点蒸馏写进 MCP 握手指令和
398
- init 指令文件(agent 提议 1–3 条,人来定)。
399
-
400
- **Coming —— `0.3.0`(稳定版):** 组织层——组织记忆仓库、
401
- curator 约定、distill-to-AGENTS.md 辅助、`export`/`import` 归档
402
- (Markdown + manifest,不造围墙花园,用户可带走;
403
- private 默认不导出,`-a`/`--all` 全量迁移)。
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 决策日志 D1–D20)。
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
- Example:
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.10",
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
- Example:
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
  }