open-memex 0.5.0-alpha.4 → 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/dist/init.js +35 -22
- package/docs/V2-DESIGN.md +13 -0
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +10 -1
- package/src/init.ts +33 -17
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/dist/init.js
CHANGED
|
@@ -170,6 +170,29 @@ export function printManualEntryHint(sectionKey, entry) {
|
|
|
170
170
|
const pretty = JSON.stringify({ "open-memex": entry }, null, 2).replace(/\n/g, "\n ");
|
|
171
171
|
console.error(` ${pretty}`);
|
|
172
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
|
+
}
|
|
173
196
|
/**
|
|
174
197
|
* Read (or create) a JSON config file, merge the open-memex server entry, write
|
|
175
198
|
* it back. Existing files are merged, never clobbered; invalid JSON is left
|
|
@@ -179,14 +202,13 @@ export function printManualEntryHint(sectionKey, entry) {
|
|
|
179
202
|
function writeServerEntryFile(file, sectionKey, entry, force, mkdir) {
|
|
180
203
|
let doc = {};
|
|
181
204
|
if (fs.existsSync(file)) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
205
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
206
|
+
if (parsed === null) {
|
|
186
207
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
187
208
|
printManualEntryHint(sectionKey, entry);
|
|
188
209
|
return null;
|
|
189
210
|
}
|
|
211
|
+
doc = parsed;
|
|
190
212
|
}
|
|
191
213
|
const merged = mergeServerEntry(doc, sectionKey, entry, force);
|
|
192
214
|
if (merged === "kept") {
|
|
@@ -282,10 +304,8 @@ function writeOpencodeMcpJson(root, force) {
|
|
|
282
304
|
const file = path.join(root, "opencode.jsonc");
|
|
283
305
|
let doc = {};
|
|
284
306
|
if (fs.existsSync(file)) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
307
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
308
|
+
if (parsed === null) {
|
|
289
309
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
290
310
|
const mc = resolveMcpCommand();
|
|
291
311
|
printManualEntryHint("mcp", {
|
|
@@ -295,6 +315,7 @@ function writeOpencodeMcpJson(root, force) {
|
|
|
295
315
|
});
|
|
296
316
|
return null;
|
|
297
317
|
}
|
|
318
|
+
doc = parsed;
|
|
298
319
|
}
|
|
299
320
|
const section = (doc["mcp"] ??= {});
|
|
300
321
|
if (section["open-memex"] && !force) {
|
|
@@ -323,10 +344,8 @@ function writeVisualStudioMcpJson(root, force) {
|
|
|
323
344
|
const file = path.join(root, ".mcp.json");
|
|
324
345
|
let doc = {};
|
|
325
346
|
if (fs.existsSync(file)) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
347
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
348
|
+
if (parsed === null) {
|
|
330
349
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
331
350
|
const mc = resolveMcpCommand();
|
|
332
351
|
printManualEntryHint("servers", {
|
|
@@ -696,11 +715,8 @@ export function removeInstructionsSection(text) {
|
|
|
696
715
|
function removeServerEntryFile(file, sectionKey) {
|
|
697
716
|
if (!fs.existsSync(file))
|
|
698
717
|
return "absent";
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
702
|
-
}
|
|
703
|
-
catch {
|
|
718
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
719
|
+
if (doc === null) {
|
|
704
720
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
705
721
|
console.error(` Delete the "open-memex" key under "${sectionKey}".`);
|
|
706
722
|
return null;
|
|
@@ -716,11 +732,8 @@ function removeServerEntryFile(file, sectionKey) {
|
|
|
716
732
|
function removeOpencodePluginFile(file) {
|
|
717
733
|
if (!fs.existsSync(file))
|
|
718
734
|
return "absent";
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
722
|
-
}
|
|
723
|
-
catch {
|
|
735
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
736
|
+
if (doc === null) {
|
|
724
737
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
725
738
|
console.error(` Delete the open-memex URL from the "plugin" array.`);
|
|
726
739
|
return null;
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -954,6 +954,19 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
954
954
|
with the manual step, same D47 treatment as the init write path.
|
|
955
955
|
*Rationale: Stone asked "有 uninstall 吗" while testing init on Windows —
|
|
956
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.*
|
|
957
970
|
|
|
958
971
|
## Open Questions
|
|
959
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, printManualEntryHint, removeServerEntry, removePluginEntry, removeInstructionsSection } 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) {
|
|
@@ -431,6 +431,15 @@ console.error = origErr;
|
|
|
431
431
|
const hintText = hintLines.join("\n");
|
|
432
432
|
ok("manual hint names the section", hintText.includes('"servers"'));
|
|
433
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);
|
|
434
443
|
// removeServerEntry: pure removal semantics.
|
|
435
444
|
const rdoc: Record<string, unknown> = { servers: { "open-memex": { command: "x" }, other: { command: "y" } }, untouched: 1 };
|
|
436
445
|
ok("remove deletes the entry", removeServerEntry(rdoc, "servers") === "removed"
|
package/src/init.ts
CHANGED
|
@@ -192,6 +192,27 @@ export function printManualEntryHint(sectionKey: string, entry: Record<string, u
|
|
|
192
192
|
console.error(` ${pretty}`);
|
|
193
193
|
}
|
|
194
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
|
+
|
|
195
216
|
/**
|
|
196
217
|
* Read (or create) a JSON config file, merge the open-memex server entry, write
|
|
197
218
|
* it back. Existing files are merged, never clobbered; invalid JSON is left
|
|
@@ -207,13 +228,13 @@ function writeServerEntryFile(
|
|
|
207
228
|
): string | null {
|
|
208
229
|
let doc: Record<string, unknown> = {};
|
|
209
230
|
if (fs.existsSync(file)) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
} catch {
|
|
231
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
232
|
+
if (parsed === null) {
|
|
213
233
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
214
234
|
printManualEntryHint(sectionKey, entry);
|
|
215
235
|
return null;
|
|
216
236
|
}
|
|
237
|
+
doc = parsed;
|
|
217
238
|
}
|
|
218
239
|
const merged = mergeServerEntry(doc, sectionKey, entry, force);
|
|
219
240
|
if (merged === "kept") {
|
|
@@ -316,9 +337,8 @@ function writeOpencodeMcpJson(root: string, force: boolean): string | null {
|
|
|
316
337
|
const file = path.join(root, "opencode.jsonc");
|
|
317
338
|
let doc: Record<string, unknown> = {};
|
|
318
339
|
if (fs.existsSync(file)) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
} catch {
|
|
340
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
341
|
+
if (parsed === null) {
|
|
322
342
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
323
343
|
const mc = resolveMcpCommand();
|
|
324
344
|
printManualEntryHint("mcp", {
|
|
@@ -328,6 +348,7 @@ function writeOpencodeMcpJson(root: string, force: boolean): string | null {
|
|
|
328
348
|
});
|
|
329
349
|
return null;
|
|
330
350
|
}
|
|
351
|
+
doc = parsed;
|
|
331
352
|
}
|
|
332
353
|
const section = ((doc["mcp"] ??= {}) as Record<string, unknown>);
|
|
333
354
|
if (section["open-memex"] && !force) {
|
|
@@ -357,9 +378,8 @@ function writeVisualStudioMcpJson(root: string, force: boolean): string | null {
|
|
|
357
378
|
const file = path.join(root, ".mcp.json");
|
|
358
379
|
let doc: Record<string, unknown> = {};
|
|
359
380
|
if (fs.existsSync(file)) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
} catch {
|
|
381
|
+
const parsed = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
382
|
+
if (parsed === null) {
|
|
363
383
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
364
384
|
const mc = resolveMcpCommand();
|
|
365
385
|
printManualEntryHint("servers", {
|
|
@@ -761,10 +781,8 @@ export function removeInstructionsSection(text: string): string {
|
|
|
761
781
|
*/
|
|
762
782
|
function removeServerEntryFile(file: string, sectionKey: string): "removed" | "absent" | null {
|
|
763
783
|
if (!fs.existsSync(file)) return "absent";
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
767
|
-
} catch {
|
|
784
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
785
|
+
if (doc === null) {
|
|
768
786
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
769
787
|
console.error(` Delete the "open-memex" key under "${sectionKey}".`);
|
|
770
788
|
return null;
|
|
@@ -780,10 +798,8 @@ function removeServerEntryFile(file: string, sectionKey: string): "removed" | "a
|
|
|
780
798
|
/** Remove the open-memex plugin URL from one opencode user-level config file. */
|
|
781
799
|
function removeOpencodePluginFile(file: string): "removed" | "absent" | null {
|
|
782
800
|
if (!fs.existsSync(file)) return "absent";
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
doc = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
786
|
-
} catch {
|
|
801
|
+
const doc = parseJsonConfig(fs.readFileSync(file, "utf8"));
|
|
802
|
+
if (doc === null) {
|
|
787
803
|
console.error(` ! ${file} is not valid JSON — left untouched, fix it manually`);
|
|
788
804
|
console.error(` Delete the open-memex URL from the "plugin" array.`);
|
|
789
805
|
return null;
|