open-memex 0.5.0-alpha.2 → 0.5.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -0
- package/README.md +81 -10
- package/README.zh-CN.md +70 -10
- package/dist/cli.js +30 -0
- package/dist/init.js +242 -12
- package/docs/V2-DESIGN.md +37 -0
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +44 -1
- package/src/cli.ts +32 -0
- package/src/init.ts +242 -9
package/AGENTS.md
CHANGED
|
@@ -64,6 +64,13 @@ previews keyword capture without writing, `open-memex doctor` runs health checks
|
|
|
64
64
|
(node version, config, scope resolution, storage writability, MCP handshake).
|
|
65
65
|
`open-memex init` with no --client auto-detects and wires every installed editor
|
|
66
66
|
(`--yes` skips, scripts never prompt); `open-memex config set <key> <value>` edits settings after install.
|
|
67
|
+
`open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]`
|
|
68
|
+
reverses init — removes the MCP server entry / opencode plugin line / Copilot
|
|
69
|
+
instructions section; memory data never touched (D48); no --client → auto-detect
|
|
70
|
+
with an interactive confirm, explicit --client never prompts; `--yes` only skips
|
|
71
|
+
that confirm; `--global` limits cleanup to user-level. Empty/whitespace-only
|
|
72
|
+
config files parse as `{}` and are safely populated (D49); non-JSON (JSONC)
|
|
73
|
+
files are left untouched with a printed manual snippet (D47).
|
|
67
74
|
The published `open-memex` bin points at `dist/cli.js` (compiled at publish time).
|
|
68
75
|
From a source checkout, `npm run cli` / `npm run mcp` still run `src/` directly
|
|
69
76
|
with type-stripping — no build step needed for development.
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ personal scope: this machine only — never synced, never enters a repo.
|
|
|
90
90
|
npm install -g open-memex
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
This installs the `0.4.
|
|
93
|
+
This installs the `0.4.1` stable release.
|
|
94
94
|
|
|
95
95
|
**Alpha** (bleeding edge, for testers) — the `alpha` tag:
|
|
96
96
|
|
|
@@ -114,10 +114,10 @@ npx -y open-memex <command> # e.g. npx -y open-memex init --client vscod
|
|
|
114
114
|
npx -y open-memex@alpha <command> # alpha line, no install
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
**From source** (bleeding edge, `
|
|
117
|
+
**From source** (bleeding edge, `main` branch):
|
|
118
118
|
|
|
119
119
|
```sh
|
|
120
|
-
git clone -b
|
|
120
|
+
git clone -b main https://github.com/stoneskin/open-memex.git
|
|
121
121
|
cd open-memex
|
|
122
122
|
npm install
|
|
123
123
|
node --experimental-strip-types src/cli.ts <command>
|
|
@@ -158,7 +158,7 @@ Run from your **project root** (so the project scope resolves to this repo):
|
|
|
158
158
|
|
|
159
159
|
```sh
|
|
160
160
|
open-memex init --yes
|
|
161
|
-
# …or without
|
|
161
|
+
# …or without installing the package first:
|
|
162
162
|
npx -y open-memex init --yes
|
|
163
163
|
```
|
|
164
164
|
|
|
@@ -167,16 +167,25 @@ With no `--client`, `init` **detects your installed editors and wires them all**
|
|
|
167
167
|
native plugin), so one init covers every project. Visual Studio joins in when the
|
|
168
168
|
project has a solution file. Prefer to pick a single editor? Pass `--client`:
|
|
169
169
|
|
|
170
|
+
> **Two different "globals" — don't mix them up.**
|
|
171
|
+
> - `npm install -g open-memex` installs the *package* globally: it puts the
|
|
172
|
+
> `open-memex` command on your PATH.
|
|
173
|
+
> - `init --global` writes the *editor config* at user level instead of the
|
|
174
|
+
> project: init once, the wiring works in every project. It works the same
|
|
175
|
+
> whether the package was installed globally or run via npx.
|
|
176
|
+
|
|
170
177
|
**VS Code** (Copilot):
|
|
171
178
|
|
|
172
179
|
```sh
|
|
173
180
|
open-memex init --client vscode
|
|
174
|
-
# …or without
|
|
181
|
+
# …or without installing the package first:
|
|
175
182
|
npx -y open-memex init --client vscode
|
|
176
183
|
```
|
|
177
184
|
|
|
178
|
-
|
|
179
|
-
window and confirm the `open-memex` server is started in Copilot
|
|
185
|
+
Wires the project-level `.vscode/mcp.json` and user-level Copilot instructions,
|
|
186
|
+
then reload the window and confirm the `open-memex` server is started in Copilot
|
|
187
|
+
Chat's MCP panel. On a TTY, `init` asks whether the MCP config should be
|
|
188
|
+
per-project or user-level instead of guessing; `--global` forces user-level.
|
|
180
189
|
|
|
181
190
|
**Cursor:**
|
|
182
191
|
|
|
@@ -184,7 +193,9 @@ window and confirm the `open-memex` server is started in Copilot Chat's MCP pane
|
|
|
184
193
|
open-memex init --client cursor
|
|
185
194
|
```
|
|
186
195
|
|
|
187
|
-
|
|
196
|
+
Same shape as VS Code: project-level `.cursor/mcp.json` by default, user-level
|
|
197
|
+
with `--global` (or when `init` asks on a TTY), plus user-level Copilot
|
|
198
|
+
instructions.
|
|
188
199
|
|
|
189
200
|
**One-time setup for all projects (VS Code / Cursor):**
|
|
190
201
|
|
|
@@ -198,7 +209,8 @@ Writes the server entry to the editor's *user-level* MCP config
|
|
|
198
209
|
`~/.config/Code/User/mcp.json` on Linux; `~/.cursor/mcp.json` for Cursor)
|
|
199
210
|
instead of the project — init once, the server starts in every project.
|
|
200
211
|
A per-project `.vscode/mcp.json` still wins if a project defines its own.
|
|
201
|
-
|
|
212
|
+
If the user-level file has comments in it (VS Code accepts JSONC), `init` leaves
|
|
213
|
+
the file untouched and prints the exact snippet to paste in by hand.
|
|
202
214
|
|
|
203
215
|
**opencode** (native plugin — recommended):
|
|
204
216
|
|
|
@@ -243,6 +255,17 @@ above works too.
|
|
|
243
255
|
`open-memex mcp --print-config` as a starting point (`[mcp_servers]` in
|
|
244
256
|
`config.toml`, or `codex mcp add`).
|
|
245
257
|
|
|
258
|
+
**Remove the wiring:**
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
open-memex uninstall --yes
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Reverses `init` — removes the MCP server entry, the opencode plugin line, and
|
|
265
|
+
the open-memex section of the Copilot instructions. With no `--client` it cleans
|
|
266
|
+
up every detected editor (project-level and user-level wiring); `--global`
|
|
267
|
+
limits the cleanup to user-level. Your memories are never touched.
|
|
268
|
+
|
|
246
269
|
`init` notes:
|
|
247
270
|
|
|
248
271
|
- With no `--client`, `init` auto-detects installed editors (VS Code via `code`
|
|
@@ -437,7 +460,9 @@ Settable keys: `maxProjectMemories`, `maxProfileItems`, `injectOnFirstTurn`,
|
|
|
437
460
|
Setup & health:
|
|
438
461
|
|
|
439
462
|
```sh
|
|
440
|
-
open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
463
|
+
open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
464
|
+
[--instructions personal|project] [--global] [--force] [--yes]
|
|
465
|
+
open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]
|
|
441
466
|
open-memex config # print effective config
|
|
442
467
|
open-memex config set <key> <value> # change a setting
|
|
443
468
|
open-memex doctor # environment health check
|
|
@@ -607,6 +632,52 @@ sharing, ACL, or compliance needs demand it.
|
|
|
607
632
|
|
|
608
633
|
Design details: [docs/V2-DESIGN.md](./docs/V2-DESIGN.md) (append-only decision log).
|
|
609
634
|
|
|
635
|
+
## FAQ
|
|
636
|
+
|
|
637
|
+
**Do I need to initialize open-memex for each project after installing?**
|
|
638
|
+
Two layers. The data layer needs nothing — there is no per-project init:
|
|
639
|
+
the data dir is created on demand and the project scope is derived
|
|
640
|
+
automatically from your cwd's git remote or path, so memories are
|
|
641
|
+
namespaced per project with zero setup. The editor wiring takes one step:
|
|
642
|
+
run `open-memex init` with no arguments and it auto-detects every editor
|
|
643
|
+
you have installed (VS Code, Cursor, opencode — plus Visual Studio when the
|
|
644
|
+
project has a `.sln`) and wires them all, user-level wherever the editor
|
|
645
|
+
supports it, so one init covers every project. Prefer a single editor?
|
|
646
|
+
`open-memex init --client <vscode|cursor|opencode|visualstudio>`. Prefer to
|
|
647
|
+
force user-level for VS Code / Cursor? Add `--global`. The Copilot memory
|
|
648
|
+
instructions default to user-level (`~/.copilot/`), which is global.
|
|
649
|
+
|
|
650
|
+
**Does opencode need `init`?**
|
|
651
|
+
Two paths. Recommended: `open-memex init --client opencode --global` —
|
|
652
|
+
it merges the native open-memex plugin into
|
|
653
|
+
`~/.config/opencode/opencode.json` for you. One-time setup, applies to all
|
|
654
|
+
projects, and additionally enables keyword auto-capture and first-turn
|
|
655
|
+
memory injection. Prefer to do it by hand? Add
|
|
656
|
+
`"plugin": ["file:///absolute/path/to/open-memex/src/index.ts"]`
|
|
657
|
+
(the installed package's path) to that file instead. As a plain MCP
|
|
658
|
+
consumer: `open-memex init --client opencode` writes a project-level
|
|
659
|
+
`opencode.jsonc` (no hooks). If your user-level config has comments, `init`
|
|
660
|
+
leaves it untouched and prints the manual step.
|
|
661
|
+
|
|
662
|
+
**VS Code — run `init` once, or per project?**
|
|
663
|
+
Once. Plain `open-memex init` auto-detects VS Code and writes the MCP server
|
|
664
|
+
entry to VS Code's user-level `mcp.json`
|
|
665
|
+
(`%APPDATA%/Code/User/mcp.json` on Windows,
|
|
666
|
+
`~/Library/Application Support/Code/User/mcp.json` on macOS,
|
|
667
|
+
`~/.config/Code/User/mcp.json` on Linux), so the server starts in every
|
|
668
|
+
project. A per-project `.vscode/mcp.json` still wins when present, and the
|
|
669
|
+
entry keeps `cwd=${workspaceFolder}` so project-scope resolution keeps
|
|
670
|
+
working per window. If your user-level `mcp.json` has comments (VS Code
|
|
671
|
+
accepts JSONC), `init` leaves it alone and prints the exact snippet to add
|
|
672
|
+
by hand.
|
|
673
|
+
|
|
674
|
+
**How do I remove the editor wiring?**
|
|
675
|
+
`open-memex uninstall` reverses `init`: it removes the MCP server entry,
|
|
676
|
+
the opencode plugin line, and the open-memex section of the Copilot
|
|
677
|
+
instructions. With no `--client` it cleans up every detected editor;
|
|
678
|
+
`--global` limits the cleanup to user-level wiring. Your memories are
|
|
679
|
+
never touched.
|
|
680
|
+
|
|
610
681
|
## License
|
|
611
682
|
|
|
612
683
|
[Apache-2.0](./LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -87,7 +87,7 @@ personal scope:只属于这台机器——永不同步,永远进不了仓库
|
|
|
87
87
|
npm install -g open-memex
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
安装的是 `0.4.
|
|
90
|
+
安装的是 `0.4.1` 正式版。
|
|
91
91
|
|
|
92
92
|
**Alpha 版**(最新开发版,给测试者)——`alpha` 标签:
|
|
93
93
|
|
|
@@ -111,10 +111,10 @@ npx -y open-memex <命令> # 例如 npx -y open-memex init --client vsc
|
|
|
111
111
|
npx -y open-memex@alpha <命令> # alpha 线,免安装
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
**从源码安装**(最新开发版,`
|
|
114
|
+
**从源码安装**(最新开发版,`main` 分支):
|
|
115
115
|
|
|
116
116
|
```sh
|
|
117
|
-
git clone -b
|
|
117
|
+
git clone -b main https://github.com/stoneskin/open-memex.git
|
|
118
118
|
cd open-memex
|
|
119
119
|
npm install
|
|
120
120
|
node --experimental-strip-types src/cli.ts <命令>
|
|
@@ -153,7 +153,7 @@ Windows 下被进程加载的 DLL 是锁死的:如果 open-memex MCP server
|
|
|
153
153
|
|
|
154
154
|
```sh
|
|
155
155
|
open-memex init --yes
|
|
156
|
-
#
|
|
156
|
+
# ……没先装包的话:
|
|
157
157
|
npx -y open-memex init --yes
|
|
158
158
|
```
|
|
159
159
|
|
|
@@ -162,16 +162,24 @@ npx -y open-memex init --yes
|
|
|
162
162
|
一次 init,所有项目通用;项目里有 solution 文件时 Visual Studio 也会一起配。
|
|
163
163
|
想只配某一个编辑器?加 `--client`:
|
|
164
164
|
|
|
165
|
+
> **两个"全局"不是一回事,别搞混。**
|
|
166
|
+
> - `npm install -g open-memex` 是把*包*装到全局:让 `open-memex` 命令出现在
|
|
167
|
+
> 你的 PATH 里。
|
|
168
|
+
> - `init --global` 是把*编辑器配置*写到用户级而不是项目里:init 一次,
|
|
169
|
+
> 每个项目都生效。不管包是全局安装的还是用 npx 临时跑的,效果一样。
|
|
170
|
+
|
|
165
171
|
**VS Code**(Copilot):
|
|
166
172
|
|
|
167
173
|
```sh
|
|
168
174
|
open-memex init --client vscode
|
|
169
|
-
#
|
|
175
|
+
# ……没先装包的话:
|
|
170
176
|
npx -y open-memex init --client vscode
|
|
171
177
|
```
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
在 Copilot Chat 的 MCP 面板里确认 `open-memex` server
|
|
179
|
+
配项目级 `.vscode/mcp.json` 和用户级 Copilot instructions,然后重新加载窗口,
|
|
180
|
+
在 Copilot Chat 的 MCP 面板里确认 `open-memex` server 已启动。终端交互模式下
|
|
181
|
+
`init` 会问你要配到项目级还是用户级,而不是替你猜;`--global` 直接强制
|
|
182
|
+
用户级。
|
|
175
183
|
|
|
176
184
|
**Cursor:**
|
|
177
185
|
|
|
@@ -179,7 +187,9 @@ npx -y open-memex init --client vscode
|
|
|
179
187
|
open-memex init --client cursor
|
|
180
188
|
```
|
|
181
189
|
|
|
182
|
-
|
|
190
|
+
和 VS Code 一个套路:默认写项目级 `.cursor/mcp.json`,`--global`
|
|
191
|
+
(或终端里 `init` 问你时选用户级)就写用户级,外加用户级 Copilot
|
|
192
|
+
instructions。
|
|
183
193
|
|
|
184
194
|
**一次配置、所有项目通用(VS Code / Cursor):**
|
|
185
195
|
|
|
@@ -193,7 +203,8 @@ open-memex init --client vscode --global --yes
|
|
|
193
203
|
`~/.config/Code/User/mcp.json`;Cursor 是 `~/.cursor/mcp.json`),
|
|
194
204
|
而不是写到项目里——init 一次,每个项目打开自动启动 server。
|
|
195
205
|
如果某个项目自己定义了 `.vscode/mcp.json`,项目级的优先。
|
|
196
|
-
|
|
206
|
+
如果用户级文件里带注释(VS Code 接受 JSONC),`init` 不会碰这个文件,
|
|
207
|
+
只打印可直接手贴的配置片段。
|
|
197
208
|
|
|
198
209
|
**opencode**(原生插件——推荐):
|
|
199
210
|
|
|
@@ -236,6 +247,16 @@ Visual Studio 也会自动发现 `.vscode/mcp.json` 和 `.cursor/mcp.json`,
|
|
|
236
247
|
**Codex:** 暂无 `init` 客户端——以 `open-memex mcp --print-config` 为起点手动添加
|
|
237
248
|
(`config.toml` 的 `[mcp_servers]`,或 `codex mcp add`)。
|
|
238
249
|
|
|
250
|
+
**拆掉接线:**
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
open-memex uninstall --yes
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`init` 的逆操作——删掉 MCP server 条目、opencode 插件行和 Copilot
|
|
257
|
+
instructions 里的 open-memex 段。不带 `--client` 时把检测到的编辑器全清掉
|
|
258
|
+
(项目级和用户级都清);`--global` 只清用户级。你的记忆数据永远不会被碰。
|
|
259
|
+
|
|
239
260
|
`init` 说明:
|
|
240
261
|
|
|
241
262
|
- Copilot 记忆 instructions 默认写到**用户级**
|
|
@@ -416,7 +437,9 @@ open-memex config set maxProjectMemories 12
|
|
|
416
437
|
安装与健康检查:
|
|
417
438
|
|
|
418
439
|
```sh
|
|
419
|
-
open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
440
|
+
open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
441
|
+
[--instructions personal|project] [--global] [--force] [--yes]
|
|
442
|
+
open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]
|
|
420
443
|
open-memex config # 打印生效配置
|
|
421
444
|
open-memex config set <key> <value> # 改设置
|
|
422
445
|
open-memex doctor # 环境健康检查
|
|
@@ -572,6 +595,43 @@ ACL 或合规需求出现时才做。
|
|
|
572
595
|
|
|
573
596
|
设计细节:[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)(append-only 决策日志)。
|
|
574
597
|
|
|
598
|
+
## 常见问题(FAQ)
|
|
599
|
+
|
|
600
|
+
**装完之后,每个项目都要初始化 open-memex 吗(像其他 app 那样)?**
|
|
601
|
+
分两层。数据层不需要——没有 per-project 初始化的概念:数据目录按需自动创建,
|
|
602
|
+
project scope 从当前目录的 git remote 或路径自动推导,记忆天然按项目隔离,
|
|
603
|
+
零配置。编辑器接线只需要一步:直接跑 `open-memex init`(不带参数),它会自动
|
|
604
|
+
检测你装好的编辑器(VS Code、Cursor、opencode;项目里有 `.sln` 时还有 Visual
|
|
605
|
+
Studio)并一次全接上——支持用户级配置的编辑器就写用户级,一次 init 所有项目
|
|
606
|
+
通用。只想接某一个编辑器?用
|
|
607
|
+
`open-memex init --client <vscode|cursor|opencode|visualstudio>`。
|
|
608
|
+
想给 VS Code / Cursor 强制写用户级?加 `--global`。
|
|
609
|
+
Copilot 记忆指令默认写用户级(`~/.copilot/`),那个是全局的。
|
|
610
|
+
|
|
611
|
+
**opencode 需要跑 `init` 吗?**
|
|
612
|
+
两条路。推荐:`open-memex init --client opencode --global`——自动把原生插件
|
|
613
|
+
合并进 `~/.config/opencode/opencode.json`。一次配置,所有项目生效,还多拿
|
|
614
|
+
关键词自动捕获和首轮记忆注入。想手写?往那个文件里加
|
|
615
|
+
`"plugin": ["file:///absolute/path/to/open-memex/src/index.ts"]`
|
|
616
|
+
(填 open-memex 的实际安装路径)就行。当纯 MCP 用:
|
|
617
|
+
`open-memex init --client opencode` 写项目级 `opencode.jsonc`(无 hooks)。
|
|
618
|
+
如果你的用户级配置里带注释,`init` 不会碰它,只打印手动步骤。
|
|
619
|
+
|
|
620
|
+
**VS Code 呢——跑一次就行,还是每个项目都要跑?**
|
|
621
|
+
跑一次就行。直接 `open-memex init` 会自动检测到 VS Code,把 MCP server 写进
|
|
622
|
+
VS Code 的用户级 `mcp.json`(Windows:`%APPDATA%/Code/User/mcp.json`;
|
|
623
|
+
macOS:`~/Library/Application Support/Code/User/mcp.json`;
|
|
624
|
+
Linux:`~/.config/Code/User/mcp.json`),每个项目打开 server 都在。
|
|
625
|
+
项目里如果有 `.vscode/mcp.json` 仍然优先;entry 里保留了
|
|
626
|
+
`cwd=${workspaceFolder}`,project scope 按窗口照常工作。
|
|
627
|
+
如果你的用户级 `mcp.json` 带注释(VS Code 接受 JSONC),`init` 不会碰它,
|
|
628
|
+
只打印可直接手贴的配置片段。
|
|
629
|
+
|
|
630
|
+
**怎么拆掉编辑器接线?**
|
|
631
|
+
`open-memex uninstall` 就是 `init` 的逆操作:删掉 MCP server 条目、opencode
|
|
632
|
+
插件行和 Copilot instructions 里的 open-memex 段。不带 `--client` 时把检测
|
|
633
|
+
到的编辑器全清掉;`--global` 只清用户级。你的记忆数据永远不会被碰。
|
|
634
|
+
|
|
575
635
|
## 许可证
|
|
576
636
|
|
|
577
637
|
[Apache-2.0](./LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -266,6 +266,22 @@ Examples:
|
|
|
266
266
|
open-memex init
|
|
267
267
|
open-memex init --client vscode --global --yes
|
|
268
268
|
open-memex init --client cursor --yes`,
|
|
269
|
+
uninstall: `Remove the editor wiring that \`init\` wrote: the MCP server entry,
|
|
270
|
+
the opencode native plugin line, and the Copilot instructions section.
|
|
271
|
+
Your memories are never touched.
|
|
272
|
+
|
|
273
|
+
Usage: open-memex uninstall [--client vscode|cursor|opencode|visualstudio]
|
|
274
|
+
[--global] [--yes]
|
|
275
|
+
|
|
276
|
+
Flags:
|
|
277
|
+
--client editor to unwire (default: auto-detect all installed editors)
|
|
278
|
+
--global only the user-level config; without it, both project-level and
|
|
279
|
+
user-level wiring are removed (init may have written either)
|
|
280
|
+
--yes accept all defaults, never prompt
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
open-memex uninstall
|
|
284
|
+
open-memex uninstall --client vscode --yes`,
|
|
269
285
|
config: `Show config, or set a key.
|
|
270
286
|
|
|
271
287
|
Usage: open-memex config [set <key> <value>]
|
|
@@ -318,6 +334,8 @@ Usage:
|
|
|
318
334
|
open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
319
335
|
open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
320
336
|
[--instructions personal|project] [--force] [--yes]
|
|
337
|
+
open-memex uninstall [--client vscode|cursor|opencode|visualstudio]
|
|
338
|
+
[--global] [--yes]
|
|
321
339
|
open-memex config [set <key> <value>]
|
|
322
340
|
open-memex capture --dry-run "text"
|
|
323
341
|
open-memex doctor
|
|
@@ -547,6 +565,18 @@ async function main() {
|
|
|
547
565
|
});
|
|
548
566
|
return;
|
|
549
567
|
}
|
|
568
|
+
// `uninstall` reverses `init` — removes the editor wiring; never touches data.
|
|
569
|
+
// No DB needed (pure file operation, like init).
|
|
570
|
+
if (cmd === "uninstall") {
|
|
571
|
+
const flags = parseFlags(rest);
|
|
572
|
+
const { uninstallProject } = await import("./init.js");
|
|
573
|
+
await uninstallProject({
|
|
574
|
+
client: flags["client"],
|
|
575
|
+
global: flags["global"] === "true",
|
|
576
|
+
yes: flags["yes"] === "true",
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
550
580
|
// `doctor` runs environment health checks — no DB needed (it self-contains).
|
|
551
581
|
if (cmd === "doctor") {
|
|
552
582
|
const { runDoctor } = await import("./doctor.js");
|
package/dist/init.js
CHANGED
|
@@ -160,6 +160,39 @@ export function mergeServerEntry(doc, sectionKey, entry, force) {
|
|
|
160
160
|
section["open-memex"] = entry;
|
|
161
161
|
return "added";
|
|
162
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* D46-followup: when a config file isn't valid JSON we leave it untouched —
|
|
165
|
+
* but "fix it manually" alone isn't helpful. Print the exact snippet the
|
|
166
|
+
* user needs to add by hand (same idea as the opencode --global hint).
|
|
167
|
+
*/
|
|
168
|
+
export function printManualEntryHint(sectionKey, entry) {
|
|
169
|
+
console.error(` Add this under "${sectionKey}":`);
|
|
170
|
+
const pretty = JSON.stringify({ "open-memex": entry }, null, 2).replace(/\n/g, "\n ");
|
|
171
|
+
console.error(` ${pretty}`);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Parse a JSON config file's raw text for merging. An empty or
|
|
175
|
+
* whitespace-only file counts as `{}` — there is nothing to preserve, so
|
|
176
|
+
* writers can safely populate it (an empty mcp.json is a normal first-run
|
|
177
|
+
* state, e.g. created by the editor, not a corrupt file). Returns null when
|
|
178
|
+
* the content is genuinely unparseable (e.g. JSONC comments) or not an
|
|
179
|
+
* object, in which case the caller leaves the file untouched and prints the
|
|
180
|
+
* manual step.
|
|
181
|
+
*/
|
|
182
|
+
export function parseJsonConfig(raw) {
|
|
183
|
+
if (raw.trim() === "")
|
|
184
|
+
return {};
|
|
185
|
+
let doc;
|
|
186
|
+
try {
|
|
187
|
+
doc = JSON.parse(raw);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc))
|
|
193
|
+
return null;
|
|
194
|
+
return doc;
|
|
195
|
+
}
|
|
163
196
|
/**
|
|
164
197
|
* Read (or create) a JSON config file, merge the open-memex server entry, write
|
|
165
198
|
* it back. Existing files are merged, never clobbered; invalid JSON is left
|
|
@@ -169,13 +202,13 @@ export function mergeServerEntry(doc, sectionKey, entry, force) {
|
|
|
169
202
|
function writeServerEntryFile(file, sectionKey, entry, force, mkdir) {
|
|
170
203
|
let doc = {};
|
|
171
204
|
if (fs.existsSync(file)) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
205
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
206
|
+
if (parsed === null) {
|
|
176
207
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
208
|
+
printManualEntryHint(sectionKey, entry);
|
|
177
209
|
return null;
|
|
178
210
|
}
|
|
211
|
+
doc = parsed;
|
|
179
212
|
}
|
|
180
213
|
const merged = mergeServerEntry(doc, sectionKey, entry, force);
|
|
181
214
|
if (merged === "kept") {
|
|
@@ -271,13 +304,18 @@ function writeOpencodeMcpJson(root, force) {
|
|
|
271
304
|
const file = path.join(root, "opencode.jsonc");
|
|
272
305
|
let doc = {};
|
|
273
306
|
if (fs.existsSync(file)) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
307
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
308
|
+
if (parsed === null) {
|
|
278
309
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
310
|
+
const mc = resolveMcpCommand();
|
|
311
|
+
printManualEntryHint("mcp", {
|
|
312
|
+
type: "local",
|
|
313
|
+
command: [mc.command, ...mc.args],
|
|
314
|
+
enabled: true,
|
|
315
|
+
});
|
|
279
316
|
return null;
|
|
280
317
|
}
|
|
318
|
+
doc = parsed;
|
|
281
319
|
}
|
|
282
320
|
const section = (doc["mcp"] ??= {});
|
|
283
321
|
if (section["open-memex"] && !force) {
|
|
@@ -306,11 +344,15 @@ function writeVisualStudioMcpJson(root, force) {
|
|
|
306
344
|
const file = path.join(root, ".mcp.json");
|
|
307
345
|
let doc = {};
|
|
308
346
|
if (fs.existsSync(file)) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
347
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
348
|
+
if (parsed === null) {
|
|
313
349
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
350
|
+
const mc = resolveMcpCommand();
|
|
351
|
+
printManualEntryHint("servers", {
|
|
352
|
+
type: "stdio",
|
|
353
|
+
command: mc.command,
|
|
354
|
+
args: mc.args,
|
|
355
|
+
});
|
|
314
356
|
return null;
|
|
315
357
|
}
|
|
316
358
|
}
|
|
@@ -615,3 +657,191 @@ export async function initProject(opts) {
|
|
|
615
657
|
}
|
|
616
658
|
console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
|
|
617
659
|
}
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
// uninstall — the reverse of init (D48). Removes the editor wiring init wrote:
|
|
662
|
+
// the MCP server entry, the opencode native plugin line, and the Copilot
|
|
663
|
+
// instructions section. Memory data is never touched.
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
/**
|
|
666
|
+
* Pure removal of the open-memex server entry from a parsed config doc.
|
|
667
|
+
* Prunes the section when it becomes empty. Mutates `doc`.
|
|
668
|
+
*/
|
|
669
|
+
export function removeServerEntry(doc, sectionKey) {
|
|
670
|
+
const section = doc[sectionKey];
|
|
671
|
+
if (!section || typeof section !== "object" || !("open-memex" in section))
|
|
672
|
+
return "absent";
|
|
673
|
+
delete section["open-memex"];
|
|
674
|
+
if (Object.keys(section).length === 0)
|
|
675
|
+
delete doc[sectionKey];
|
|
676
|
+
return "removed";
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Pure removal of open-memex plugin entries from a parsed opencode config doc.
|
|
680
|
+
* Matches any plugin URL containing `match` (robust against the package moving
|
|
681
|
+
* between installs). Prunes the `plugin` array when it becomes empty.
|
|
682
|
+
* Mutates `doc`.
|
|
683
|
+
*/
|
|
684
|
+
export function removePluginEntry(doc, match) {
|
|
685
|
+
const plugins = doc["plugin"];
|
|
686
|
+
if (!Array.isArray(plugins))
|
|
687
|
+
return "absent";
|
|
688
|
+
const kept = plugins.filter((p) => !(typeof p === "string" && p.includes(match)));
|
|
689
|
+
if (kept.length === plugins.length)
|
|
690
|
+
return "absent";
|
|
691
|
+
if (kept.length === 0)
|
|
692
|
+
delete doc["plugin"];
|
|
693
|
+
else
|
|
694
|
+
doc["plugin"] = kept;
|
|
695
|
+
return "removed";
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Pure removal of the open-memex instructions section from a
|
|
699
|
+
* copilot-instructions.md body. init always appends it last (MARKER..end),
|
|
700
|
+
* so cutting from the marker to the end is exact; returns "" when nothing
|
|
701
|
+
* but the section remains so the caller can delete the file.
|
|
702
|
+
*/
|
|
703
|
+
export function removeInstructionsSection(text) {
|
|
704
|
+
const idx = text.lastIndexOf(MARKER);
|
|
705
|
+
if (idx < 0)
|
|
706
|
+
return text;
|
|
707
|
+
const rest = text.slice(0, idx).replace(/\s+$/, "");
|
|
708
|
+
return rest ? rest + "\n" : "";
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Remove the server entry from one JSON config file. Existing files are
|
|
712
|
+
* merged, never clobbered; invalid JSON is left untouched with the manual
|
|
713
|
+
* step (same D47 treatment as the init write path).
|
|
714
|
+
*/
|
|
715
|
+
function removeServerEntryFile(file, sectionKey) {
|
|
716
|
+
if (!fs.existsSync(file))
|
|
717
|
+
return "absent";
|
|
718
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
719
|
+
if (doc === null) {
|
|
720
|
+
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
721
|
+
console.error(` Delete the "open-memex" key under "${sectionKey}".`);
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
const res = removeServerEntry(doc, sectionKey);
|
|
725
|
+
if (res === "removed") {
|
|
726
|
+
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
727
|
+
console.log(` - ${file} (open-memex entry removed)`);
|
|
728
|
+
}
|
|
729
|
+
return res;
|
|
730
|
+
}
|
|
731
|
+
/** Remove the open-memex plugin URL from one opencode user-level config file. */
|
|
732
|
+
function removeOpencodePluginFile(file) {
|
|
733
|
+
if (!fs.existsSync(file))
|
|
734
|
+
return "absent";
|
|
735
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
736
|
+
if (doc === null) {
|
|
737
|
+
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
738
|
+
console.error(` Delete the open-memex URL from the "plugin" array.`);
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const res = removePluginEntry(doc, "open-memex");
|
|
742
|
+
if (res === "removed") {
|
|
743
|
+
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
744
|
+
console.log(` - ${file} (open-memex plugin removed)`);
|
|
745
|
+
}
|
|
746
|
+
return res;
|
|
747
|
+
}
|
|
748
|
+
/** Remove the open-memex section from one copilot-instructions.md file. */
|
|
749
|
+
function removeInstructionsFile(file) {
|
|
750
|
+
if (!fs.existsSync(file))
|
|
751
|
+
return "absent";
|
|
752
|
+
const cur = fs.readFileSync(file, "utf8");
|
|
753
|
+
if (!cur.includes(MARKER))
|
|
754
|
+
return "absent";
|
|
755
|
+
const next = removeInstructionsSection(cur);
|
|
756
|
+
if (next === "") {
|
|
757
|
+
fs.unlinkSync(file);
|
|
758
|
+
console.log(` - ${file} (deleted — it only held open-memex instructions)`);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
fs.writeFileSync(file, next);
|
|
762
|
+
console.log(` - ${file} (open-memex instructions removed)`);
|
|
763
|
+
}
|
|
764
|
+
return "removed";
|
|
765
|
+
}
|
|
766
|
+
export async function uninstallProject(opts) {
|
|
767
|
+
const interactive = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
768
|
+
const explicit = normalizeClient(opts.client ?? "");
|
|
769
|
+
if (explicit && !INIT_CLIENTS.includes(explicit)) {
|
|
770
|
+
console.error(`unknown client "${opts.client}" (${INIT_CLIENTS.join("|")})`);
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
// Mirror init's client resolution: explicit --client, else auto-detect.
|
|
774
|
+
let clients;
|
|
775
|
+
if (explicit) {
|
|
776
|
+
clients = [explicit];
|
|
777
|
+
}
|
|
778
|
+
else if (interactive) {
|
|
779
|
+
const detected = detectInstalledClients();
|
|
780
|
+
if (detected.length === 0) {
|
|
781
|
+
console.log(" - no supported editors detected — nothing to remove");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
console.log(`Detected editors: ${detected.join(", ")}`);
|
|
785
|
+
const all = await askBool("Remove open-memex wiring from all of them?", true);
|
|
786
|
+
if (all) {
|
|
787
|
+
clients = detected;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
const one = await promptClient();
|
|
791
|
+
clients = one ? [one] : [];
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
clients = detectInstalledClients();
|
|
796
|
+
if (clients.length > 0) {
|
|
797
|
+
console.log(`Detected editors: ${clients.join(", ")} — removing from all (use --client to pick one)`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (clients.length === 0) {
|
|
801
|
+
console.log(" - nothing to remove");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
// Without --global, clean both levels: init may have written either one,
|
|
805
|
+
// and a leftover entry at the other level would be a surprise.
|
|
806
|
+
const levels = opts.global ? ["user"] : ["project", "user"];
|
|
807
|
+
const root = projectRoot();
|
|
808
|
+
console.log(`open-memex uninstall — project root: ${root}`);
|
|
809
|
+
let changed = 0;
|
|
810
|
+
const bump = (r) => {
|
|
811
|
+
if (r === "removed")
|
|
812
|
+
changed++;
|
|
813
|
+
};
|
|
814
|
+
for (const client of clients) {
|
|
815
|
+
if (client === "vscode" || client === "cursor") {
|
|
816
|
+
const sectionKey = client === "cursor" ? "mcpServers" : "servers";
|
|
817
|
+
for (const level of levels) {
|
|
818
|
+
const file = level === "user"
|
|
819
|
+
? userMcpConfigPath(client)
|
|
820
|
+
: path.join(root, client === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json");
|
|
821
|
+
bump(removeServerEntryFile(file, sectionKey));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else if (client === "opencode") {
|
|
825
|
+
for (const level of levels) {
|
|
826
|
+
if (level === "user")
|
|
827
|
+
bump(removeOpencodePluginFile(opencodeGlobalConfigPath()));
|
|
828
|
+
else
|
|
829
|
+
bump(removeServerEntryFile(path.join(root, "opencode.jsonc"), "mcp"));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (client === "visualstudio") {
|
|
833
|
+
// Solution-level only — --global is meaningless, same as init.
|
|
834
|
+
bump(removeServerEntryFile(path.join(root, ".mcp.json"), "servers"));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// Copilot instructions: init may have written personal (default) or project.
|
|
838
|
+
if (clients.some((c) => c !== "opencode")) {
|
|
839
|
+
bump(removeInstructionsFile(path.join(os.homedir(), ".copilot", "copilot-instructions.md")));
|
|
840
|
+
bump(removeInstructionsFile(path.join(os.homedir(), "copilot-instructions.md")));
|
|
841
|
+
bump(removeInstructionsFile(path.join(root, ".github", "copilot-instructions.md")));
|
|
842
|
+
}
|
|
843
|
+
console.log(changed > 0
|
|
844
|
+
? `\nDone. Removed open-memex wiring from ${changed} file(s).`
|
|
845
|
+
: "\nDone. Nothing to remove — no open-memex wiring found.");
|
|
846
|
+
console.log("Your memories are untouched (uninstall never deletes data).");
|
|
847
|
+
}
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -930,6 +930,43 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
930
930
|
should not have to learn `--client` to get started. The help text already
|
|
931
931
|
promised "default: auto-detect"; D46 makes the code keep that promise.
|
|
932
932
|
Approved 2026-09-29.*
|
|
933
|
+
- **D47** — when `init` refuses to touch a config file that isn't valid JSON
|
|
934
|
+
(usually JSONC with comments — VS Code / Cursor / opencode all accept them),
|
|
935
|
+
it now prints the exact snippet to add by hand: the section key plus the
|
|
936
|
+
`"open-memex"` entry, pretty-printed. Covers all three MCP-writing paths —
|
|
937
|
+
vscode/cursor (user + project level), opencode project-level `opencode.jsonc`,
|
|
938
|
+
and Visual Studio `.mcp.json` — matching the manual hint the opencode
|
|
939
|
+
`--global` plugin path already printed (D46). The file is still never
|
|
940
|
+
rewritten; the hint just makes "fix it manually" actionable. Triggered by
|
|
941
|
+
Stone's real Windows run: his user-level `mcp.json` had comments, init
|
|
942
|
+
correctly left it alone, but the old message gave him nothing to paste.
|
|
943
|
+
*Rationale: a refusal without a remedy is a dead end; the entry shape is
|
|
944
|
+
already computed, so printing it costs nothing. Approved 2026-09-29.*
|
|
945
|
+
- **D48** — new `open-memex uninstall` command, the reverse of `init`
|
|
946
|
+
(0.5.0-alpha.4). Removes the MCP server entry / opencode native plugin line /
|
|
947
|
+
Copilot instructions section that `init` wrote; memory data is never touched.
|
|
948
|
+
Client resolution mirrors `init` (explicit `--client`, else auto-detect with
|
|
949
|
+
an interactive confirm). Without `--global` it cleans both project-level and
|
|
950
|
+
user-level files — init may have written either, and a leftover entry at the
|
|
951
|
+
other level would be a surprise; `--global` restricts to user-level. Empty
|
|
952
|
+
sections/arrays are pruned; an instructions file that only held the
|
|
953
|
+
open-memex section is deleted. Non-JSON (JSONC) configs are left untouched
|
|
954
|
+
with the manual step, same D47 treatment as the init write path.
|
|
955
|
+
*Rationale: Stone asked "有 uninstall 吗" while testing init on Windows —
|
|
956
|
+
every write deserves an undo. Approved 2026-09-29.*
|
|
957
|
+
- **D49** — empty config files are no longer treated as corrupt (0.5.0-alpha.5).
|
|
958
|
+
Stone's real Windows run: his user-level `mcp.json` existed but was empty,
|
|
959
|
+
and `init` refused it with "not valid JSON — left untouched", because
|
|
960
|
+
`JSON.parse("")` throws. New `parseJsonConfig` helper: empty or
|
|
961
|
+
whitespace-only content parses as `{}` — there is nothing to preserve, so
|
|
962
|
+
writers safely populate it; genuinely unparseable content (JSONC comments)
|
|
963
|
+
or non-objects still return null and keep the D47 leave-untouched + manual
|
|
964
|
+
hint behavior. Applied to all five config file touch points: the three init
|
|
965
|
+
writers (vscode/cursor MCP, opencode `opencode.jsonc`, Visual Studio
|
|
966
|
+
`.mcp.json`) and the two uninstall removers (empty file = nothing to remove).
|
|
967
|
+
*Rationale: an empty file is the safest write target, not a corrupt file;
|
|
968
|
+
refusing it sent the user down a manual path for no reason. Triggered by
|
|
969
|
+
Stone's report 2026-09-29.*
|
|
933
970
|
|
|
934
971
|
## Open Questions
|
|
935
972
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-memex",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.5",
|
|
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",
|
package/scripts/smoke-pure.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { DEFAULT_CONFIG } from "../src/config.ts";
|
|
|
8
8
|
import { resolveProjectScope, resolveCwdScope, PERSONAL_SCOPE } from "../src/scope.ts";
|
|
9
9
|
import { cjkIndexText, cjkQueryExpr, hasCjk } from "../src/retrieve/cjk.ts";
|
|
10
10
|
import { contentHash, similarity, NEAR_DUP_THRESHOLD } from "../src/store/lifecycle.ts";
|
|
11
|
-
import { userMcpConfigPath, mergeServerEntry, detectInstalledClients, mergePluginEntry, opencodeGlobalConfigPath } from "../src/init.ts";
|
|
11
|
+
import { userMcpConfigPath, mergeServerEntry, detectInstalledClients, mergePluginEntry, opencodeGlobalConfigPath, printManualEntryHint, parseJsonConfig, removeServerEntry, removePluginEntry, removeInstructionsSection } from "../src/init.ts";
|
|
12
12
|
|
|
13
13
|
let fails = 0;
|
|
14
14
|
function ok(name: string, cond: boolean, info?: unknown) {
|
|
@@ -422,6 +422,49 @@ ok("plugin merge force on dup adds nothing twice", mergePluginEntry(pdoc, "file:
|
|
|
422
422
|
const pdoc2: Record<string, unknown> = { plugin: ["file:///other"], theme: "dark" };
|
|
423
423
|
ok("plugin merge preserves siblings", mergePluginEntry(pdoc2, "file:///x", false) === "added"
|
|
424
424
|
&& (pdoc2["plugin"] as unknown[]).length === 2 && pdoc2["theme"] === "dark");
|
|
425
|
+
// printManualEntryHint: the invalid-JSON bail-out must show the exact snippet to add by hand.
|
|
426
|
+
const hintLines: string[] = [];
|
|
427
|
+
const origErr = console.error;
|
|
428
|
+
console.error = (msg?: unknown) => { hintLines.push(String(msg)); };
|
|
429
|
+
printManualEntryHint("servers", { type: "stdio", command: "open-memex", args: ["mcp"] });
|
|
430
|
+
console.error = origErr;
|
|
431
|
+
const hintText = hintLines.join("\n");
|
|
432
|
+
ok("manual hint names the section", hintText.includes('"servers"'));
|
|
433
|
+
ok("manual hint contains the entry", hintText.includes('"open-memex"') && hintText.includes("stdio"));
|
|
434
|
+
// parseJsonConfig: empty/whitespace-only files are empty docs (safe to populate);
|
|
435
|
+
// genuinely unparseable content (JSONC comments) or non-objects yield null.
|
|
436
|
+
ok("empty file → {}", JSON.stringify(parseJsonConfig("")) === "{}");
|
|
437
|
+
ok("whitespace-only file → {}", JSON.stringify(parseJsonConfig(" \n\t ")) === "{}");
|
|
438
|
+
ok("valid JSON parses", (parseJsonConfig('{"servers":{}}') as Record<string, unknown>)["servers"] !== undefined);
|
|
439
|
+
ok("JSONC comments → null", parseJsonConfig('{\n // a comment\n}') === null);
|
|
440
|
+
ok("trailing garbage → null", parseJsonConfig('{} trailing') === null);
|
|
441
|
+
ok("array → null", parseJsonConfig("[1,2]") === null);
|
|
442
|
+
ok("scalar → null", parseJsonConfig("42") === null);
|
|
443
|
+
// removeServerEntry: pure removal semantics.
|
|
444
|
+
const rdoc: Record<string, unknown> = { servers: { "open-memex": { command: "x" }, other: { command: "y" } }, untouched: 1 };
|
|
445
|
+
ok("remove deletes the entry", removeServerEntry(rdoc, "servers") === "removed"
|
|
446
|
+
&& !("open-memex" in (rdoc["servers"] as Record<string, unknown>))
|
|
447
|
+
&& "other" in (rdoc["servers"] as Record<string, unknown>));
|
|
448
|
+
ok("remove absent when entry already gone", removeServerEntry(rdoc, "servers") === "absent"
|
|
449
|
+
&& "servers" in rdoc); // sibling kept, section kept
|
|
450
|
+
const rdoc2: Record<string, unknown> = { servers: { "open-memex": { command: "x" } } };
|
|
451
|
+
ok("remove prunes empty section", removeServerEntry(rdoc2, "servers") === "removed" && !("servers" in rdoc2));
|
|
452
|
+
ok("remove absent when no entry", removeServerEntry({ servers: {} }, "servers") === "absent");
|
|
453
|
+
ok("remove absent when no section", removeServerEntry({}, "servers") === "absent");
|
|
454
|
+
// removePluginEntry: removes any open-memex URL, prunes empty array.
|
|
455
|
+
const rpdoc: Record<string, unknown> = { plugin: ["file:///x/open-memex/src/index.ts", "other-plugin"], theme: "dark" };
|
|
456
|
+
ok("plugin remove by substring", removePluginEntry(rpdoc, "open-memex") === "removed"
|
|
457
|
+
&& JSON.stringify(rpdoc["plugin"]) === JSON.stringify(["other-plugin"]));
|
|
458
|
+
const rpdoc3: Record<string, unknown> = { plugin: ["file:///open-memex/y"] };
|
|
459
|
+
ok("plugin remove prunes empty array", removePluginEntry(rpdoc3, "open-memex") === "removed" && !("plugin" in rpdoc3));
|
|
460
|
+
ok("plugin remove absent", removePluginEntry({ plugin: ["other"] }, "open-memex") === "absent");
|
|
461
|
+
// removeInstructionsSection: cuts MARKER..end, "" when nothing remains.
|
|
462
|
+
ok("instructions remove keeps prior content",
|
|
463
|
+
removeInstructionsSection("# mine\n\n<!-- open-memex -->\n# OpenMemex memory\n") === "# mine\n");
|
|
464
|
+
ok("instructions remove returns empty when wholesale",
|
|
465
|
+
removeInstructionsSection("<!-- open-memex -->\n# OpenMemex memory\n") === "");
|
|
466
|
+
ok("instructions remove no-op without marker",
|
|
467
|
+
removeInstructionsSection("# mine\n") === "# mine\n");
|
|
425
468
|
// detectInstalledClients with a fully fake env.
|
|
426
469
|
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "memex-bin-"));
|
|
427
470
|
fs.writeFileSync(path.join(binDir, "code"), "#!/bin/sh\n");
|
package/src/cli.ts
CHANGED
|
@@ -296,6 +296,23 @@ Examples:
|
|
|
296
296
|
open-memex init --client vscode --global --yes
|
|
297
297
|
open-memex init --client cursor --yes`,
|
|
298
298
|
|
|
299
|
+
uninstall: `Remove the editor wiring that \`init\` wrote: the MCP server entry,
|
|
300
|
+
the opencode native plugin line, and the Copilot instructions section.
|
|
301
|
+
Your memories are never touched.
|
|
302
|
+
|
|
303
|
+
Usage: open-memex uninstall [--client vscode|cursor|opencode|visualstudio]
|
|
304
|
+
[--global] [--yes]
|
|
305
|
+
|
|
306
|
+
Flags:
|
|
307
|
+
--client editor to unwire (default: auto-detect all installed editors)
|
|
308
|
+
--global only the user-level config; without it, both project-level and
|
|
309
|
+
user-level wiring are removed (init may have written either)
|
|
310
|
+
--yes accept all defaults, never prompt
|
|
311
|
+
|
|
312
|
+
Examples:
|
|
313
|
+
open-memex uninstall
|
|
314
|
+
open-memex uninstall --client vscode --yes`,
|
|
315
|
+
|
|
299
316
|
config: `Show config, or set a key.
|
|
300
317
|
|
|
301
318
|
Usage: open-memex config [set <key> <value>]
|
|
@@ -351,6 +368,8 @@ Usage:
|
|
|
351
368
|
open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
|
|
352
369
|
open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
353
370
|
[--instructions personal|project] [--force] [--yes]
|
|
371
|
+
open-memex uninstall [--client vscode|cursor|opencode|visualstudio]
|
|
372
|
+
[--global] [--yes]
|
|
354
373
|
open-memex config [set <key> <value>]
|
|
355
374
|
open-memex capture --dry-run "text"
|
|
356
375
|
open-memex doctor
|
|
@@ -617,6 +636,19 @@ async function main() {
|
|
|
617
636
|
return;
|
|
618
637
|
}
|
|
619
638
|
|
|
639
|
+
// `uninstall` reverses `init` — removes the editor wiring; never touches data.
|
|
640
|
+
// No DB needed (pure file operation, like init).
|
|
641
|
+
if (cmd === "uninstall") {
|
|
642
|
+
const flags = parseFlags(rest);
|
|
643
|
+
const { uninstallProject } = await import("./init.ts");
|
|
644
|
+
await uninstallProject({
|
|
645
|
+
client: flags["client"],
|
|
646
|
+
global: flags["global"] === "true",
|
|
647
|
+
yes: flags["yes"] === "true",
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
620
652
|
// `doctor` runs environment health checks — no DB needed (it self-contains).
|
|
621
653
|
if (cmd === "doctor") {
|
|
622
654
|
const { runDoctor } = await import("./doctor.ts");
|
package/src/init.ts
CHANGED
|
@@ -181,6 +181,38 @@ export function mergeServerEntry(
|
|
|
181
181
|
return "added";
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* D46-followup: when a config file isn't valid JSON we leave it untouched —
|
|
186
|
+
* but "fix it manually" alone isn't helpful. Print the exact snippet the
|
|
187
|
+
* user needs to add by hand (same idea as the opencode --global hint).
|
|
188
|
+
*/
|
|
189
|
+
export function printManualEntryHint(sectionKey: string, entry: Record<string, unknown>): void {
|
|
190
|
+
console.error(` Add this under "${sectionKey}":`);
|
|
191
|
+
const pretty = JSON.stringify({ "open-memex": entry }, null, 2).replace(/\n/g, "\n ");
|
|
192
|
+
console.error(` ${pretty}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parse a JSON config file's raw text for merging. An empty or
|
|
197
|
+
* whitespace-only file counts as `{}` — there is nothing to preserve, so
|
|
198
|
+
* writers can safely populate it (an empty mcp.json is a normal first-run
|
|
199
|
+
* state, e.g. created by the editor, not a corrupt file). Returns null when
|
|
200
|
+
* the content is genuinely unparseable (e.g. JSONC comments) or not an
|
|
201
|
+
* object, in which case the caller leaves the file untouched and prints the
|
|
202
|
+
* manual step.
|
|
203
|
+
*/
|
|
204
|
+
export function parseJsonConfig(raw: string): Record<string, unknown> | null {
|
|
205
|
+
if (raw.trim() === "") return {};
|
|
206
|
+
let doc: unknown;
|
|
207
|
+
try {
|
|
208
|
+
doc = JSON.parse(raw);
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
213
|
+
return doc as Record<string, unknown>;
|
|
214
|
+
}
|
|
215
|
+
|
|
184
216
|
/**
|
|
185
217
|
* Read (or create) a JSON config file, merge the open-memex server entry, write
|
|
186
218
|
* it back. Existing files are merged, never clobbered; invalid JSON is left
|
|
@@ -196,12 +228,13 @@ function writeServerEntryFile(
|
|
|
196
228
|
): string | null {
|
|
197
229
|
let doc: Record<string, unknown> = {};
|
|
198
230
|
if (fs.existsSync(file)) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
} catch {
|
|
231
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
232
|
+
if (parsed === null) {
|
|
202
233
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
234
|
+
printManualEntryHint(sectionKey, entry);
|
|
203
235
|
return null;
|
|
204
236
|
}
|
|
237
|
+
doc = parsed;
|
|
205
238
|
}
|
|
206
239
|
const merged = mergeServerEntry(doc, sectionKey, entry, force);
|
|
207
240
|
if (merged === "kept") {
|
|
@@ -304,12 +337,18 @@ function writeOpencodeMcpJson(root: string, force: boolean): string | null {
|
|
|
304
337
|
const file = path.join(root, "opencode.jsonc");
|
|
305
338
|
let doc: Record<string, unknown> = {};
|
|
306
339
|
if (fs.existsSync(file)) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
} catch {
|
|
340
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
341
|
+
if (parsed === null) {
|
|
310
342
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
343
|
+
const mc = resolveMcpCommand();
|
|
344
|
+
printManualEntryHint("mcp", {
|
|
345
|
+
type: "local",
|
|
346
|
+
command: [mc.command, ...mc.args],
|
|
347
|
+
enabled: true,
|
|
348
|
+
});
|
|
311
349
|
return null;
|
|
312
350
|
}
|
|
351
|
+
doc = parsed;
|
|
313
352
|
}
|
|
314
353
|
const section = ((doc["mcp"] ??= {}) as Record<string, unknown>);
|
|
315
354
|
if (section["open-memex"] && !force) {
|
|
@@ -339,10 +378,15 @@ function writeVisualStudioMcpJson(root: string, force: boolean): string | null {
|
|
|
339
378
|
const file = path.join(root, ".mcp.json");
|
|
340
379
|
let doc: Record<string, unknown> = {};
|
|
341
380
|
if (fs.existsSync(file)) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
} catch {
|
|
381
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
382
|
+
if (parsed === null) {
|
|
345
383
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
384
|
+
const mc = resolveMcpCommand();
|
|
385
|
+
printManualEntryHint("servers", {
|
|
386
|
+
type: "stdio",
|
|
387
|
+
command: mc.command,
|
|
388
|
+
args: mc.args,
|
|
389
|
+
});
|
|
346
390
|
return null;
|
|
347
391
|
}
|
|
348
392
|
}
|
|
@@ -677,3 +721,192 @@ export async function initProject(opts: {
|
|
|
677
721
|
}
|
|
678
722
|
console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
|
|
679
723
|
}
|
|
724
|
+
|
|
725
|
+
// ---------------------------------------------------------------------------
|
|
726
|
+
// uninstall — the reverse of init (D48). Removes the editor wiring init wrote:
|
|
727
|
+
// the MCP server entry, the opencode native plugin line, and the Copilot
|
|
728
|
+
// instructions section. Memory data is never touched.
|
|
729
|
+
// ---------------------------------------------------------------------------
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Pure removal of the open-memex server entry from a parsed config doc.
|
|
733
|
+
* Prunes the section when it becomes empty. Mutates `doc`.
|
|
734
|
+
*/
|
|
735
|
+
export function removeServerEntry(
|
|
736
|
+
doc: Record<string, unknown>,
|
|
737
|
+
sectionKey: string,
|
|
738
|
+
): "removed" | "absent" {
|
|
739
|
+
const section = doc[sectionKey];
|
|
740
|
+
if (!section || typeof section !== "object" || !("open-memex" in section)) return "absent";
|
|
741
|
+
delete (section as Record<string, unknown>)["open-memex"];
|
|
742
|
+
if (Object.keys(section as Record<string, unknown>).length === 0) delete doc[sectionKey];
|
|
743
|
+
return "removed";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Pure removal of open-memex plugin entries from a parsed opencode config doc.
|
|
748
|
+
* Matches any plugin URL containing `match` (robust against the package moving
|
|
749
|
+
* between installs). Prunes the `plugin` array when it becomes empty.
|
|
750
|
+
* Mutates `doc`.
|
|
751
|
+
*/
|
|
752
|
+
export function removePluginEntry(doc: Record<string, unknown>, match: string): "removed" | "absent" {
|
|
753
|
+
const plugins = doc["plugin"];
|
|
754
|
+
if (!Array.isArray(plugins)) return "absent";
|
|
755
|
+
const kept = (plugins as unknown[]).filter(
|
|
756
|
+
(p) => !(typeof p === "string" && p.includes(match)),
|
|
757
|
+
);
|
|
758
|
+
if (kept.length === plugins.length) return "absent";
|
|
759
|
+
if (kept.length === 0) delete doc["plugin"];
|
|
760
|
+
else doc["plugin"] = kept;
|
|
761
|
+
return "removed";
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Pure removal of the open-memex instructions section from a
|
|
766
|
+
* copilot-instructions.md body. init always appends it last (MARKER..end),
|
|
767
|
+
* so cutting from the marker to the end is exact; returns "" when nothing
|
|
768
|
+
* but the section remains so the caller can delete the file.
|
|
769
|
+
*/
|
|
770
|
+
export function removeInstructionsSection(text: string): string {
|
|
771
|
+
const idx = text.lastIndexOf(MARKER);
|
|
772
|
+
if (idx < 0) return text;
|
|
773
|
+
const rest = text.slice(0, idx).replace(/\s+$/, "");
|
|
774
|
+
return rest ? rest + "\n" : "";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Remove the server entry from one JSON config file. Existing files are
|
|
779
|
+
* merged, never clobbered; invalid JSON is left untouched with the manual
|
|
780
|
+
* step (same D47 treatment as the init write path).
|
|
781
|
+
*/
|
|
782
|
+
function removeServerEntryFile(file: string, sectionKey: string): "removed" | "absent" | null {
|
|
783
|
+
if (!fs.existsSync(file)) return "absent";
|
|
784
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
785
|
+
if (doc === null) {
|
|
786
|
+
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
787
|
+
console.error(` Delete the "open-memex" key under "${sectionKey}".`);
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const res = removeServerEntry(doc, sectionKey);
|
|
791
|
+
if (res === "removed") {
|
|
792
|
+
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
793
|
+
console.log(` - ${file} (open-memex entry removed)`);
|
|
794
|
+
}
|
|
795
|
+
return res;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Remove the open-memex plugin URL from one opencode user-level config file. */
|
|
799
|
+
function removeOpencodePluginFile(file: string): "removed" | "absent" | null {
|
|
800
|
+
if (!fs.existsSync(file)) return "absent";
|
|
801
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
802
|
+
if (doc === null) {
|
|
803
|
+
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
804
|
+
console.error(` Delete the open-memex URL from the "plugin" array.`);
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
const res = removePluginEntry(doc, "open-memex");
|
|
808
|
+
if (res === "removed") {
|
|
809
|
+
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
810
|
+
console.log(` - ${file} (open-memex plugin removed)`);
|
|
811
|
+
}
|
|
812
|
+
return res;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Remove the open-memex section from one copilot-instructions.md file. */
|
|
816
|
+
function removeInstructionsFile(file: string): "removed" | "absent" {
|
|
817
|
+
if (!fs.existsSync(file)) return "absent";
|
|
818
|
+
const cur = fs.readFileSync(file, "utf8");
|
|
819
|
+
if (!cur.includes(MARKER)) return "absent";
|
|
820
|
+
const next = removeInstructionsSection(cur);
|
|
821
|
+
if (next === "") {
|
|
822
|
+
fs.unlinkSync(file);
|
|
823
|
+
console.log(` - ${file} (deleted — it only held open-memex instructions)`);
|
|
824
|
+
} else {
|
|
825
|
+
fs.writeFileSync(file, next);
|
|
826
|
+
console.log(` - ${file} (open-memex instructions removed)`);
|
|
827
|
+
}
|
|
828
|
+
return "removed";
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export async function uninstallProject(opts: {
|
|
832
|
+
client?: string;
|
|
833
|
+
/** D48: only the user-level config; without it, both levels are cleaned. */
|
|
834
|
+
global?: boolean;
|
|
835
|
+
yes: boolean;
|
|
836
|
+
}): Promise<void> {
|
|
837
|
+
const interactive = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
838
|
+
const explicit = normalizeClient(opts.client ?? "");
|
|
839
|
+
if (explicit && !(INIT_CLIENTS as readonly string[]).includes(explicit)) {
|
|
840
|
+
console.error(`unknown client "${opts.client}" (${INIT_CLIENTS.join("|")})`);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
// Mirror init's client resolution: explicit --client, else auto-detect.
|
|
844
|
+
let clients: InitClient[];
|
|
845
|
+
if (explicit) {
|
|
846
|
+
clients = [explicit as InitClient];
|
|
847
|
+
} else if (interactive) {
|
|
848
|
+
const detected = detectInstalledClients();
|
|
849
|
+
if (detected.length === 0) {
|
|
850
|
+
console.log(" - no supported editors detected — nothing to remove");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
console.log(`Detected editors: ${detected.join(", ")}`);
|
|
854
|
+
const all = await askBool("Remove open-memex wiring from all of them?", true);
|
|
855
|
+
if (all) {
|
|
856
|
+
clients = detected;
|
|
857
|
+
} else {
|
|
858
|
+
const one = await promptClient();
|
|
859
|
+
clients = one ? [one as InitClient] : [];
|
|
860
|
+
}
|
|
861
|
+
} else {
|
|
862
|
+
clients = detectInstalledClients();
|
|
863
|
+
if (clients.length > 0) {
|
|
864
|
+
console.log(`Detected editors: ${clients.join(", ")} — removing from all (use --client to pick one)`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (clients.length === 0) {
|
|
868
|
+
console.log(" - nothing to remove");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
// Without --global, clean both levels: init may have written either one,
|
|
872
|
+
// and a leftover entry at the other level would be a surprise.
|
|
873
|
+
const levels = opts.global ? ["user"] : ["project", "user"];
|
|
874
|
+
const root = projectRoot();
|
|
875
|
+
console.log(`open-memex uninstall — project root: ${root}`);
|
|
876
|
+
let changed = 0;
|
|
877
|
+
const bump = (r: "removed" | "absent" | null) => {
|
|
878
|
+
if (r === "removed") changed++;
|
|
879
|
+
};
|
|
880
|
+
for (const client of clients) {
|
|
881
|
+
if (client === "vscode" || client === "cursor") {
|
|
882
|
+
const sectionKey = client === "cursor" ? "mcpServers" : "servers";
|
|
883
|
+
for (const level of levels) {
|
|
884
|
+
const file =
|
|
885
|
+
level === "user"
|
|
886
|
+
? userMcpConfigPath(client)
|
|
887
|
+
: path.join(root, client === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json");
|
|
888
|
+
bump(removeServerEntryFile(file, sectionKey));
|
|
889
|
+
}
|
|
890
|
+
} else if (client === "opencode") {
|
|
891
|
+
for (const level of levels) {
|
|
892
|
+
if (level === "user") bump(removeOpencodePluginFile(opencodeGlobalConfigPath()));
|
|
893
|
+
else bump(removeServerEntryFile(path.join(root, "opencode.jsonc"), "mcp"));
|
|
894
|
+
}
|
|
895
|
+
} else if (client === "visualstudio") {
|
|
896
|
+
// Solution-level only — --global is meaningless, same as init.
|
|
897
|
+
bump(removeServerEntryFile(path.join(root, ".mcp.json"), "servers"));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
// Copilot instructions: init may have written personal (default) or project.
|
|
901
|
+
if (clients.some((c) => c !== "opencode")) {
|
|
902
|
+
bump(removeInstructionsFile(path.join(os.homedir(), ".copilot", "copilot-instructions.md")));
|
|
903
|
+
bump(removeInstructionsFile(path.join(os.homedir(), "copilot-instructions.md")));
|
|
904
|
+
bump(removeInstructionsFile(path.join(root, ".github", "copilot-instructions.md")));
|
|
905
|
+
}
|
|
906
|
+
console.log(
|
|
907
|
+
changed > 0
|
|
908
|
+
? `\nDone. Removed open-memex wiring from ${changed} file(s).`
|
|
909
|
+
: "\nDone. Nothing to remove — no open-memex wiring found.",
|
|
910
|
+
);
|
|
911
|
+
console.log("Your memories are untouched (uninstall never deletes data).");
|
|
912
|
+
}
|