open-memex 0.4.1 → 0.5.0-alpha.2
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 +13 -7
- package/CONTRIBUTING.md +10 -10
- package/README.md +52 -8
- package/README.zh-CN.md +49 -7
- package/dist/cli.js +18 -9
- package/dist/init.js +299 -30
- package/docs/TEST-PLAN.md +2 -0
- package/docs/V2-DESIGN.md +37 -0
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +70 -0
- package/src/cli.ts +18 -9
- package/src/init.ts +327 -27
package/dist/init.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
7
8
|
import { DEFAULT_CONFIG, saveConfig } from "./config.js";
|
|
8
9
|
import { projectRoot } from "./paths.js";
|
|
9
10
|
const MARKER = "<!-- open-memex -->";
|
|
@@ -100,6 +101,72 @@ function writeMcpJson(root, client, force) {
|
|
|
100
101
|
const dir = client === "cursor" ? path.join(root, ".cursor") : path.join(root, ".vscode");
|
|
101
102
|
const file = path.join(dir, "mcp.json");
|
|
102
103
|
const sectionKey = client === "cursor" ? "mcpServers" : "servers";
|
|
104
|
+
const { entry, durable } = stdioServerEntry(client);
|
|
105
|
+
const written = writeServerEntryFile(file, sectionKey, entry, force, true);
|
|
106
|
+
if (written)
|
|
107
|
+
warnNonDurable(durable);
|
|
108
|
+
return written;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* D45: user-level MCP config path — `init --global` writes here so one init
|
|
112
|
+
* covers all projects. `platform` and `home` are parameters (default: current)
|
|
113
|
+
* so tests can cover all OS layouts without mocking.
|
|
114
|
+
*/
|
|
115
|
+
export function userMcpConfigPath(client, platform = process.platform, home = os.homedir()) {
|
|
116
|
+
if (client === "cursor")
|
|
117
|
+
return path.join(home, ".cursor", "mcp.json");
|
|
118
|
+
if (platform === "win32") {
|
|
119
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
120
|
+
return path.join(appData, "Code", "User", "mcp.json");
|
|
121
|
+
}
|
|
122
|
+
if (platform === "darwin") {
|
|
123
|
+
return path.join(home, "Library", "Application Support", "Code", "User", "mcp.json");
|
|
124
|
+
}
|
|
125
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
126
|
+
return path.join(xdg, "Code", "User", "mcp.json");
|
|
127
|
+
}
|
|
128
|
+
/** The open-memex server entry, same shape init writes at project level. */
|
|
129
|
+
function stdioServerEntry(client) {
|
|
130
|
+
// D17: resolve the server command at init time — a one-shot npx leaves no bin behind.
|
|
131
|
+
const mc = resolveMcpCommand();
|
|
132
|
+
const entry = client === "cursor"
|
|
133
|
+
? { command: mc.command, args: mc.args }
|
|
134
|
+
: {
|
|
135
|
+
type: "stdio",
|
|
136
|
+
command: mc.command,
|
|
137
|
+
args: mc.args,
|
|
138
|
+
// VS Code substitutes ${workspaceFolder} per window, so the server
|
|
139
|
+
// starts with the open project as cwd and scope resolution just works.
|
|
140
|
+
cwd: "${workspaceFolder}",
|
|
141
|
+
};
|
|
142
|
+
return { entry, durable: mc.durable };
|
|
143
|
+
}
|
|
144
|
+
/** Warn when init had to fall back to an npx-based server command. */
|
|
145
|
+
function warnNonDurable(durable) {
|
|
146
|
+
if (durable)
|
|
147
|
+
return;
|
|
148
|
+
console.log(` ! no durable \`open-memex\` on PATH (one-shot npx?) — wrote an npx-based command.`);
|
|
149
|
+
console.log(` For faster startup: \`npm i -g ${ALPHA_TAG}\`, then re-run \`open-memex init --force\`.`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Pure merge of one server entry into a parsed config doc.
|
|
153
|
+
* Returns "added" when the entry was written, "kept" when an entry already
|
|
154
|
+
* existed and force was not set. Mutates `doc`.
|
|
155
|
+
*/
|
|
156
|
+
export function mergeServerEntry(doc, sectionKey, entry, force) {
|
|
157
|
+
const section = (doc[sectionKey] ??= {});
|
|
158
|
+
if (section["open-memex"] && !force)
|
|
159
|
+
return "kept";
|
|
160
|
+
section["open-memex"] = entry;
|
|
161
|
+
return "added";
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read (or create) a JSON config file, merge the open-memex server entry, write
|
|
165
|
+
* it back. Existing files are merged, never clobbered; invalid JSON is left
|
|
166
|
+
* untouched. `mkdir` controls whether parent dirs are created (user-level
|
|
167
|
+
* configs) or expected to exist via the project root.
|
|
168
|
+
*/
|
|
169
|
+
function writeServerEntryFile(file, sectionKey, entry, force, mkdir) {
|
|
103
170
|
let doc = {};
|
|
104
171
|
if (fs.existsSync(file)) {
|
|
105
172
|
try {
|
|
@@ -110,29 +177,93 @@ function writeMcpJson(root, client, force) {
|
|
|
110
177
|
return null;
|
|
111
178
|
}
|
|
112
179
|
}
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
180
|
+
const merged = mergeServerEntry(doc, sectionKey, entry, force);
|
|
181
|
+
if (merged === "kept") {
|
|
115
182
|
console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
|
|
116
183
|
return file;
|
|
117
184
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
section["open-memex"] =
|
|
121
|
-
client === "cursor"
|
|
122
|
-
? { command: mc.command, args: mc.args }
|
|
123
|
-
: {
|
|
124
|
-
type: "stdio",
|
|
125
|
-
command: mc.command,
|
|
126
|
-
args: mc.args,
|
|
127
|
-
cwd: "${workspaceFolder}",
|
|
128
|
-
};
|
|
129
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
185
|
+
if (mkdir)
|
|
186
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
130
187
|
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
131
188
|
console.log(` + ${file}`);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
189
|
+
return file;
|
|
190
|
+
}
|
|
191
|
+
/** D45: `init --global` — one-time user-level wiring for VS Code / Cursor. */
|
|
192
|
+
function writeGlobalMcpJson(client, force) {
|
|
193
|
+
const file = userMcpConfigPath(client);
|
|
194
|
+
const sectionKey = client === "cursor" ? "mcpServers" : "servers";
|
|
195
|
+
const { entry, durable } = stdioServerEntry(client);
|
|
196
|
+
const written = writeServerEntryFile(file, sectionKey, entry, force, true);
|
|
197
|
+
if (written) {
|
|
198
|
+
warnNonDurable(durable);
|
|
199
|
+
const projFile = client === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json";
|
|
200
|
+
console.log(` i user-level config — the open-memex MCP server now starts in every project.`);
|
|
201
|
+
console.log(` (a per-project ${projFile} still wins if a project defines its own)`);
|
|
202
|
+
}
|
|
203
|
+
return written;
|
|
204
|
+
}
|
|
205
|
+
/** Where this installed copy's opencode native plugin entry point lives. */
|
|
206
|
+
function opencodePluginUrl() {
|
|
207
|
+
// init.ts sits in <pkg>/src — the plugin entry is <pkg>/src/index.ts.
|
|
208
|
+
const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
209
|
+
return pathToFileURL(path.join(pkgRoot, "src", "index.ts")).href;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* D46: opencode's user-level config. opencode reads `~/.config/opencode/`
|
|
213
|
+
* (`$XDG_CONFIG_HOME` when set) on every platform; a `plugin` entry there
|
|
214
|
+
* loads open-memex in every project with no per-project init.
|
|
215
|
+
*/
|
|
216
|
+
export function opencodeGlobalConfigPath(home = os.homedir()) {
|
|
217
|
+
return path.join(opencodeConfigDir(home, process.env.XDG_CONFIG_HOME), "opencode.json");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Pure merge of the open-memex plugin URL into a parsed opencode config doc.
|
|
221
|
+
* Creates the `plugin` array when missing; never duplicates the URL.
|
|
222
|
+
* Returns "added" when the doc changed, "kept" when already present.
|
|
223
|
+
*/
|
|
224
|
+
export function mergePluginEntry(doc, url, force) {
|
|
225
|
+
let plugins = doc["plugin"];
|
|
226
|
+
if (!Array.isArray(plugins)) {
|
|
227
|
+
plugins = [];
|
|
228
|
+
doc["plugin"] = plugins;
|
|
135
229
|
}
|
|
230
|
+
const list = plugins;
|
|
231
|
+
if (list.includes(url) && !force)
|
|
232
|
+
return "kept";
|
|
233
|
+
if (!list.includes(url))
|
|
234
|
+
list.push(url);
|
|
235
|
+
return "added";
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* D46: wire the native plugin at the opencode user level — the one-time
|
|
239
|
+
* global setup. Merges into the existing config when it parses as JSON;
|
|
240
|
+
* a file with comments (JSONC) is left untouched with a manual hint instead
|
|
241
|
+
* of being clobbered.
|
|
242
|
+
*/
|
|
243
|
+
function writeOpencodeGlobalPlugin(force) {
|
|
244
|
+
const dir = path.dirname(opencodeGlobalConfigPath());
|
|
245
|
+
const file = ["opencode.jsonc", "opencode.json"]
|
|
246
|
+
.map((f) => path.join(dir, f))
|
|
247
|
+
.find((f) => fs.existsSync(f)) ?? path.join(dir, "opencode.json");
|
|
248
|
+
const url = opencodePluginUrl();
|
|
249
|
+
let doc = {};
|
|
250
|
+
if (fs.existsSync(file)) {
|
|
251
|
+
try {
|
|
252
|
+
doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
console.log(` ! ${file} has comments or invalid JSON — left untouched, fix it manually.`);
|
|
256
|
+
console.log(` To enable open-memex everywhere, add "plugin": ["${url}"] to it.`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (mergePluginEntry(doc, url, force) === "kept") {
|
|
261
|
+
console.log(` = ${file} already loads the open-memex plugin — left as is (use --force to overwrite)`);
|
|
262
|
+
return file;
|
|
263
|
+
}
|
|
264
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
265
|
+
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
266
|
+
console.log(` + ${file} (native plugin — works in every project, no per-project init needed)`);
|
|
136
267
|
return file;
|
|
137
268
|
}
|
|
138
269
|
/** opencode MCP config: project-level opencode.jsonc, `type: "local"` + command array (v1 format). */
|
|
@@ -293,17 +424,133 @@ async function promptInstructionsScope() {
|
|
|
293
424
|
rl.close();
|
|
294
425
|
}
|
|
295
426
|
}
|
|
427
|
+
/** D45: ask whether the MCP server config should be project-level or user-level. */
|
|
428
|
+
async function promptConfigLevel() {
|
|
429
|
+
console.log("Where should the MCP server config live?");
|
|
430
|
+
console.log(" 1) this project only — .vscode/mcp.json (or .cursor/mcp.json)");
|
|
431
|
+
console.log(" 2) user-level — all projects, init once (VS Code / Cursor)");
|
|
432
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
433
|
+
try {
|
|
434
|
+
const ans = (await rl.question("Choice [1]: ")).trim();
|
|
435
|
+
return ans === "2";
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
rl.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function exists(p) {
|
|
442
|
+
try {
|
|
443
|
+
return fs.existsSync(p);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** True when `name` resolves on PATH (honors .cmd/.exe on Windows). */
|
|
450
|
+
function binOnPath(name, pathEnv, platform) {
|
|
451
|
+
const names = platform === "win32" ? [`${name}.cmd`, `${name}.exe`, name] : [name];
|
|
452
|
+
return pathEnv
|
|
453
|
+
.split(path.delimiter)
|
|
454
|
+
.filter((d) => d && !d.includes("_npx"))
|
|
455
|
+
.some((d) => names.some((n) => exists(path.join(d, n))));
|
|
456
|
+
}
|
|
457
|
+
function opencodeConfigDir(home, xdg) {
|
|
458
|
+
return path.join(xdg || path.join(home, ".config"), "opencode");
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* D46: detect which supported editors are installed. Used when `init` runs
|
|
462
|
+
* without --client — one init wires every detected editor (user-level where
|
|
463
|
+
* the editor supports it). Visual Studio is included only when the project
|
|
464
|
+
* has a solution file, since VS config is solution-scoped by design.
|
|
465
|
+
*/
|
|
466
|
+
export function detectInstalledClients(env = {}) {
|
|
467
|
+
const platform = env.platform ?? process.platform;
|
|
468
|
+
const pathEnv = env.pathEnv ?? process.env.PATH ?? "";
|
|
469
|
+
const home = env.home ?? os.homedir();
|
|
470
|
+
const found = [];
|
|
471
|
+
// VS Code: bin on PATH, well-known install location, or an existing
|
|
472
|
+
// user-level MCP config (a previous init counts as installed).
|
|
473
|
+
const vscodeInstallPaths = platform === "win32"
|
|
474
|
+
? [path.join(home, "AppData", "Local", "Programs", "Microsoft VS Code", "Code.exe")]
|
|
475
|
+
: platform === "darwin"
|
|
476
|
+
? ["/Applications/Visual Studio Code.app"]
|
|
477
|
+
: ["/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code"];
|
|
478
|
+
if (binOnPath("code", pathEnv, platform) ||
|
|
479
|
+
vscodeInstallPaths.some(exists) ||
|
|
480
|
+
exists(userMcpConfigPath("vscode", platform, home))) {
|
|
481
|
+
found.push("vscode");
|
|
482
|
+
}
|
|
483
|
+
// Cursor: bin on PATH or its user config dir exists.
|
|
484
|
+
if (binOnPath("cursor", pathEnv, platform) || exists(path.join(home, ".cursor"))) {
|
|
485
|
+
found.push("cursor");
|
|
486
|
+
}
|
|
487
|
+
// opencode: bin on PATH or its global config dir exists.
|
|
488
|
+
const xdg = env.xdgConfigHome ?? process.env.XDG_CONFIG_HOME;
|
|
489
|
+
if (binOnPath("opencode", pathEnv, platform) || exists(opencodeConfigDir(home, xdg))) {
|
|
490
|
+
found.push("opencode");
|
|
491
|
+
}
|
|
492
|
+
// Visual Studio: solution-scoped — only when the project has a .sln.
|
|
493
|
+
let hasSln = false;
|
|
494
|
+
try {
|
|
495
|
+
hasSln = fs.readdirSync(env.root ?? projectRoot()).some((f) => f.toLowerCase().endsWith(".sln"));
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
hasSln = false;
|
|
499
|
+
}
|
|
500
|
+
if (hasSln)
|
|
501
|
+
found.push("visualstudio");
|
|
502
|
+
return found;
|
|
503
|
+
}
|
|
296
504
|
export async function initProject(opts) {
|
|
297
505
|
const interactive = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
298
|
-
|
|
299
|
-
if (
|
|
506
|
+
const explicit = normalizeClient(opts.client ?? "");
|
|
507
|
+
if (explicit && !INIT_CLIENTS.includes(explicit)) {
|
|
300
508
|
console.error(`unknown client "${opts.client}" (${INIT_CLIENTS.join("|")})`);
|
|
301
509
|
process.exit(1);
|
|
302
510
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
511
|
+
// D46: no --client → auto-detect installed editors and wire them all
|
|
512
|
+
// (user-level where the editor supports it — init once). An explicit
|
|
513
|
+
// --client keeps the old single-editor behavior.
|
|
514
|
+
let clients;
|
|
515
|
+
let autoGlobal = false;
|
|
516
|
+
if (explicit) {
|
|
517
|
+
clients = [explicit];
|
|
518
|
+
}
|
|
519
|
+
else if (interactive) {
|
|
520
|
+
const detected = detectInstalledClients();
|
|
521
|
+
if (detected.length === 0) {
|
|
522
|
+
console.log(" - no supported editors detected — editor setup skipped");
|
|
523
|
+
clients = [];
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
console.log(`Detected editors: ${detected.join(", ")}`);
|
|
527
|
+
const all = await askBool("Wire up open-memex in all of them (one-time, user-level where supported)?", true);
|
|
528
|
+
if (all) {
|
|
529
|
+
clients = detected;
|
|
530
|
+
autoGlobal = true;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
const one = await promptClient();
|
|
534
|
+
clients = one ? [one] : [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
clients = detectInstalledClients();
|
|
540
|
+
autoGlobal = true;
|
|
541
|
+
if (clients.length === 0) {
|
|
542
|
+
console.log(" - no supported editors detected — editor setup skipped");
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
console.log(`Detected editors: ${clients.join(", ")} — wiring all (use --client to pick one)`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
let global = !!opts.global || autoGlobal;
|
|
549
|
+
// The project-vs-user-level choice only applies to an explicit single
|
|
550
|
+
// client; auto mode is user-level by design (init once).
|
|
551
|
+
if (explicit && !global && interactive && (clients[0] === "vscode" || clients[0] === "cursor")) {
|
|
552
|
+
global = await promptConfigLevel();
|
|
553
|
+
}
|
|
307
554
|
let scope = "personal";
|
|
308
555
|
if (opts.instructions) {
|
|
309
556
|
if (opts.instructions !== "personal" && opts.instructions !== "project") {
|
|
@@ -332,17 +579,39 @@ export async function initProject(opts) {
|
|
|
332
579
|
}
|
|
333
580
|
const root = projectRoot();
|
|
334
581
|
console.log(`open-memex init — project root: ${root}`);
|
|
335
|
-
|
|
336
|
-
|
|
582
|
+
for (const client of clients) {
|
|
583
|
+
if (client === "vscode" || client === "cursor") {
|
|
584
|
+
if (global)
|
|
585
|
+
writeGlobalMcpJson(client, opts.force);
|
|
586
|
+
else
|
|
587
|
+
writeMcpJson(root, client, opts.force);
|
|
588
|
+
}
|
|
589
|
+
else if (client === "opencode") {
|
|
590
|
+
// D46: the native plugin is the one-time global setup; without --global
|
|
591
|
+
// (explicit single-client mode) keep the per-project plain-MCP config.
|
|
592
|
+
if (global)
|
|
593
|
+
writeOpencodeGlobalPlugin(opts.force);
|
|
594
|
+
else
|
|
595
|
+
writeMcpJson(root, client, opts.force);
|
|
596
|
+
}
|
|
597
|
+
else if (client === "visualstudio") {
|
|
598
|
+
if (explicit && global) {
|
|
599
|
+
console.log(` - visualstudio: --global not supported — VS uses solution-level .mcp.json by design.`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
writeMcpJson(root, client, opts.force);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (clients.length === 0) {
|
|
607
|
+
console.log(" - editor setup skipped");
|
|
608
|
+
}
|
|
609
|
+
else if (clients.some((c) => c !== "opencode")) {
|
|
337
610
|
// copilot-instructions.md is VS Code/Cursor-shaped; opencode as a plain MCP
|
|
338
611
|
// consumer already gets the guidance from the tool descriptions (D16).
|
|
339
612
|
// D22: personal scope (default) writes to the Copilot user-level location
|
|
340
613
|
// so the repo stays clean for teammates without open-memex.
|
|
341
|
-
|
|
342
|
-
writeInstructions(root, scope, client);
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
console.log(" - editor setup skipped");
|
|
614
|
+
writeInstructions(root, scope, clients.find((c) => c !== "opencode"));
|
|
346
615
|
}
|
|
347
616
|
console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
|
|
348
617
|
}
|
package/docs/TEST-PLAN.md
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
- [ ] Cursor:`open-memex mcp --print-config cursor` → 贴到 Cursor MCP 配置 → 能连上
|
|
35
35
|
- [ ] opencode:`open-memex init --client opencode` → `opencode.jsonc` 生效
|
|
36
|
+
- [ ] `open-memex init --yes`(不带 --client)→ 自动检测已装编辑器并一次全接上
|
|
37
|
+
- [ ] opencode:`open-memex init --client opencode --global` → `~/.config/opencode/opencode.json` 的 `plugin` 数组合并(带注释的 jsonc 不动、只给手动提示)
|
|
36
38
|
- [ ] Claude Code:`open-memex mcp --print-config claude` 给出的 `claude mcp add` 命令能跑通
|
|
37
39
|
|
|
38
40
|
## D. 跨机迁移(export/import 真实场景)
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -882,6 +882,24 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
882
882
|
cannot confirm what the real run will do; and a destructive-path failure must
|
|
883
883
|
speak in user terms. Shipped as 0.4.1 hotfix on the stable line. Approved
|
|
884
884
|
2026-09-29.*
|
|
885
|
+
- **D45** — `init --global` (0.5.0-alpha.1): one-time user-level MCP wiring for
|
|
886
|
+
VS Code / Cursor. Writes the open-memex server entry to the editor's
|
|
887
|
+
user-level `mcp.json` (`%APPDATA%\Code\User\mcp.json` on Windows,
|
|
888
|
+
`~/Library/Application Support/Code/User/mcp.json` on macOS,
|
|
889
|
+
`~/.config/Code/User/mcp.json` on Linux; `~/.cursor/mcp.json` for Cursor)
|
|
890
|
+
instead of the project's `.vscode/mcp.json` — init once, the server starts
|
|
891
|
+
in every project. The entry keeps `cwd: "${workspaceFolder}"` so VS Code
|
|
892
|
+
substitutes it per window and project-scope resolution keeps working;
|
|
893
|
+
a per-project config still wins when present. Merge semantics are shared
|
|
894
|
+
with project-level init (merge, never clobber; `--force` overwrites).
|
|
895
|
+
Interactive `init` now asks per-project vs user-level for vscode/cursor
|
|
896
|
+
(default: per-project, preserving old behavior); `--global` skips the
|
|
897
|
+
question. For opencode, `--global` is a no-op that prints the native-plugin
|
|
898
|
+
one-liner (already global, and strictly more capable than plain-MCP mode);
|
|
899
|
+
Visual Studio stays solution-level by design. *Rationale: per-project init
|
|
900
|
+
is a paper cut that compounds — the data layer already needs zero per-project
|
|
901
|
+
setup (scope is derived from cwd), so the editor wiring should be able to
|
|
902
|
+
match. Approved 2026-09-29.*
|
|
885
903
|
- **D41** — The type taxonomy is reconciled to 11 types with one-line definitions
|
|
886
904
|
(§3.1): `fact` `preference` `decision` `constraint` `todo` `knowledge` `howto`
|
|
887
905
|
`gotcha` `lesson` `observation` `reference`. Merged away: `warning`→`gotcha`,
|
|
@@ -893,6 +911,25 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
893
911
|
is deliberately broader than `incident`: a postmortem's shape (timeline, root
|
|
894
912
|
cause, actions) is a template concern, not a type. Code
|
|
895
913
|
`MEMORY_TYPE_TAXONOMY` updated to match. Approved 2026-09-28.*
|
|
914
|
+
- **D46** — `init` with no `--client` auto-detects installed editors and wires
|
|
915
|
+
them all (0.5.0-alpha.2). Detection: VS Code via `code` on `PATH`, well-known
|
|
916
|
+
install locations, or an existing user-level `mcp.json`; Cursor via `cursor`
|
|
917
|
+
on `PATH` or `~/.cursor`; opencode via `opencode` on `PATH` or its global
|
|
918
|
+
config dir; Visual Studio only when the project has a `.sln` (solution-scoped
|
|
919
|
+
by design). Auto mode always wires user-level where the editor supports it —
|
|
920
|
+
VS Code / Cursor MCP entry (D45), and for opencode the native plugin entry is
|
|
921
|
+
now *actually merged* into `~/.config/opencode/opencode.json` (replacing D45's
|
|
922
|
+
hint-only `--global`), so one init covers every editor and every project.
|
|
923
|
+
A config file with comments (JSONC) is never rewritten — init prints the
|
|
924
|
+
manual one-liner instead. Interactive `init` shows the detected editors and
|
|
925
|
+
confirms wiring all of them (declining falls back to the single-editor
|
|
926
|
+
prompt); non-interactive (`--yes`) wires all detected with no prompts.
|
|
927
|
+
An explicit `--client` keeps the old single-editor behavior, including the
|
|
928
|
+
per-project default for vscode/cursor. *Rationale: Stone's two hats — he
|
|
929
|
+
writes code in several editors himself, and new users / pilot colleagues
|
|
930
|
+
should not have to learn `--client` to get started. The help text already
|
|
931
|
+
promised "default: auto-detect"; D46 makes the code keep that promise.
|
|
932
|
+
Approved 2026-09-29.*
|
|
896
933
|
|
|
897
934
|
## Open Questions
|
|
898
935
|
|
package/package.json
CHANGED
package/scripts/smoke-pure.ts
CHANGED
|
@@ -8,6 +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
12
|
|
|
12
13
|
let fails = 0;
|
|
13
14
|
function ok(name: string, cond: boolean, info?: unknown) {
|
|
@@ -376,5 +377,74 @@ const near = similarity(
|
|
|
376
377
|
ok(`similarity near-dup ${near.toFixed(2)} >= ${NEAR_DUP_THRESHOLD}`, near >= NEAR_DUP_THRESHOLD);
|
|
377
378
|
ok("similarity empty → 0", similarity("", "anything") === 0);
|
|
378
379
|
|
|
380
|
+
console.log("== init --global: userMcpConfigPath / mergeServerEntry ==");
|
|
381
|
+
// D45: user-level MCP config locations, per platform (platform param is injectable).
|
|
382
|
+
ok("vscode win32", userMcpConfigPath("vscode", "win32").endsWith(path.join("Code", "User", "mcp.json")));
|
|
383
|
+
ok("vscode darwin", userMcpConfigPath("vscode", "darwin").includes(path.join("Library", "Application Support", "Code", "User", "mcp.json")));
|
|
384
|
+
ok("vscode linux", userMcpConfigPath("vscode", "linux").endsWith(path.join(".config", "Code", "User", "mcp.json")));
|
|
385
|
+
ok("cursor is ~/.cursor/mcp.json on every platform",
|
|
386
|
+
(["win32", "darwin", "linux"] as const).every((p) =>
|
|
387
|
+
userMcpConfigPath("cursor", p).endsWith(path.join(".cursor", "mcp.json"))));
|
|
388
|
+
// mergeServerEntry: pure merge semantics.
|
|
389
|
+
const doc1: Record<string, unknown> = {};
|
|
390
|
+
ok("merge into empty doc adds", mergeServerEntry(doc1, "servers", { command: "x" }, false) === "added");
|
|
391
|
+
ok("entry landed under section", (doc1["servers"] as Record<string, unknown>)["open-memex"] !== undefined);
|
|
392
|
+
ok("existing entry kept without force", mergeServerEntry(doc1, "servers", { command: "y" }, false) === "kept");
|
|
393
|
+
ok("kept entry untouched", ((doc1["servers"] as Record<string, unknown>)["open-memex"] as Record<string, unknown>)["command"] === "x");
|
|
394
|
+
ok("force overwrites", mergeServerEntry(doc1, "servers", { command: "y" }, true) === "added");
|
|
395
|
+
ok("forced entry applied", ((doc1["servers"] as Record<string, unknown>)["open-memex"] as Record<string, unknown>)["command"] === "y");
|
|
396
|
+
const doc2: Record<string, unknown> = { servers: { other: { command: "z" } } };
|
|
397
|
+
ok("merge preserves sibling entries", mergeServerEntry(doc2, "servers", { command: "x" }, false) === "added"
|
|
398
|
+
&& (doc2["servers"] as Record<string, unknown>)["other"] !== undefined);
|
|
399
|
+
|
|
400
|
+
console.log("== init D46: auto-detect + opencode global plugin ==");
|
|
401
|
+
// opencodeGlobalConfigPath: user-level location, XDG-aware.
|
|
402
|
+
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "memex-smoke-"));
|
|
403
|
+
ok("opencode global config under ~/.config/opencode",
|
|
404
|
+
opencodeGlobalConfigPath(fakeHome).endsWith(path.join(".config", "opencode", "opencode.json")));
|
|
405
|
+
ok("opencode global config honors XDG_CONFIG_HOME", (() => {
|
|
406
|
+
const prev = process.env.XDG_CONFIG_HOME;
|
|
407
|
+
process.env.XDG_CONFIG_HOME = path.join(fakeHome, "xdg");
|
|
408
|
+
try {
|
|
409
|
+
return opencodeGlobalConfigPath(fakeHome).startsWith(path.join(fakeHome, "xdg"));
|
|
410
|
+
} finally {
|
|
411
|
+
if (prev === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
412
|
+
else process.env.XDG_CONFIG_HOME = prev;
|
|
413
|
+
}
|
|
414
|
+
})());
|
|
415
|
+
// mergePluginEntry: pure merge semantics.
|
|
416
|
+
const pdoc: Record<string, unknown> = {};
|
|
417
|
+
ok("plugin merge creates array", mergePluginEntry(pdoc, "file:///x", false) === "added"
|
|
418
|
+
&& JSON.stringify(pdoc["plugin"]) === JSON.stringify(["file:///x"]));
|
|
419
|
+
ok("plugin merge dup kept", mergePluginEntry(pdoc, "file:///x", false) === "kept");
|
|
420
|
+
ok("plugin merge force on dup adds nothing twice", mergePluginEntry(pdoc, "file:///x", true) === "added"
|
|
421
|
+
&& (pdoc["plugin"] as unknown[]).length === 1);
|
|
422
|
+
const pdoc2: Record<string, unknown> = { plugin: ["file:///other"], theme: "dark" };
|
|
423
|
+
ok("plugin merge preserves siblings", mergePluginEntry(pdoc2, "file:///x", false) === "added"
|
|
424
|
+
&& (pdoc2["plugin"] as unknown[]).length === 2 && pdoc2["theme"] === "dark");
|
|
425
|
+
// detectInstalledClients with a fully fake env.
|
|
426
|
+
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "memex-bin-"));
|
|
427
|
+
fs.writeFileSync(path.join(binDir, "code"), "#!/bin/sh\n");
|
|
428
|
+
const detHome = fs.mkdtempSync(path.join(os.tmpdir(), "memex-home-"));
|
|
429
|
+
fs.mkdirSync(path.join(detHome, ".cursor"), { recursive: true });
|
|
430
|
+
fs.mkdirSync(path.join(detHome, ".config", "opencode"), { recursive: true });
|
|
431
|
+
const detRoot = fs.mkdtempSync(path.join(os.tmpdir(), "memex-root-"));
|
|
432
|
+
const detEnv = { pathEnv: binDir, home: detHome, platform: "linux" as const, root: detRoot, xdgConfigHome: path.join(detHome, ".config") };
|
|
433
|
+
const det = detectInstalledClients(detEnv);
|
|
434
|
+
ok("detects vscode via PATH", det.includes("vscode"));
|
|
435
|
+
ok("detects cursor via ~/.cursor", det.includes("cursor"));
|
|
436
|
+
ok("detects opencode via config dir", det.includes("opencode"));
|
|
437
|
+
ok("no visualstudio without .sln", !det.includes("visualstudio"));
|
|
438
|
+
fs.writeFileSync(path.join(detRoot, "app.sln"), "");
|
|
439
|
+
ok("visualstudio detected with .sln", detectInstalledClients(detEnv).includes("visualstudio"));
|
|
440
|
+
const emptyHome = fs.mkdtempSync(path.join(os.tmpdir(), "memex-empty-"));
|
|
441
|
+
const detEmpty = detectInstalledClients({ pathEnv: "", home: emptyHome, platform: "linux", root: emptyHome, xdgConfigHome: path.join(emptyHome, ".config") });
|
|
442
|
+
ok("empty env detects nothing", detEmpty.length === 0);
|
|
443
|
+
const winBin = fs.mkdtempSync(path.join(os.tmpdir(), "memex-winbin-"));
|
|
444
|
+
fs.writeFileSync(path.join(winBin, "code.cmd"), "@echo off\n");
|
|
445
|
+
ok("win32 detects vscode via code.cmd",
|
|
446
|
+
detectInstalledClients({ pathEnv: winBin, home: emptyHome, platform: "win32", root: emptyHome, xdgConfigHome: path.join(emptyHome, ".config") }).includes("vscode"));
|
|
447
|
+
for (const d of [fakeHome, binDir, detHome, detRoot, emptyHome, winBin]) fs.rmSync(d, { recursive: true, force: true });
|
|
448
|
+
|
|
379
449
|
console.log(fails === 0 ? "\nALL PASS" : `\n${fails} FAILURES`);
|
|
380
450
|
process.exit(fails === 0 ? 0 : 1);
|
package/src/cli.ts
CHANGED
|
@@ -279,16 +279,21 @@ Examples:
|
|
|
279
279
|
agent memory instructions. Existing files are merged, never clobbered.
|
|
280
280
|
|
|
281
281
|
Usage: open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
282
|
-
[--instructions personal|project] [--force] [--yes]
|
|
282
|
+
[--instructions personal|project] [--global] [--force] [--yes]
|
|
283
283
|
|
|
284
284
|
Flags:
|
|
285
|
-
--client editor to configure (default: auto-detect)
|
|
285
|
+
--client editor to configure (default: auto-detect all installed editors)
|
|
286
286
|
--instructions personal (default, ~/.copilot/copilot-instructions.md) or project
|
|
287
|
+
--global write the MCP server entry to the editor's user-level config
|
|
288
|
+
(VS Code / Cursor) — init once, works in every project.
|
|
289
|
+
For opencode, --global wires the native plugin at user level
|
|
290
|
+
(~/.config/opencode/opencode.json) — no per-project init needed.
|
|
287
291
|
--force overwrite existing config
|
|
288
292
|
--yes accept all defaults, never prompt
|
|
289
293
|
|
|
290
294
|
Examples:
|
|
291
295
|
open-memex init
|
|
296
|
+
open-memex init --client vscode --global --yes
|
|
292
297
|
open-memex init --client cursor --yes`,
|
|
293
298
|
|
|
294
299
|
config: `Show config, or set a key.
|
|
@@ -353,16 +358,19 @@ Usage:
|
|
|
353
358
|
Every command has its own help with description and examples:
|
|
354
359
|
open-memex <command> --help (or -h)
|
|
355
360
|
|
|
356
|
-
One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
361
|
+
One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) detects
|
|
362
|
+
your installed editors and wires them all — user-level where the editor supports it
|
|
363
|
+
(VS Code / Cursor MCP config, opencode native plugin), so one init covers every project;
|
|
364
|
+
Visual Studio is included when the project has a solution file (its \`.mcp.json\`
|
|
365
|
+
stays solution-level by design). \`--client\` picks a single editor instead, and a
|
|
366
|
+
single-editor opencode init writes the per-project plain-MCP \`opencode.jsonc\`.
|
|
367
|
+
The Copilot memory instructions default to your user-level
|
|
360
368
|
\`~/.copilot/copilot-instructions.md\` (all projects, never checked into a repo);
|
|
361
369
|
\`--instructions project\` writes \`.github/copilot-instructions.md\` instead for
|
|
362
370
|
teams where everyone uses open-memex.
|
|
363
371
|
Existing files are merged, never clobbered; re-running is safe. On a terminal it
|
|
364
|
-
|
|
365
|
-
injection); \`--yes\` accepts all defaults, and non-terminal runs never prompt.
|
|
372
|
+
confirms the detected editors and asks a couple of settings (keyword capture,
|
|
373
|
+
first-turn injection); \`--yes\` accepts all defaults, and non-terminal runs never prompt.
|
|
366
374
|
\`open-memex config set <key> <value>\` changes those settings after install.
|
|
367
375
|
|
|
368
376
|
Once installed globally (\`npm i -g open-memex@alpha\`) the \`open-memex\` command is
|
|
@@ -595,7 +603,7 @@ async function main() {
|
|
|
595
603
|
}
|
|
596
604
|
|
|
597
605
|
// `init` is a pure file operation (§17 adoption path) — no DB needed.
|
|
598
|
-
// Interactive when on a TTY (
|
|
606
|
+
// Interactive when on a TTY (confirms detected editors + settings); --yes skips prompts.
|
|
599
607
|
if (cmd === "init") {
|
|
600
608
|
const flags = parseFlags(rest);
|
|
601
609
|
const { initProject } = await import("./init.ts");
|
|
@@ -604,6 +612,7 @@ async function main() {
|
|
|
604
612
|
force: flags["force"] === "true",
|
|
605
613
|
yes: flags["yes"] === "true",
|
|
606
614
|
instructions: flags["instructions"],
|
|
615
|
+
global: flags["global"] === "true",
|
|
607
616
|
});
|
|
608
617
|
return;
|
|
609
618
|
}
|