open-memex 0.5.0-alpha.2 → 0.5.0-alpha.4

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
@@ -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.0` stable release.
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, `V2-dev-p2` branch):
117
+ **From source** (bleeding edge, `main` branch):
118
118
 
119
119
  ```sh
120
- git clone -b V2-dev-p2 https://github.com/stoneskin/open-memex.git
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 a global install:
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 a global install:
181
+ # …or without installing the package first:
175
182
  npx -y open-memex init --client vscode
176
183
  ```
177
184
 
178
- Writes `.vscode/mcp.json` and user-level Copilot instructions, then reload the
179
- window and confirm the `open-memex` server is started in Copilot Chat's MCP panel.
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
- Writes `.cursor/mcp.json` and user-level Copilot instructions.
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
- (On a TTY, `init` asks whether the config should be per-project or user-level.)
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] [--force] [--yes]
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.0` 正式版。
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
- **从源码安装**(最新开发版,`V2-dev-p2` 分支):
114
+ **从源码安装**(最新开发版,`main` 分支):
115
115
 
116
116
  ```sh
117
- git clone -b V2-dev-p2 https://github.com/stoneskin/open-memex.git
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
- 自动写 `.vscode/mcp.json` 和用户级 Copilot instructions,然后重新加载窗口,
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
- 自动写 `.cursor/mcp.json` 和用户级 Copilot instructions。
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
- (终端交互模式下,`init` 会问你配到项目级还是用户级。)
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] [--force] [--yes]
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,16 @@ 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
+ }
163
173
  /**
164
174
  * Read (or create) a JSON config file, merge the open-memex server entry, write
165
175
  * it back. Existing files are merged, never clobbered; invalid JSON is left
@@ -174,6 +184,7 @@ function writeServerEntryFile(file, sectionKey, entry, force, mkdir) {
174
184
  }
175
185
  catch {
176
186
  console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
187
+ printManualEntryHint(sectionKey, entry);
177
188
  return null;
178
189
  }
179
190
  }
@@ -276,6 +287,12 @@ function writeOpencodeMcpJson(root, force) {
276
287
  }
277
288
  catch {
278
289
  console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
290
+ const mc = resolveMcpCommand();
291
+ printManualEntryHint("mcp", {
292
+ type: "local",
293
+ command: [mc.command, ...mc.args],
294
+ enabled: true,
295
+ });
279
296
  return null;
280
297
  }
281
298
  }
@@ -311,6 +328,12 @@ function writeVisualStudioMcpJson(root, force) {
311
328
  }
312
329
  catch {
313
330
  console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
331
+ const mc = resolveMcpCommand();
332
+ printManualEntryHint("servers", {
333
+ type: "stdio",
334
+ command: mc.command,
335
+ args: mc.args,
336
+ });
314
337
  return null;
315
338
  }
316
339
  }
@@ -615,3 +638,197 @@ export async function initProject(opts) {
615
638
  }
616
639
  console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
617
640
  }
641
+ // ---------------------------------------------------------------------------
642
+ // uninstall — the reverse of init (D48). Removes the editor wiring init wrote:
643
+ // the MCP server entry, the opencode native plugin line, and the Copilot
644
+ // instructions section. Memory data is never touched.
645
+ // ---------------------------------------------------------------------------
646
+ /**
647
+ * Pure removal of the open-memex server entry from a parsed config doc.
648
+ * Prunes the section when it becomes empty. Mutates `doc`.
649
+ */
650
+ export function removeServerEntry(doc, sectionKey) {
651
+ const section = doc[sectionKey];
652
+ if (!section || typeof section !== "object" || !("open-memex" in section))
653
+ return "absent";
654
+ delete section["open-memex"];
655
+ if (Object.keys(section).length === 0)
656
+ delete doc[sectionKey];
657
+ return "removed";
658
+ }
659
+ /**
660
+ * Pure removal of open-memex plugin entries from a parsed opencode config doc.
661
+ * Matches any plugin URL containing `match` (robust against the package moving
662
+ * between installs). Prunes the `plugin` array when it becomes empty.
663
+ * Mutates `doc`.
664
+ */
665
+ export function removePluginEntry(doc, match) {
666
+ const plugins = doc["plugin"];
667
+ if (!Array.isArray(plugins))
668
+ return "absent";
669
+ const kept = plugins.filter((p) => !(typeof p === "string" && p.includes(match)));
670
+ if (kept.length === plugins.length)
671
+ return "absent";
672
+ if (kept.length === 0)
673
+ delete doc["plugin"];
674
+ else
675
+ doc["plugin"] = kept;
676
+ return "removed";
677
+ }
678
+ /**
679
+ * Pure removal of the open-memex instructions section from a
680
+ * copilot-instructions.md body. init always appends it last (MARKER..end),
681
+ * so cutting from the marker to the end is exact; returns "" when nothing
682
+ * but the section remains so the caller can delete the file.
683
+ */
684
+ export function removeInstructionsSection(text) {
685
+ const idx = text.lastIndexOf(MARKER);
686
+ if (idx < 0)
687
+ return text;
688
+ const rest = text.slice(0, idx).replace(/\s+$/, "");
689
+ return rest ? rest + "\n" : "";
690
+ }
691
+ /**
692
+ * Remove the server entry from one JSON config file. Existing files are
693
+ * merged, never clobbered; invalid JSON is left untouched with the manual
694
+ * step (same D47 treatment as the init write path).
695
+ */
696
+ function removeServerEntryFile(file, sectionKey) {
697
+ if (!fs.existsSync(file))
698
+ return "absent";
699
+ let doc;
700
+ try {
701
+ doc = JSON.parse(fs.readFileSync(file, "utf8"));
702
+ }
703
+ catch {
704
+ console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
705
+ console.error(` Delete the "open-memex" key under "${sectionKey}".`);
706
+ return null;
707
+ }
708
+ const res = removeServerEntry(doc, sectionKey);
709
+ if (res === "removed") {
710
+ fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
711
+ console.log(` - ${file} (open-memex entry removed)`);
712
+ }
713
+ return res;
714
+ }
715
+ /** Remove the open-memex plugin URL from one opencode user-level config file. */
716
+ function removeOpencodePluginFile(file) {
717
+ if (!fs.existsSync(file))
718
+ return "absent";
719
+ let doc;
720
+ try {
721
+ doc = JSON.parse(fs.readFileSync(file, "utf8"));
722
+ }
723
+ catch {
724
+ console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
725
+ console.error(` Delete the open-memex URL from the "plugin" array.`);
726
+ return null;
727
+ }
728
+ const res = removePluginEntry(doc, "open-memex");
729
+ if (res === "removed") {
730
+ fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
731
+ console.log(` - ${file} (open-memex plugin removed)`);
732
+ }
733
+ return res;
734
+ }
735
+ /** Remove the open-memex section from one copilot-instructions.md file. */
736
+ function removeInstructionsFile(file) {
737
+ if (!fs.existsSync(file))
738
+ return "absent";
739
+ const cur = fs.readFileSync(file, "utf8");
740
+ if (!cur.includes(MARKER))
741
+ return "absent";
742
+ const next = removeInstructionsSection(cur);
743
+ if (next === "") {
744
+ fs.unlinkSync(file);
745
+ console.log(` - ${file} (deleted — it only held open-memex instructions)`);
746
+ }
747
+ else {
748
+ fs.writeFileSync(file, next);
749
+ console.log(` - ${file} (open-memex instructions removed)`);
750
+ }
751
+ return "removed";
752
+ }
753
+ export async function uninstallProject(opts) {
754
+ const interactive = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY;
755
+ const explicit = normalizeClient(opts.client ?? "");
756
+ if (explicit && !INIT_CLIENTS.includes(explicit)) {
757
+ console.error(`unknown client "${opts.client}" (${INIT_CLIENTS.join("|")})`);
758
+ process.exit(1);
759
+ }
760
+ // Mirror init's client resolution: explicit --client, else auto-detect.
761
+ let clients;
762
+ if (explicit) {
763
+ clients = [explicit];
764
+ }
765
+ else if (interactive) {
766
+ const detected = detectInstalledClients();
767
+ if (detected.length === 0) {
768
+ console.log(" - no supported editors detected — nothing to remove");
769
+ return;
770
+ }
771
+ console.log(`Detected editors: ${detected.join(", ")}`);
772
+ const all = await askBool("Remove open-memex wiring from all of them?", true);
773
+ if (all) {
774
+ clients = detected;
775
+ }
776
+ else {
777
+ const one = await promptClient();
778
+ clients = one ? [one] : [];
779
+ }
780
+ }
781
+ else {
782
+ clients = detectInstalledClients();
783
+ if (clients.length > 0) {
784
+ console.log(`Detected editors: ${clients.join(", ")} — removing from all (use --client to pick one)`);
785
+ }
786
+ }
787
+ if (clients.length === 0) {
788
+ console.log(" - nothing to remove");
789
+ return;
790
+ }
791
+ // Without --global, clean both levels: init may have written either one,
792
+ // and a leftover entry at the other level would be a surprise.
793
+ const levels = opts.global ? ["user"] : ["project", "user"];
794
+ const root = projectRoot();
795
+ console.log(`open-memex uninstall — project root: ${root}`);
796
+ let changed = 0;
797
+ const bump = (r) => {
798
+ if (r === "removed")
799
+ changed++;
800
+ };
801
+ for (const client of clients) {
802
+ if (client === "vscode" || client === "cursor") {
803
+ const sectionKey = client === "cursor" ? "mcpServers" : "servers";
804
+ for (const level of levels) {
805
+ const file = level === "user"
806
+ ? userMcpConfigPath(client)
807
+ : path.join(root, client === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json");
808
+ bump(removeServerEntryFile(file, sectionKey));
809
+ }
810
+ }
811
+ else if (client === "opencode") {
812
+ for (const level of levels) {
813
+ if (level === "user")
814
+ bump(removeOpencodePluginFile(opencodeGlobalConfigPath()));
815
+ else
816
+ bump(removeServerEntryFile(path.join(root, "opencode.jsonc"), "mcp"));
817
+ }
818
+ }
819
+ else if (client === "visualstudio") {
820
+ // Solution-level only — --global is meaningless, same as init.
821
+ bump(removeServerEntryFile(path.join(root, ".mcp.json"), "servers"));
822
+ }
823
+ }
824
+ // Copilot instructions: init may have written personal (default) or project.
825
+ if (clients.some((c) => c !== "opencode")) {
826
+ bump(removeInstructionsFile(path.join(os.homedir(), ".copilot", "copilot-instructions.md")));
827
+ bump(removeInstructionsFile(path.join(os.homedir(), "copilot-instructions.md")));
828
+ bump(removeInstructionsFile(path.join(root, ".github", "copilot-instructions.md")));
829
+ }
830
+ console.log(changed > 0
831
+ ? `\nDone. Removed open-memex wiring from ${changed} file(s).`
832
+ : "\nDone. Nothing to remove — no open-memex wiring found.");
833
+ console.log("Your memories are untouched (uninstall never deletes data).");
834
+ }
package/docs/V2-DESIGN.md CHANGED
@@ -930,6 +930,30 @@ 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.*
933
957
 
934
958
  ## Open Questions
935
959
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.5.0-alpha.2",
3
+ "version": "0.5.0-alpha.4",
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",
@@ -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, 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,40 @@ 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
+ // removeServerEntry: pure removal semantics.
435
+ const rdoc: Record<string, unknown> = { servers: { "open-memex": { command: "x" }, other: { command: "y" } }, untouched: 1 };
436
+ ok("remove deletes the entry", removeServerEntry(rdoc, "servers") === "removed"
437
+ && !("open-memex" in (rdoc["servers"] as Record<string, unknown>))
438
+ && "other" in (rdoc["servers"] as Record<string, unknown>));
439
+ ok("remove absent when entry already gone", removeServerEntry(rdoc, "servers") === "absent"
440
+ && "servers" in rdoc); // sibling kept, section kept
441
+ const rdoc2: Record<string, unknown> = { servers: { "open-memex": { command: "x" } } };
442
+ ok("remove prunes empty section", removeServerEntry(rdoc2, "servers") === "removed" && !("servers" in rdoc2));
443
+ ok("remove absent when no entry", removeServerEntry({ servers: {} }, "servers") === "absent");
444
+ ok("remove absent when no section", removeServerEntry({}, "servers") === "absent");
445
+ // removePluginEntry: removes any open-memex URL, prunes empty array.
446
+ const rpdoc: Record<string, unknown> = { plugin: ["file:///x/open-memex/src/index.ts", "other-plugin"], theme: "dark" };
447
+ ok("plugin remove by substring", removePluginEntry(rpdoc, "open-memex") === "removed"
448
+ && JSON.stringify(rpdoc["plugin"]) === JSON.stringify(["other-plugin"]));
449
+ const rpdoc3: Record<string, unknown> = { plugin: ["file:///open-memex/y"] };
450
+ ok("plugin remove prunes empty array", removePluginEntry(rpdoc3, "open-memex") === "removed" && !("plugin" in rpdoc3));
451
+ ok("plugin remove absent", removePluginEntry({ plugin: ["other"] }, "open-memex") === "absent");
452
+ // removeInstructionsSection: cuts MARKER..end, "" when nothing remains.
453
+ ok("instructions remove keeps prior content",
454
+ removeInstructionsSection("# mine\n\n<!-- open-memex -->\n# OpenMemex memory\n") === "# mine\n");
455
+ ok("instructions remove returns empty when wholesale",
456
+ removeInstructionsSection("<!-- open-memex -->\n# OpenMemex memory\n") === "");
457
+ ok("instructions remove no-op without marker",
458
+ removeInstructionsSection("# mine\n") === "# mine\n");
425
459
  // detectInstalledClients with a fully fake env.
426
460
  const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "memex-bin-"));
427
461
  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,17 @@ 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
+
184
195
  /**
185
196
  * Read (or create) a JSON config file, merge the open-memex server entry, write
186
197
  * it back. Existing files are merged, never clobbered; invalid JSON is left
@@ -200,6 +211,7 @@ function writeServerEntryFile(
200
211
  doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
201
212
  } catch {
202
213
  console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
214
+ printManualEntryHint(sectionKey, entry);
203
215
  return null;
204
216
  }
205
217
  }
@@ -308,6 +320,12 @@ function writeOpencodeMcpJson(root: string, force: boolean): string | null {
308
320
  doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
309
321
  } catch {
310
322
  console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
323
+ const mc = resolveMcpCommand();
324
+ printManualEntryHint("mcp", {
325
+ type: "local",
326
+ command: [mc.command, ...mc.args],
327
+ enabled: true,
328
+ });
311
329
  return null;
312
330
  }
313
331
  }
@@ -343,6 +361,12 @@ function writeVisualStudioMcpJson(root: string, force: boolean): string | null {
343
361
  doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
344
362
  } catch {
345
363
  console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
364
+ const mc = resolveMcpCommand();
365
+ printManualEntryHint("servers", {
366
+ type: "stdio",
367
+ command: mc.command,
368
+ args: mc.args,
369
+ });
346
370
  return null;
347
371
  }
348
372
  }
@@ -677,3 +701,196 @@ export async function initProject(opts: {
677
701
  }
678
702
  console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
679
703
  }
704
+
705
+ // ---------------------------------------------------------------------------
706
+ // uninstall — the reverse of init (D48). Removes the editor wiring init wrote:
707
+ // the MCP server entry, the opencode native plugin line, and the Copilot
708
+ // instructions section. Memory data is never touched.
709
+ // ---------------------------------------------------------------------------
710
+
711
+ /**
712
+ * Pure removal of the open-memex server entry from a parsed config doc.
713
+ * Prunes the section when it becomes empty. Mutates `doc`.
714
+ */
715
+ export function removeServerEntry(
716
+ doc: Record<string, unknown>,
717
+ sectionKey: string,
718
+ ): "removed" | "absent" {
719
+ const section = doc[sectionKey];
720
+ if (!section || typeof section !== "object" || !("open-memex" in section)) return "absent";
721
+ delete (section as Record<string, unknown>)["open-memex"];
722
+ if (Object.keys(section as Record<string, unknown>).length === 0) delete doc[sectionKey];
723
+ return "removed";
724
+ }
725
+
726
+ /**
727
+ * Pure removal of open-memex plugin entries from a parsed opencode config doc.
728
+ * Matches any plugin URL containing `match` (robust against the package moving
729
+ * between installs). Prunes the `plugin` array when it becomes empty.
730
+ * Mutates `doc`.
731
+ */
732
+ export function removePluginEntry(doc: Record<string, unknown>, match: string): "removed" | "absent" {
733
+ const plugins = doc["plugin"];
734
+ if (!Array.isArray(plugins)) return "absent";
735
+ const kept = (plugins as unknown[]).filter(
736
+ (p) => !(typeof p === "string" && p.includes(match)),
737
+ );
738
+ if (kept.length === plugins.length) return "absent";
739
+ if (kept.length === 0) delete doc["plugin"];
740
+ else doc["plugin"] = kept;
741
+ return "removed";
742
+ }
743
+
744
+ /**
745
+ * Pure removal of the open-memex instructions section from a
746
+ * copilot-instructions.md body. init always appends it last (MARKER..end),
747
+ * so cutting from the marker to the end is exact; returns "" when nothing
748
+ * but the section remains so the caller can delete the file.
749
+ */
750
+ export function removeInstructionsSection(text: string): string {
751
+ const idx = text.lastIndexOf(MARKER);
752
+ if (idx < 0) return text;
753
+ const rest = text.slice(0, idx).replace(/\s+$/, "");
754
+ return rest ? rest + "\n" : "";
755
+ }
756
+
757
+ /**
758
+ * Remove the server entry from one JSON config file. Existing files are
759
+ * merged, never clobbered; invalid JSON is left untouched with the manual
760
+ * step (same D47 treatment as the init write path).
761
+ */
762
+ function removeServerEntryFile(file: string, sectionKey: string): "removed" | "absent" | null {
763
+ if (!fs.existsSync(file)) return "absent";
764
+ let doc: Record<string, unknown>;
765
+ try {
766
+ doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
767
+ } catch {
768
+ console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
769
+ console.error(` Delete the "open-memex" key under "${sectionKey}".`);
770
+ return null;
771
+ }
772
+ const res = removeServerEntry(doc, sectionKey);
773
+ if (res === "removed") {
774
+ fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
775
+ console.log(` - ${file} (open-memex entry removed)`);
776
+ }
777
+ return res;
778
+ }
779
+
780
+ /** Remove the open-memex plugin URL from one opencode user-level config file. */
781
+ function removeOpencodePluginFile(file: string): "removed" | "absent" | null {
782
+ if (!fs.existsSync(file)) return "absent";
783
+ let doc: Record<string, unknown>;
784
+ try {
785
+ doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
786
+ } catch {
787
+ console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
788
+ console.error(` Delete the open-memex URL from the "plugin" array.`);
789
+ return null;
790
+ }
791
+ const res = removePluginEntry(doc, "open-memex");
792
+ if (res === "removed") {
793
+ fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
794
+ console.log(` - ${file} (open-memex plugin removed)`);
795
+ }
796
+ return res;
797
+ }
798
+
799
+ /** Remove the open-memex section from one copilot-instructions.md file. */
800
+ function removeInstructionsFile(file: string): "removed" | "absent" {
801
+ if (!fs.existsSync(file)) return "absent";
802
+ const cur = fs.readFileSync(file, "utf8");
803
+ if (!cur.includes(MARKER)) return "absent";
804
+ const next = removeInstructionsSection(cur);
805
+ if (next === "") {
806
+ fs.unlinkSync(file);
807
+ console.log(` - ${file} (deleted — it only held open-memex instructions)`);
808
+ } else {
809
+ fs.writeFileSync(file, next);
810
+ console.log(` - ${file} (open-memex instructions removed)`);
811
+ }
812
+ return "removed";
813
+ }
814
+
815
+ export async function uninstallProject(opts: {
816
+ client?: string;
817
+ /** D48: only the user-level config; without it, both levels are cleaned. */
818
+ global?: boolean;
819
+ yes: boolean;
820
+ }): Promise<void> {
821
+ const interactive = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY;
822
+ const explicit = normalizeClient(opts.client ?? "");
823
+ if (explicit && !(INIT_CLIENTS as readonly string[]).includes(explicit)) {
824
+ console.error(`unknown client "${opts.client}" (${INIT_CLIENTS.join("|")})`);
825
+ process.exit(1);
826
+ }
827
+ // Mirror init's client resolution: explicit --client, else auto-detect.
828
+ let clients: InitClient[];
829
+ if (explicit) {
830
+ clients = [explicit as InitClient];
831
+ } else if (interactive) {
832
+ const detected = detectInstalledClients();
833
+ if (detected.length === 0) {
834
+ console.log(" - no supported editors detected — nothing to remove");
835
+ return;
836
+ }
837
+ console.log(`Detected editors: ${detected.join(", ")}`);
838
+ const all = await askBool("Remove open-memex wiring from all of them?", true);
839
+ if (all) {
840
+ clients = detected;
841
+ } else {
842
+ const one = await promptClient();
843
+ clients = one ? [one as InitClient] : [];
844
+ }
845
+ } else {
846
+ clients = detectInstalledClients();
847
+ if (clients.length > 0) {
848
+ console.log(`Detected editors: ${clients.join(", ")} — removing from all (use --client to pick one)`);
849
+ }
850
+ }
851
+ if (clients.length === 0) {
852
+ console.log(" - nothing to remove");
853
+ return;
854
+ }
855
+ // Without --global, clean both levels: init may have written either one,
856
+ // and a leftover entry at the other level would be a surprise.
857
+ const levels = opts.global ? ["user"] : ["project", "user"];
858
+ const root = projectRoot();
859
+ console.log(`open-memex uninstall — project root: ${root}`);
860
+ let changed = 0;
861
+ const bump = (r: "removed" | "absent" | null) => {
862
+ if (r === "removed") changed++;
863
+ };
864
+ for (const client of clients) {
865
+ if (client === "vscode" || client === "cursor") {
866
+ const sectionKey = client === "cursor" ? "mcpServers" : "servers";
867
+ for (const level of levels) {
868
+ const file =
869
+ level === "user"
870
+ ? userMcpConfigPath(client)
871
+ : path.join(root, client === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json");
872
+ bump(removeServerEntryFile(file, sectionKey));
873
+ }
874
+ } else if (client === "opencode") {
875
+ for (const level of levels) {
876
+ if (level === "user") bump(removeOpencodePluginFile(opencodeGlobalConfigPath()));
877
+ else bump(removeServerEntryFile(path.join(root, "opencode.jsonc"), "mcp"));
878
+ }
879
+ } else if (client === "visualstudio") {
880
+ // Solution-level only — --global is meaningless, same as init.
881
+ bump(removeServerEntryFile(path.join(root, ".mcp.json"), "servers"));
882
+ }
883
+ }
884
+ // Copilot instructions: init may have written personal (default) or project.
885
+ if (clients.some((c) => c !== "opencode")) {
886
+ bump(removeInstructionsFile(path.join(os.homedir(), ".copilot", "copilot-instructions.md")));
887
+ bump(removeInstructionsFile(path.join(os.homedir(), "copilot-instructions.md")));
888
+ bump(removeInstructionsFile(path.join(root, ".github", "copilot-instructions.md")));
889
+ }
890
+ console.log(
891
+ changed > 0
892
+ ? `\nDone. Removed open-memex wiring from ${changed} file(s).`
893
+ : "\nDone. Nothing to remove — no open-memex wiring found.",
894
+ );
895
+ console.log("Your memories are untouched (uninstall never deletes data).");
896
+ }