open-memex 0.6.0-alpha.5 → 0.6.0

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 CHANGED
@@ -75,13 +75,23 @@ TTY (usage as before when non-interactive); init/uninstall maintain a
75
75
  `.init.json` first-run marker at the data root so the offer is asked once (D50);
76
76
  init ends with a one-line next-step hint (`open-memex add` + ask the agent to
77
77
  recall it) so a first-time user sees what "it works" looks like (D51).
78
+ init also installs the bundled `open-memex` Agent Skill (skills/open-memex/SKILL.md,
79
+ teaches skill-aware agents the CLI: save/search/scope rules/outbox flow) into each
80
+ wired editor's user-level skills dir — ~/.copilot/skills/ (VS Code),
81
+ ~/.cursor/skills/ (Cursor), ~/.config/opencode/skills/ (opencode, per-project MCP
82
+ mode only — with the native plugin the skill is skipped and any previously installed
83
+ one is removed, D55); Visual Studio
84
+ has no skills concept and is skipped. Copy, not symlink (Windows needs no
85
+ Developer Mode); existing skill kept unless --force (D54).
78
86
  `open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]`
79
87
  reverses init — removes the MCP server entry / opencode plugin line / Copilot
80
- instructions section; memory data never touched (D48); no --client → auto-detect
88
+ instructions section / Agent Skill directory; memory data never touched (D48); no --client → auto-detect
81
89
  with an interactive confirm, explicit --client never prompts; `--yes` only skips
82
90
  that confirm; `--global` limits cleanup to user-level. Empty/whitespace-only
83
91
  config files parse as `{}` and are safely populated (D49); non-JSON (JSONC)
84
- files are left untouched with a printed manual snippet (D47).
92
+ files are left untouched with a printed manual snippet (D47). init/uninstall also sweep
93
+ pre-rename `my-o-memory` plugin entries and MCP server keys wherever they touch a
94
+ config, and `open-memex doctor` reports any remaining pre-rename leftovers (F30).
85
95
  The published `open-memex` bin points at `dist/cli.js` (compiled at publish time).
86
96
  From a source checkout, `npm run cli` / `npm run mcp` still run `src/` directly
87
97
  with type-stripping — no build step needed for development.
package/README.md CHANGED
@@ -113,6 +113,12 @@ This installs the `0.5.1` stable release.
113
113
  npm install -g open-memex@alpha
114
114
  ```
115
115
 
116
+ ```sh
117
+ open-memex init
118
+ ```
119
+
120
+ The install isn't complete until you run `open-memex init` — it wires up your editors (VS Code, Cursor, opencode, and Visual Studio for solution projects).
121
+
116
122
  See what's published:
117
123
 
118
124
  ```sh
@@ -180,7 +186,10 @@ npx -y open-memex init --yes
180
186
  With no `--client`, `init` **detects your installed editors and wires them all**
181
187
  — user-level where the editor supports it (VS Code / Cursor MCP config, opencode
182
188
  native plugin), so one init covers every project. Visual Studio joins in when the
183
- project has a solution file. Prefer to pick a single editor? Pass `--client`:
189
+ project has a solution file. It also installs an **Agent Skill** (`open-memex`)
190
+ into each editor's skills folder (not opencode when the native plugin is wired —
191
+ the plugin already provides memory tools), so skill-aware agents can use your memory via
192
+ the CLI with no MCP configuration. Prefer to pick a single editor? Pass `--client`:
184
193
 
185
194
  > **Two different "globals" — don't mix them up.**
186
195
  > - `npm install -g open-memex` installs the *package* globally: it puts the
package/README.zh-CN.md CHANGED
@@ -108,6 +108,12 @@ npm install -g open-memex
108
108
  npm install -g open-memex@alpha
109
109
  ```
110
110
 
111
+ ```sh
112
+ open-memex init
113
+ ```
114
+
115
+ 不跑 `open-memex init` 把编辑器接上,安装就不算完成(支持 VS Code、Cursor、opencode,有 .sln 的项目还支持 Visual Studio)。
116
+
111
117
  查看已发布版本:
112
118
 
113
119
  ```sh
@@ -173,6 +179,9 @@ npx -y open-memex init --yes
173
179
  不带 `--client` 时,`init` 会**自动检测本机装了哪些编辑器,一次全接上**——
174
180
  支持用户级的编辑器走用户级(VS Code / Cursor 的 MCP 配置、opencode 原生插件),
175
181
  一次 init,所有项目通用;项目里有 solution 文件时 Visual Studio 也会一起配。
182
+ 同时会给每个编辑器装一个 **Agent Skill**(`open-memex`,opencode 用原生插件时除外——
183
+ 插件已经提供了记忆工具),懂 skill 的 agent
184
+ 不用配 MCP 也能通过 CLI 用你的记忆。
176
185
  想只配某一个编辑器?加 `--client`:
177
186
 
178
187
  > **两个"全局"不是一回事,别搞混。**
package/dist/cli.js CHANGED
@@ -516,30 +516,31 @@ async function main() {
516
516
  usage(0);
517
517
  return;
518
518
  }
519
- if (cmd === "--help" || cmd === "-h" || cmd === "help")
520
- usage(0);
521
- if (cmd === "--version" || cmd === "-v") {
522
- // package.json sits two levels above this file in both layouts
523
- // (src/cli.ts and dist/cli.js).
524
- const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
525
- const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
526
- console.log(`open-memex ${pkg.version}`);
527
- return;
528
- }
529
519
  // F28: npm runs lifecycle scripts in the background and swallows their
530
520
  // stdout (unless --foreground-scripts), so the D50 postinstall pointer
531
521
  // never reaches the user. Every CLI entry point therefore carries a
532
522
  // one-line nudge on stderr until init has run or been declined — stderr
533
523
  // keeps the MCP stdio protocol (stdout) intact, and the .init.json marker
534
524
  // makes it once-ever. `init`/`uninstall` are excluded (already there /
535
- // nothing to wire).
525
+ // nothing to wire). Placed before the --help/--version early returns:
526
+ // `-v` is the first thing people run after installing.
536
527
  if (cmd !== "init" && cmd !== "uninstall") {
537
528
  const { isFirstRun } = await import("./first-run.js");
538
529
  if (isFirstRun()) {
539
530
  console.error("open-memex: first run? `open-memex init` wires it into your editors " +
540
- "(auto-detects VS Code, Cursor, opencode).");
531
+ "(auto-detects VS Code, Cursor, opencode — and Visual Studio for solution projects).");
541
532
  }
542
533
  }
534
+ if (cmd === "--help" || cmd === "-h" || cmd === "help")
535
+ usage(0);
536
+ if (cmd === "--version" || cmd === "-v") {
537
+ // package.json sits two levels above this file in both layouts
538
+ // (src/cli.ts and dist/cli.js).
539
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
540
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
541
+ console.log(`open-memex ${pkg.version}`);
542
+ return;
543
+ }
543
544
  // Per-command help: `open-memex <command> --help`. Checked before loadConfig()
544
545
  // so it works even when the environment is broken.
545
546
  if (rest.includes("--help") || rest.includes("-h")) {
package/dist/doctor.js CHANGED
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  import { loadConfig, configSource, DEFAULT_CONFIG } from "./config.js";
11
11
  import { paths } from "./paths.js";
12
12
  import { resolveCwdScope } from "./scope.js";
13
+ import { LEGACY_PACKAGE_NAME, opencodeGlobalConfigPath, userMcpConfigPath } from "./init.js";
13
14
  const EXPECTED_TOOLS = [
14
15
  "memory_add",
15
16
  "memory_search",
@@ -278,6 +279,7 @@ export async function runDoctor() {
278
279
  console.log("open-memex doctor");
279
280
  const checks = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), vscodeMcpCheck()];
280
281
  checks.push(await mcpCheck());
282
+ checks.push(legacyCheck());
281
283
  let allOk = true;
282
284
  for (const c of checks) {
283
285
  console.log(` ${c.ok ? "ok " : "FAIL"} ${c.name}: ${c.detail}`);
@@ -287,3 +289,31 @@ export async function runDoctor() {
287
289
  console.log(allOk ? "All checks passed." : "Some checks failed — see above.");
288
290
  return allOk;
289
291
  }
292
+ /**
293
+ * F30: the my-o-memory → open-memex rename left configs and data behind that
294
+ * silently split memories across two data dirs (the exact trap: the old
295
+ * opencode plugin kept loading and writing to the OLD dir). Read-only.
296
+ */
297
+ function legacyCheck() {
298
+ const name = "legacy my-o-memory";
299
+ const found = [];
300
+ const envHome = process.env.MY_O_MEMORY_HOME;
301
+ if (envHome && envHome.includes(LEGACY_PACKAGE_NAME))
302
+ found.push(`MY_O_MEMORY_HOME points at a pre-rename dir (${envHome}) — unset it, then merge old data with \`open-memex migrate --to-v2\``);
303
+ const root = paths().root;
304
+ const legacyDir = path.join(path.dirname(root), LEGACY_PACKAGE_NAME);
305
+ if (fs.existsSync(legacyDir) && fs.statSync(legacyDir).isDirectory())
306
+ found.push(`legacy data dir ${legacyDir} — merge it with \`open-memex migrate --to-v2\``);
307
+ for (const f of [opencodeGlobalConfigPath(), userMcpConfigPath("vscode"), userMcpConfigPath("cursor")]) {
308
+ try {
309
+ if (fs.existsSync(f) && fs.readFileSync(f, "utf8").includes(LEGACY_PACKAGE_NAME))
310
+ found.push(`stale "${LEGACY_PACKAGE_NAME}" reference in ${f} — re-run \`open-memex init --force --yes\``);
311
+ }
312
+ catch {
313
+ /* unreadable — not this check's problem */
314
+ }
315
+ }
316
+ return found.length === 0
317
+ ? { name, ok: true, detail: "no pre-rename leftovers found" }
318
+ : { name, ok: false, detail: found.join("; ") };
319
+ }
package/dist/first-run.js CHANGED
@@ -64,7 +64,7 @@ export async function offerFirstRunInit(runInit) {
64
64
  if (!shouldOfferFirstRun())
65
65
  return "skipped";
66
66
  console.log(`It looks like open-memex hasn't been set up on this machine yet.\n` +
67
- "`open-memex init` wires it into your editors (auto-detects VS Code, Cursor and opencode).\n");
67
+ "`open-memex init` wires it into your editors (auto-detects VS Code, Cursor and opencode — and Visual Studio for solution projects).\n");
68
68
  if (await askYesNo("Run it now? [Y/n] ")) {
69
69
  await runInit();
70
70
  return "initialized";
package/dist/init.js CHANGED
@@ -11,6 +11,14 @@ import { markFirstRunDone, clearFirstRunMarker } from "./first-run.js";
11
11
  const MARKER = "<!-- open-memex -->";
12
12
  /** npm dist-tag carrying the 0.3.x preview line. */
13
13
  const ALPHA_TAG = "open-memex@alpha";
14
+ /**
15
+ * F30: pre-rename package name. The my-o-memory → open-memex rename left
16
+ * editor configs behind that still load the OLD plugin/server — which writes
17
+ * to the OLD data dir, silently splitting the user's memories in two.
18
+ * init, uninstall and doctor treat these leftovers as stale and remove (or
19
+ * report) them wherever they touch a config.
20
+ */
21
+ export const LEGACY_PACKAGE_NAME = "my-o-memory";
14
22
  /**
15
23
  * Resolve the MCP server command to write into client configs.
16
24
  *
@@ -228,13 +236,18 @@ function writeServerEntryFile(file, sectionKey, entry, force, mkdir) {
228
236
  doc = parsed;
229
237
  }
230
238
  const merged = mergeServerEntry(doc, sectionKey, entry, force);
231
- if (merged === "kept") {
239
+ // F30: drop a stale my-o-memory server entry — the rename left configs
240
+ // loading the OLD server, which writes to the OLD data dir.
241
+ const staleRemoved = removeServerEntry(doc, sectionKey, LEGACY_PACKAGE_NAME) === "removed";
242
+ if (merged === "kept" && !staleRemoved) {
232
243
  console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
233
244
  return file;
234
245
  }
235
246
  if (mkdir)
236
247
  fs.mkdirSync(path.dirname(file), { recursive: true });
237
248
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
249
+ if (staleRemoved)
250
+ console.log(` - ${file} (stale my-o-memory server entry removed — package renamed to open-memex)`);
238
251
  console.log(` + ${file}`);
239
252
  return file;
240
253
  }
@@ -304,16 +317,25 @@ function writeOpencodeGlobalPlugin(force) {
304
317
  catch {
305
318
  console.log(` ! ${file} has comments or invalid JSON — left untouched, fix it manually.`);
306
319
  console.log(` To enable open-memex everywhere, add "plugin": ["${url}"] to it.`);
320
+ console.log(` Also remove any "${LEGACY_PACKAGE_NAME}" entries from "plugin" (stale — package renamed).`);
307
321
  return null;
308
322
  }
309
323
  }
310
- if (mergePluginEntry(doc, url, force) === "kept") {
324
+ // F30: the my-o-memory → open-memex rename left the OLD plugin loading
325
+ // alongside the new one — it writes to the OLD data dir, silently splitting
326
+ // memories in two. Drop it wherever init touches the plugin list.
327
+ const staleRemoved = removePluginEntry(doc, LEGACY_PACKAGE_NAME) === "removed";
328
+ const merged = mergePluginEntry(doc, url, force);
329
+ if (merged === "kept" && !staleRemoved) {
311
330
  console.log(` = ${file} already loads the open-memex plugin — left as is (use --force to overwrite)`);
312
331
  return file;
313
332
  }
314
333
  fs.mkdirSync(path.dirname(file), { recursive: true });
315
334
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
316
- console.log(` + ${file} (native plugin — works in every project, no per-project init needed)`);
335
+ if (staleRemoved)
336
+ console.log(` - ${file} (stale my-o-memory plugin entry removed — package renamed to open-memex)`);
337
+ if (merged === "added")
338
+ console.log(` + ${file} (native plugin — works in every project, no per-project init needed)`);
317
339
  return file;
318
340
  }
319
341
  /** opencode MCP config: project-level opencode.jsonc, `type: "local"` + command array (v1 format). */
@@ -335,7 +357,9 @@ function writeOpencodeMcpJson(root, force) {
335
357
  doc = parsed;
336
358
  }
337
359
  const section = (doc["mcp"] ??= {});
338
- if (section["open-memex"] && !force) {
360
+ // F30: drop the pre-rename server entry — it points at the OLD data dir.
361
+ const staleRemoved = removeServerEntry(doc, "mcp", LEGACY_PACKAGE_NAME) === "removed";
362
+ if (section["open-memex"] && !force && !staleRemoved) {
339
363
  console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
340
364
  return file;
341
365
  }
@@ -347,6 +371,8 @@ function writeOpencodeMcpJson(root, force) {
347
371
  enabled: true,
348
372
  };
349
373
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
374
+ if (staleRemoved)
375
+ console.log(` - ${file} (stale my-o-memory MCP entry removed — package renamed to open-memex)`);
350
376
  console.log(` + ${file}`);
351
377
  if (!mc.durable) {
352
378
  console.log(` ! no durable \`open-memex\` on PATH (one-shot npx?) — wrote an npx-based command.`);
@@ -374,7 +400,9 @@ function writeVisualStudioMcpJson(root, force) {
374
400
  }
375
401
  }
376
402
  const section = (doc["servers"] ??= {});
377
- if (section["open-memex"] && !force) {
403
+ // F30: drop the pre-rename server entry — it points at the OLD data dir.
404
+ const staleRemoved = removeServerEntry(doc, "servers", LEGACY_PACKAGE_NAME) === "removed";
405
+ if (section["open-memex"] && !force && !staleRemoved) {
378
406
  console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
379
407
  return file;
380
408
  }
@@ -386,6 +414,8 @@ function writeVisualStudioMcpJson(root, force) {
386
414
  args: mc.args,
387
415
  };
388
416
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
417
+ if (staleRemoved)
418
+ console.log(` - ${file} (stale my-o-memory MCP entry removed — package renamed to open-memex)`);
389
419
  console.log(` + ${file}`);
390
420
  if (!mc.durable) {
391
421
  console.log(` ! no durable \`open-memex\` on PATH (one-shot npx?) — wrote an npx-based command.`);
@@ -497,6 +527,78 @@ async function promptConfigLevel() {
497
527
  rl.close();
498
528
  }
499
529
  }
530
+ // ---------------------------------------------------------------------------
531
+ // D54: Agent Skills. The bundled open-memex skill (skills/open-memex/SKILL.md)
532
+ // teaches skill-aware agents to use open-memex via the CLI. init copies it
533
+ // (not symlinks — Windows needs no Developer Mode) into each wired editor's
534
+ // user-level skills dir; uninstall removes only our directory.
535
+ // D55 exception: opencode with the native plugin (global mode) never gets the
536
+ // skill — the plugin already provides memory tools, and the duplicated CLI
537
+ // guidance only makes the agent chatty. See shouldInstallSkill.
538
+ // ---------------------------------------------------------------------------
539
+ /** Package root, two levels above this module (src/init.ts or dist/init.js). */
540
+ export function packageRoot() {
541
+ return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
542
+ }
543
+ /** Where the bundled skill lives inside the installed package. */
544
+ export function skillSourceDir(pkgRoot = packageRoot()) {
545
+ return path.join(pkgRoot, "skills", "open-memex");
546
+ }
547
+ /**
548
+ * D54: user-level Agent Skills directory for the open-memex skill, per client.
549
+ * null = the client has no skills concept (Visual Studio).
550
+ */
551
+ export function skillTargetDir(client, env = {}) {
552
+ const home = env.home ?? os.homedir();
553
+ switch (client) {
554
+ case "vscode":
555
+ return path.join(home, ".copilot", "skills", "open-memex");
556
+ case "cursor":
557
+ return path.join(home, ".cursor", "skills", "open-memex");
558
+ case "opencode":
559
+ return path.join(opencodeConfigDir(home, env.xdgConfigHome ?? process.env.XDG_CONFIG_HOME), "skills", "open-memex");
560
+ case "visualstudio":
561
+ return null;
562
+ }
563
+ }
564
+ /**
565
+ * Install the bundled skill for one client. Skips when already present unless
566
+ * force (a customized skill is never clobbered silently).
567
+ */
568
+ export function writeSkill(client, opts = {}) {
569
+ const target = skillTargetDir(client, opts);
570
+ if (!target)
571
+ return "unsupported";
572
+ const source = skillSourceDir(opts.pkgRoot ?? packageRoot());
573
+ if (!exists(path.join(source, "SKILL.md")))
574
+ return "missing-source";
575
+ if (exists(target) && !opts.force)
576
+ return "skipped";
577
+ fs.rmSync(target, { recursive: true, force: true });
578
+ fs.cpSync(source, target, { recursive: true });
579
+ return "installed";
580
+ }
581
+ /** Remove the open-memex skill installed by init for one client. */
582
+ export function removeSkill(client, env = {}) {
583
+ const target = skillTargetDir(client, env);
584
+ if (!target || !exists(target))
585
+ return "absent";
586
+ fs.rmSync(target, { recursive: true, force: true });
587
+ return "removed";
588
+ }
589
+ /**
590
+ * D55: whether init installs the Agent Skill for a client. opencode with the
591
+ * native plugin (global mode) already gives the agent memory tools plus
592
+ * keyword capture and context injection — the skill's CLI guidance is pure
593
+ * duplication there and makes the agent chatty (two overlapping instruction
594
+ * sets + a "MCP or CLI?" choice on every memory action). Everywhere else the
595
+ * skill stays as the no-tools fallback.
596
+ */
597
+ export function shouldInstallSkill(client, global) {
598
+ if (client === "opencode" && global)
599
+ return false;
600
+ return true;
601
+ }
500
602
  function exists(p) {
501
603
  try {
502
604
  return fs.existsSync(p);
@@ -639,6 +741,12 @@ export async function initProject(opts) {
639
741
  const root = projectRoot();
640
742
  console.log(`open-memex init — project root: ${root}`);
641
743
  for (const client of clients) {
744
+ // D55: opencode's native plugin supersedes the skill — don't install it
745
+ // there, and remove one a previous init left behind.
746
+ const installSkill = shouldInstallSkill(client, global);
747
+ if (client === "opencode" && !installSkill && removeSkill(client) === "removed") {
748
+ console.log(` - Agent Skill removed for opencode (superseded by the native plugin)`);
749
+ }
642
750
  if (client === "vscode" || client === "cursor") {
643
751
  if (global)
644
752
  writeGlobalMcpJson(client, opts.force);
@@ -661,6 +769,14 @@ export async function initProject(opts) {
661
769
  writeMcpJson(root, client, opts.force);
662
770
  }
663
771
  }
772
+ if (!installSkill)
773
+ continue;
774
+ // D54: Agent Skills — user-level by design (init once), alongside the MCP wiring.
775
+ const skill = writeSkill(client, { force: opts.force });
776
+ if (skill === "installed")
777
+ console.log(` + Agent Skill installed (${skillTargetDir(client)})`);
778
+ else if (skill === "skipped")
779
+ console.log(` - Agent Skill already present for ${client} (use --force to refresh)`);
664
780
  }
665
781
  if (clients.length === 0) {
666
782
  console.log(" - editor setup skipped");
@@ -685,14 +801,16 @@ export async function initProject(opts) {
685
801
  // instructions section. Memory data is never touched.
686
802
  // ---------------------------------------------------------------------------
687
803
  /**
688
- * Pure removal of the open-memex server entry from a parsed config doc.
804
+ * Pure removal of a server entry from a parsed config doc.
689
805
  * Prunes the section when it becomes empty. Mutates `doc`.
806
+ * `key` defaults to "open-memex"; F30 passes LEGACY_PACKAGE_NAME to sweep
807
+ * the pre-rename leftovers.
690
808
  */
691
- export function removeServerEntry(doc, sectionKey) {
809
+ export function removeServerEntry(doc, sectionKey, key = "open-memex") {
692
810
  const section = doc[sectionKey];
693
- if (!section || typeof section !== "object" || !("open-memex" in section))
811
+ if (!section || typeof section !== "object" || !(key in section))
694
812
  return "absent";
695
- delete section["open-memex"];
813
+ delete section[key];
696
814
  if (Object.keys(section).length === 0)
697
815
  delete doc[sectionKey];
698
816
  return "removed";
@@ -744,11 +862,15 @@ function removeServerEntryFile(file, sectionKey) {
744
862
  return null;
745
863
  }
746
864
  const res = removeServerEntry(doc, sectionKey);
747
- if (res === "removed") {
865
+ // F30: also sweep the pre-rename leftovers — uninstalling open-memex while
866
+ // the OLD my-o-memory entry still loads leaves a zombie server on the old data dir.
867
+ const stale = removeServerEntry(doc, sectionKey, LEGACY_PACKAGE_NAME);
868
+ if (res === "removed" || stale === "removed") {
748
869
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
749
- console.log(` - ${file} (open-memex entry removed)`);
870
+ console.log(` - ${file} (memory-plugin server entries removed)`);
871
+ return "removed";
750
872
  }
751
- return res;
873
+ return "absent";
752
874
  }
753
875
  /** Remove the open-memex plugin URL from one opencode user-level config file. */
754
876
  function removeOpencodePluginFile(file) {
@@ -761,11 +883,14 @@ function removeOpencodePluginFile(file) {
761
883
  return null;
762
884
  }
763
885
  const res = removePluginEntry(doc, "open-memex");
764
- if (res === "removed") {
886
+ // F30: also sweep the pre-rename leftover — see removeServerEntryFile.
887
+ const stale = removePluginEntry(doc, LEGACY_PACKAGE_NAME);
888
+ if (res === "removed" || stale === "removed") {
765
889
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
766
- console.log(` - ${file} (open-memex plugin removed)`);
890
+ console.log(` - ${file} (memory-plugin entries removed)`);
891
+ return "removed";
767
892
  }
768
- return res;
893
+ return "absent";
769
894
  }
770
895
  /** Remove the open-memex section from one copilot-instructions.md file. */
771
896
  function removeInstructionsFile(file) {
@@ -855,6 +980,8 @@ export async function uninstallProject(opts) {
855
980
  // Solution-level only — --global is meaningless, same as init.
856
981
  bump(removeServerEntryFile(path.join(root, ".mcp.json"), "servers"));
857
982
  }
983
+ // D54: remove the Agent Skill installed by init.
984
+ bump(removeSkill(client));
858
985
  }
859
986
  // Copilot instructions: init may have written personal (default) or project.
860
987
  if (clients.some((c) => c !== "opencode")) {
package/dist/tools/ops.js CHANGED
@@ -40,7 +40,7 @@ export const TOOL_DESCRIPTIONS = {
40
40
  memory_list: "List memories in a scope, newest first. Useful for browsing what is remembered, or verifying that a save landed.",
41
41
  memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
42
42
  memory_forget: "Delete a memory by id. Use when the user asks to forget something.",
43
- memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start and at task checkpoints, then ask the user which drafts to sync. The user may also trigger this flow by saying 'sync memory' (or '同步记忆').",
43
+ memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync.",
44
44
  memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically.",
45
45
  memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put.",
46
46
  memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted.",
@@ -63,7 +63,7 @@ export const memoryAddArgs = {
63
63
  source: z
64
64
  .string()
65
65
  .optional()
66
- .describe("Where this memory came from. Default: tool. Pass 'inference' for agent-proposed captures at checkpoints (V2-DESIGN §3.5)."),
66
+ .describe("Where this memory came from. Default: tool. Pass 'inference' for agent-proposed captures (V2-DESIGN §3.5)."),
67
67
  };
68
68
  export const memorySearchArgs = {
69
69
  query: z
package/docs/V2-DESIGN.md CHANGED
@@ -1073,6 +1073,68 @@ requirement: personal data never touches third-party services). Benchmarks to tr
1073
1073
  channel the project controls — the CLI — not in npm script output.
1074
1074
  Reported 2026-10-01.*
1075
1075
 
1076
+ - **F29** — Copilot review fixes on PR #11 (0.6.0-alpha.7). (a) The lockfile
1077
+ carried a stray `"version": "0.6.0-alpha.1"` key as a direct child of
1078
+ `packages` (left by the D50 version bump; hand-edited bumps preserved it) —
1079
+ `npm ls --package-lock-only` failed on it. Removed; version bumps now go
1080
+ through `npm pkg set` so npm owns the lockfile format. (b) D53 missed the
1081
+ shared `TOOL_DESCRIPTIONS.memory_status`: it still told agents to call the
1082
+ tool "at session start and at task checkpoints" — the polling this change
1083
+ retires. Now: session start, server-reported drafts, or explicit "sync
1084
+ memory". Same staleness removed from the `source` field hint. (c) Onboarding
1085
+ strings (postinstall note, first-run nudge, bare-CLI offer) now mention
1086
+ Visual Studio auto-detection for solution projects instead of listing only
1087
+ three editors. *Lesson: never hand-edit version fields in package-lock.json.
1088
+ Reported 2026-10-01.*
1089
+
1090
+ - **D54** — Agent Skills support (0.6.0-alpha.8). Ship a bundled
1091
+ `open-memex` skill (`skills/open-memex/SKILL.md`: frontmatter + CLI guide —
1092
+ proactive save, search, scope routing, outbox→submit flow) inside the npm
1093
+ package. `init` copies it (not symlinks — Windows needs no Developer Mode)
1094
+ into each wired editor's user-level skills dir: VS Code →
1095
+ `~/.copilot/skills/open-memex/`, Cursor → `~/.cursor/skills/open-memex/`,
1096
+ opencode → `~/.config/opencode/skills/open-memex/`; Visual Studio has no
1097
+ skills concept and is skipped. User-level by design (D46: init once). An
1098
+ existing skill is never clobbered silently — kept unless `--force`.
1099
+ `uninstall` removes only the `open-memex` skill directory. The skill tells
1100
+ agents to prefer MCP tools (`memory_add` etc.) when available and fall back
1101
+ to the CLI otherwise (with the `npx -y open-memex@latest` prefix when the
1102
+ CLI isn't on PATH). *Rationale: skill-aware agents get memory with zero MCP
1103
+ configuration; the skill is the CLI-shaped complement to the MCP server.
1104
+ Requested by Stone 2026-10-01 after the Agent Skills ecosystem suggestion.*
1105
+
1106
+ - **F30** — Sweep pre-rename `my-o-memory` leftovers (0.6.0-alpha.9). The
1107
+ my-o-memory → open-memex rename left editor configs behind that still load
1108
+ the OLD plugin/server, which writes to the OLD data dir — silently splitting
1109
+ the user's memories in two (found live on Stone's work machine 2026-10-01:
1110
+ the old opencode plugin sat above the new one in `opencode.json`, so
1111
+ opencode and the CLI wrote to different data roots). `init` now drops stale
1112
+ `my-o-memory` plugin entries and `my-o-memory` MCP server keys wherever it
1113
+ touches a config (opencode global plugin list, per-project `opencode.jsonc`,
1114
+ `.mcp.json`, VS Code/Cursor `mcp.json` incl. `--global`), even on the
1115
+ "kept" path; JSONC files it can't parse get a manual-removal hint.
1116
+ `uninstall` sweeps the same leftovers. `doctor` gains a read-only
1117
+ `legacy my-o-memory` check: fails when a legacy data dir exists next to the
1118
+ data root, when a user-level config still references `my-o-memory`, or when
1119
+ `MY_O_MEMORY_HOME` points at a pre-rename dir. *Lesson: a package rename
1120
+ must clean up its own old wiring — the old entry and the new entry are never
1121
+ both valid. Reported 2026-10-01.*
1122
+
1123
+ - **D55** — opencode's native plugin supersedes the Agent Skill
1124
+ (0.6.0-alpha.10). Real-world feedback (Stone 2026-10-01): with both the
1125
+ native plugin's `memory_*` tools and the D54 skill installed, opencode's
1126
+ agent turned chatty — two overlapping instruction sets plus a "use MCP or
1127
+ CLI?" choice on every memory action. No data corruption (identical saves
1128
+ dedupe), but the UX regressed from "one tool call and done" to narration.
1129
+ `init` no longer installs the skill for opencode when the native plugin is
1130
+ wired (`--global`), and removes a previously installed one; the per-project
1131
+ plain-MCP mode keeps the skill as the no-tools fallback. VS Code/Cursor
1132
+ keep the skill — their MCP wiring is more fragile (server-start failures,
1133
+ policy blocks), so the fallback still earns its keep, and the skill already
1134
+ tells agents to prefer MCP tools when available. *Lesson: don't ship two
1135
+ overlapping integrations for the same editor — the agent pays the
1136
+ duplication cost in chatter. Reported 2026-10-01.*
1137
+
1076
1138
  ## Open Questions
1077
1139
 
1078
1140
  _All resolved — see D10 (rename), D11 (type/role split), D12 (explicit pull)._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.6.0-alpha.5",
3
+ "version": "0.6.0",
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",
@@ -5,5 +5,5 @@
5
5
  // prints the pointer. Plain JS, no dependencies.
6
6
  console.log(
7
7
  "\nopen-memex installed. Run `open-memex init` to wire it into your editors — " +
8
- "it auto-detects VS Code, Cursor and opencode.\n",
8
+ "it auto-detects VS Code, Cursor and opencode (and Visual Studio for solution projects).\n",
9
9
  );
@@ -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, parseJsonConfig, removeServerEntry, removePluginEntry, removeInstructionsSection } from "../src/init.ts";
11
+ import { userMcpConfigPath, mergeServerEntry, detectInstalledClients, mergePluginEntry, opencodeGlobalConfigPath, printManualEntryHint, parseJsonConfig, removeServerEntry, removePluginEntry, removeInstructionsSection, LEGACY_PACKAGE_NAME, shouldInstallSkill } from "../src/init.ts";
12
12
 
13
13
  let fails = 0;
14
14
  function ok(name: string, cond: boolean, info?: unknown) {
@@ -451,6 +451,12 @@ const rdoc2: Record<string, unknown> = { servers: { "open-memex": { command: "x"
451
451
  ok("remove prunes empty section", removeServerEntry(rdoc2, "servers") === "removed" && !("servers" in rdoc2));
452
452
  ok("remove absent when no entry", removeServerEntry({ servers: {} }, "servers") === "absent");
453
453
  ok("remove absent when no section", removeServerEntry({}, "servers") === "absent");
454
+ // F30: pre-rename leftovers are swept by key.
455
+ const ldoc: Record<string, unknown> = { servers: { "my-o-memory": { command: "x" }, "open-memex": { command: "y" } } };
456
+ ok("remove drops legacy my-o-memory key, keeps open-memex", removeServerEntry(ldoc, "servers", LEGACY_PACKAGE_NAME) === "removed"
457
+ && !("my-o-memory" in (ldoc["servers"] as Record<string, unknown>))
458
+ && "open-memex" in (ldoc["servers"] as Record<string, unknown>));
459
+ ok("legacy package name is my-o-memory", LEGACY_PACKAGE_NAME === "my-o-memory");
454
460
  // removePluginEntry: removes any open-memex URL, prunes empty array.
455
461
  const rpdoc: Record<string, unknown> = { plugin: ["file:///x/open-memex/src/index.ts", "other-plugin"], theme: "dark" };
456
462
  ok("plugin remove by substring", removePluginEntry(rpdoc, "open-memex") === "removed"
@@ -458,6 +464,10 @@ ok("plugin remove by substring", removePluginEntry(rpdoc, "open-memex") === "rem
458
464
  const rpdoc3: Record<string, unknown> = { plugin: ["file:///open-memex/y"] };
459
465
  ok("plugin remove prunes empty array", removePluginEntry(rpdoc3, "open-memex") === "removed" && !("plugin" in rpdoc3));
460
466
  ok("plugin remove absent", removePluginEntry({ plugin: ["other"] }, "open-memex") === "absent");
467
+ // F30: the rename left the OLD plugin loading next to the new one — init must drop it.
468
+ const lpdoc: Record<string, unknown> = { plugin: ["file:///g/my-o-memory/src/index.ts", "file:///g/open-memex/src/index.ts"] };
469
+ ok("plugin remove drops legacy my-o-memory entry", removePluginEntry(lpdoc, LEGACY_PACKAGE_NAME) === "removed"
470
+ && JSON.stringify(lpdoc["plugin"]) === JSON.stringify(["file:///g/open-memex/src/index.ts"]));
461
471
  // removeInstructionsSection: cuts MARKER..end, "" when nothing remains.
462
472
  ok("instructions remove keeps prior content",
463
473
  removeInstructionsSection("# mine\n\n<!-- open-memex -->\n# OpenMemex memory\n") === "# mine\n");
@@ -488,6 +498,11 @@ fs.writeFileSync(path.join(winBin, "code.cmd"), "@echo off\n");
488
498
  ok("win32 detects vscode via code.cmd",
489
499
  detectInstalledClients({ pathEnv: winBin, home: emptyHome, platform: "win32", root: emptyHome, xdgConfigHome: path.join(emptyHome, ".config") }).includes("vscode"));
490
500
  for (const d of [fakeHome, binDir, detHome, detRoot, emptyHome, winBin]) fs.rmSync(d, { recursive: true, force: true });
501
+ // D55: opencode's native plugin supersedes the skill — init must not install it there.
502
+ ok("opencode+global skips skill", shouldInstallSkill("opencode", true) === false);
503
+ ok("opencode per-project keeps skill fallback", shouldInstallSkill("opencode", false) === true);
504
+ ok("vscode keeps skill", shouldInstallSkill("vscode", true) === true);
505
+ ok("cursor keeps skill", shouldInstallSkill("cursor", false) === true);
491
506
 
492
507
  console.log(fails === 0 ? "\nALL PASS" : `\n${fails} FAILURES`);
493
508
  process.exit(fails === 0 ? 0 : 1);
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: open-memex
3
+ description: Persistent local-first memory for the user and their projects. Use when the user shares facts, preferences, or decisions worth remembering across sessions, or when you need to recall past context before answering.
4
+ ---
5
+
6
+ # open-memex — local memory
7
+
8
+ open-memex gives you persistent memory across sessions. Memories live in local
9
+ markdown files (SQLite FTS index); the `personal` scope never leaves this machine.
10
+
11
+ If `open-memex` MCP tools (`memory_add`, `memory_search`, …) are available in
12
+ this session, prefer them. Otherwise use the CLI below. If `open-memex` is not
13
+ on PATH, prefix commands with `npx -y open-memex@latest`.
14
+
15
+ ## Save — be proactive
16
+
17
+ When the user shares something worth remembering — a fact, preference,
18
+ decision, convention, or error fix — save it without being asked:
19
+
20
+ ```sh
21
+ open-memex add "standup is at 9:30" # current project scope
22
+ open-memex add "I prefer concise diffs" --scope personal # applies everywhere
23
+ ```
24
+
25
+ Keep each memory one self-contained statement. Scope routing: facts about the
26
+ user ("I"/"me") → `personal`; everything else → the current project.
27
+
28
+ ## Recall
29
+
30
+ ```sh
31
+ open-memex search "deployment steps" # keyword search across scopes
32
+ open-memex list # recent memories in the current project
33
+ ```
34
+
35
+ Search before asking the user about past decisions or preferences they may
36
+ have told you before.
37
+
38
+ ## Project outbox → repo (the sync flow)
39
+
40
+ New project memories land in a local outbox (not in git). Review and publish:
41
+
42
+ ```sh
43
+ open-memex sync-status # drafts waiting, repo review state, uncommitted files
44
+ open-memex submit <id> # publish drafts to .ai/open-memex/ + local commit
45
+ ```
46
+
47
+ If any command output reports drafts waiting in the project outbox, run
48
+ `open-memex sync-status` and ask the user which drafts to sync. The user may
49
+ also trigger this flow by saying "sync memory".
50
+
51
+ Run `open-memex <command> --help` for full usage of any command.
package/src/cli.ts CHANGED
@@ -577,16 +577,6 @@ async function main() {
577
577
  if (outcome !== "initialized") usage(0);
578
578
  return;
579
579
  }
580
- if (cmd === "--help" || cmd === "-h" || cmd === "help") usage(0);
581
-
582
- if (cmd === "--version" || cmd === "-v") {
583
- // package.json sits two levels above this file in both layouts
584
- // (src/cli.ts and dist/cli.js).
585
- const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
586
- const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
587
- console.log(`open-memex ${pkg.version}`);
588
- return;
589
- }
590
580
 
591
581
  // F28: npm runs lifecycle scripts in the background and swallows their
592
582
  // stdout (unless --foreground-scripts), so the D50 postinstall pointer
@@ -594,17 +584,29 @@ async function main() {
594
584
  // one-line nudge on stderr until init has run or been declined — stderr
595
585
  // keeps the MCP stdio protocol (stdout) intact, and the .init.json marker
596
586
  // makes it once-ever. `init`/`uninstall` are excluded (already there /
597
- // nothing to wire).
587
+ // nothing to wire). Placed before the --help/--version early returns:
588
+ // `-v` is the first thing people run after installing.
598
589
  if (cmd !== "init" && cmd !== "uninstall") {
599
590
  const { isFirstRun } = await import("./first-run.ts");
600
591
  if (isFirstRun()) {
601
592
  console.error(
602
593
  "open-memex: first run? `open-memex init` wires it into your editors " +
603
- "(auto-detects VS Code, Cursor, opencode).",
594
+ "(auto-detects VS Code, Cursor, opencode — and Visual Studio for solution projects).",
604
595
  );
605
596
  }
606
597
  }
607
598
 
599
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") usage(0);
600
+
601
+ if (cmd === "--version" || cmd === "-v") {
602
+ // package.json sits two levels above this file in both layouts
603
+ // (src/cli.ts and dist/cli.js).
604
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
605
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
606
+ console.log(`open-memex ${pkg.version}`);
607
+ return;
608
+ }
609
+
608
610
  // Per-command help: `open-memex <command> --help`. Checked before loadConfig()
609
611
  // so it works even when the environment is broken.
610
612
  if (rest.includes("--help") || rest.includes("-h")) {
package/src/doctor.ts CHANGED
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  import { loadConfig, configSource, DEFAULT_CONFIG } from "./config.ts";
11
11
  import { paths } from "./paths.ts";
12
12
  import { resolveCwdScope } from "./scope.ts";
13
+ import { LEGACY_PACKAGE_NAME, opencodeGlobalConfigPath, userMcpConfigPath } from "./init.ts";
13
14
 
14
15
  interface Check {
15
16
  name: string;
@@ -299,6 +300,7 @@ export async function runDoctor(): Promise<boolean> {
299
300
  console.log("open-memex doctor");
300
301
  const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), vscodeMcpCheck()];
301
302
  checks.push(await mcpCheck());
303
+ checks.push(legacyCheck());
302
304
  let allOk = true;
303
305
  for (const c of checks) {
304
306
  console.log(` ${c.ok ? "ok " : "FAIL"} ${c.name}: ${c.detail}`);
@@ -307,3 +309,33 @@ export async function runDoctor(): Promise<boolean> {
307
309
  console.log(allOk ? "All checks passed." : "Some checks failed — see above.");
308
310
  return allOk;
309
311
  }
312
+
313
+ /**
314
+ * F30: the my-o-memory → open-memex rename left configs and data behind that
315
+ * silently split memories across two data dirs (the exact trap: the old
316
+ * opencode plugin kept loading and writing to the OLD dir). Read-only.
317
+ */
318
+ function legacyCheck(): Check {
319
+ const name = "legacy my-o-memory";
320
+ const found: string[] = [];
321
+ const envHome = process.env.MY_O_MEMORY_HOME;
322
+ if (envHome && envHome.includes(LEGACY_PACKAGE_NAME))
323
+ found.push(
324
+ `MY_O_MEMORY_HOME points at a pre-rename dir (${envHome}) — unset it, then merge old data with \`open-memex migrate --to-v2\``,
325
+ );
326
+ const root = paths().root;
327
+ const legacyDir = path.join(path.dirname(root), LEGACY_PACKAGE_NAME);
328
+ if (fs.existsSync(legacyDir) && fs.statSync(legacyDir).isDirectory())
329
+ found.push(`legacy data dir ${legacyDir} — merge it with \`open-memex migrate --to-v2\``);
330
+ for (const f of [opencodeGlobalConfigPath(), userMcpConfigPath("vscode"), userMcpConfigPath("cursor")]) {
331
+ try {
332
+ if (fs.existsSync(f) && fs.readFileSync(f, "utf8").includes(LEGACY_PACKAGE_NAME))
333
+ found.push(`stale "${LEGACY_PACKAGE_NAME}" reference in ${f} — re-run \`open-memex init --force --yes\``);
334
+ } catch {
335
+ /* unreadable — not this check's problem */
336
+ }
337
+ }
338
+ return found.length === 0
339
+ ? { name, ok: true, detail: "no pre-rename leftovers found" }
340
+ : { name, ok: false, detail: found.join("; ") };
341
+ }
package/src/first-run.ts CHANGED
@@ -84,7 +84,7 @@ export async function offerFirstRunInit(
84
84
  if (!shouldOfferFirstRun()) return "skipped";
85
85
  console.log(
86
86
  `It looks like open-memex hasn't been set up on this machine yet.\n` +
87
- "`open-memex init` wires it into your editors (auto-detects VS Code, Cursor and opencode).\n",
87
+ "`open-memex init` wires it into your editors (auto-detects VS Code, Cursor and opencode — and Visual Studio for solution projects).\n",
88
88
  );
89
89
  if (await askYesNo("Run it now? [Y/n] ")) {
90
90
  await runInit();
package/src/init.ts CHANGED
@@ -14,6 +14,15 @@ const MARKER = "<!-- open-memex -->";
14
14
  /** npm dist-tag carrying the 0.3.x preview line. */
15
15
  const ALPHA_TAG = "open-memex@alpha";
16
16
 
17
+ /**
18
+ * F30: pre-rename package name. The my-o-memory → open-memex rename left
19
+ * editor configs behind that still load the OLD plugin/server — which writes
20
+ * to the OLD data dir, silently splitting the user's memories in two.
21
+ * init, uninstall and doctor treat these leftovers as stale and remove (or
22
+ * report) them wherever they touch a config.
23
+ */
24
+ export const LEGACY_PACKAGE_NAME = "my-o-memory";
25
+
17
26
  export interface McpCommand {
18
27
  command: string;
19
28
  args: string[];
@@ -254,12 +263,17 @@ function writeServerEntryFile(
254
263
  doc = parsed;
255
264
  }
256
265
  const merged = mergeServerEntry(doc, sectionKey, entry, force);
257
- if (merged === "kept") {
266
+ // F30: drop a stale my-o-memory server entry — the rename left configs
267
+ // loading the OLD server, which writes to the OLD data dir.
268
+ const staleRemoved = removeServerEntry(doc, sectionKey, LEGACY_PACKAGE_NAME) === "removed";
269
+ if (merged === "kept" && !staleRemoved) {
258
270
  console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
259
271
  return file;
260
272
  }
261
273
  if (mkdir) fs.mkdirSync(path.dirname(file), { recursive: true });
262
274
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
275
+ if (staleRemoved)
276
+ console.log(` - ${file} (stale my-o-memory server entry removed — package renamed to open-memex)`);
263
277
  console.log(` + ${file}`);
264
278
  return file;
265
279
  }
@@ -336,16 +350,25 @@ function writeOpencodeGlobalPlugin(force: boolean): string | null {
336
350
  } catch {
337
351
  console.log(` ! ${file} has comments or invalid JSON — left untouched, fix it manually.`);
338
352
  console.log(` To enable open-memex everywhere, add "plugin": ["${url}"] to it.`);
353
+ console.log(` Also remove any "${LEGACY_PACKAGE_NAME}" entries from "plugin" (stale — package renamed).`);
339
354
  return null;
340
355
  }
341
356
  }
342
- if (mergePluginEntry(doc, url, force) === "kept") {
357
+ // F30: the my-o-memory → open-memex rename left the OLD plugin loading
358
+ // alongside the new one — it writes to the OLD data dir, silently splitting
359
+ // memories in two. Drop it wherever init touches the plugin list.
360
+ const staleRemoved = removePluginEntry(doc, LEGACY_PACKAGE_NAME) === "removed";
361
+ const merged = mergePluginEntry(doc, url, force);
362
+ if (merged === "kept" && !staleRemoved) {
343
363
  console.log(` = ${file} already loads the open-memex plugin — left as is (use --force to overwrite)`);
344
364
  return file;
345
365
  }
346
366
  fs.mkdirSync(path.dirname(file), { recursive: true });
347
367
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
348
- console.log(` + ${file} (native plugin — works in every project, no per-project init needed)`);
368
+ if (staleRemoved)
369
+ console.log(` - ${file} (stale my-o-memory plugin entry removed — package renamed to open-memex)`);
370
+ if (merged === "added")
371
+ console.log(` + ${file} (native plugin — works in every project, no per-project init needed)`);
349
372
  return file;
350
373
  }
351
374
 
@@ -368,7 +391,9 @@ function writeOpencodeMcpJson(root: string, force: boolean): string | null {
368
391
  doc = parsed;
369
392
  }
370
393
  const section = ((doc["mcp"] ??= {}) as Record<string, unknown>);
371
- if (section["open-memex"] && !force) {
394
+ // F30: drop the pre-rename server entry — it points at the OLD data dir.
395
+ const staleRemoved = removeServerEntry(doc, "mcp", LEGACY_PACKAGE_NAME) === "removed";
396
+ if (section["open-memex"] && !force && !staleRemoved) {
372
397
  console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
373
398
  return file;
374
399
  }
@@ -380,6 +405,8 @@ function writeOpencodeMcpJson(root: string, force: boolean): string | null {
380
405
  enabled: true,
381
406
  };
382
407
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
408
+ if (staleRemoved)
409
+ console.log(` - ${file} (stale my-o-memory MCP entry removed — package renamed to open-memex)`);
383
410
  console.log(` + ${file}`);
384
411
  if (!mc.durable) {
385
412
  console.log(` ! no durable \`open-memex\` on PATH (one-shot npx?) — wrote an npx-based command.`);
@@ -408,7 +435,9 @@ function writeVisualStudioMcpJson(root: string, force: boolean): string | null {
408
435
  }
409
436
  }
410
437
  const section = ((doc["servers"] ??= {}) as Record<string, unknown>);
411
- if (section["open-memex"] && !force) {
438
+ // F30: drop the pre-rename server entry — it points at the OLD data dir.
439
+ const staleRemoved = removeServerEntry(doc, "servers", LEGACY_PACKAGE_NAME) === "removed";
440
+ if (section["open-memex"] && !force && !staleRemoved) {
412
441
  console.log(` = ${file} already configures open-memex — left as is (use --force to overwrite)`);
413
442
  return file;
414
443
  }
@@ -420,6 +449,8 @@ function writeVisualStudioMcpJson(root: string, force: boolean): string | null {
420
449
  args: mc.args,
421
450
  };
422
451
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
452
+ if (staleRemoved)
453
+ console.log(` - ${file} (stale my-o-memory MCP entry removed — package renamed to open-memex)`);
423
454
  console.log(` + ${file}`);
424
455
  if (!mc.durable) {
425
456
  console.log(` ! no durable \`open-memex\` on PATH (one-shot npx?) — wrote an npx-based command.`);
@@ -553,6 +584,93 @@ export interface DetectEnv {
553
584
  xdgConfigHome?: string;
554
585
  }
555
586
 
587
+ // ---------------------------------------------------------------------------
588
+ // D54: Agent Skills. The bundled open-memex skill (skills/open-memex/SKILL.md)
589
+ // teaches skill-aware agents to use open-memex via the CLI. init copies it
590
+ // (not symlinks — Windows needs no Developer Mode) into each wired editor's
591
+ // user-level skills dir; uninstall removes only our directory.
592
+ // D55 exception: opencode with the native plugin (global mode) never gets the
593
+ // skill — the plugin already provides memory tools, and the duplicated CLI
594
+ // guidance only makes the agent chatty. See shouldInstallSkill.
595
+ // ---------------------------------------------------------------------------
596
+
597
+ /** Package root, two levels above this module (src/init.ts or dist/init.js). */
598
+ export function packageRoot(): string {
599
+ return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
600
+ }
601
+
602
+ /** Where the bundled skill lives inside the installed package. */
603
+ export function skillSourceDir(pkgRoot: string = packageRoot()): string {
604
+ return path.join(pkgRoot, "skills", "open-memex");
605
+ }
606
+
607
+ /**
608
+ * D54: user-level Agent Skills directory for the open-memex skill, per client.
609
+ * null = the client has no skills concept (Visual Studio).
610
+ */
611
+ export function skillTargetDir(
612
+ client: InitClient,
613
+ env: DetectEnv = {},
614
+ ): string | null {
615
+ const home = env.home ?? os.homedir();
616
+ switch (client) {
617
+ case "vscode":
618
+ return path.join(home, ".copilot", "skills", "open-memex");
619
+ case "cursor":
620
+ return path.join(home, ".cursor", "skills", "open-memex");
621
+ case "opencode":
622
+ return path.join(
623
+ opencodeConfigDir(home, env.xdgConfigHome ?? process.env.XDG_CONFIG_HOME),
624
+ "skills",
625
+ "open-memex",
626
+ );
627
+ case "visualstudio":
628
+ return null;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Install the bundled skill for one client. Skips when already present unless
634
+ * force (a customized skill is never clobbered silently).
635
+ */
636
+ export function writeSkill(
637
+ client: InitClient,
638
+ opts: { force?: boolean; pkgRoot?: string } & DetectEnv = {},
639
+ ): "installed" | "skipped" | "unsupported" | "missing-source" {
640
+ const target = skillTargetDir(client, opts);
641
+ if (!target) return "unsupported";
642
+ const source = skillSourceDir(opts.pkgRoot ?? packageRoot());
643
+ if (!exists(path.join(source, "SKILL.md"))) return "missing-source";
644
+ if (exists(target) && !opts.force) return "skipped";
645
+ fs.rmSync(target, { recursive: true, force: true });
646
+ fs.cpSync(source, target, { recursive: true });
647
+ return "installed";
648
+ }
649
+
650
+ /** Remove the open-memex skill installed by init for one client. */
651
+ export function removeSkill(
652
+ client: InitClient,
653
+ env: DetectEnv = {},
654
+ ): "removed" | "absent" {
655
+ const target = skillTargetDir(client, env);
656
+ if (!target || !exists(target)) return "absent";
657
+ fs.rmSync(target, { recursive: true, force: true });
658
+ return "removed";
659
+ }
660
+
661
+ /**
662
+ * D55: whether init installs the Agent Skill for a client. opencode with the
663
+ * native plugin (global mode) already gives the agent memory tools plus
664
+ * keyword capture and context injection — the skill's CLI guidance is pure
665
+ * duplication there and makes the agent chatty (two overlapping instruction
666
+ * sets + a "MCP or CLI?" choice on every memory action). Everywhere else the
667
+ * skill stays as the no-tools fallback.
668
+ */
669
+ export function shouldInstallSkill(client: InitClient, global: boolean): boolean {
670
+ if (client === "opencode" && global) return false;
671
+ return true;
672
+ }
673
+
556
674
  function exists(p: string): boolean {
557
675
  try {
558
676
  return fs.existsSync(p);
@@ -711,6 +829,12 @@ export async function initProject(opts: {
711
829
  const root = projectRoot();
712
830
  console.log(`open-memex init — project root: ${root}`);
713
831
  for (const client of clients) {
832
+ // D55: opencode's native plugin supersedes the skill — don't install it
833
+ // there, and remove one a previous init left behind.
834
+ const installSkill = shouldInstallSkill(client, global);
835
+ if (client === "opencode" && !installSkill && removeSkill(client) === "removed") {
836
+ console.log(` - Agent Skill removed for opencode (superseded by the native plugin)`);
837
+ }
714
838
  if (client === "vscode" || client === "cursor") {
715
839
  if (global) writeGlobalMcpJson(client, opts.force);
716
840
  else writeMcpJson(root, client, opts.force);
@@ -726,6 +850,12 @@ export async function initProject(opts: {
726
850
  writeMcpJson(root, client, opts.force);
727
851
  }
728
852
  }
853
+ if (!installSkill) continue;
854
+ // D54: Agent Skills — user-level by design (init once), alongside the MCP wiring.
855
+ const skill = writeSkill(client, { force: opts.force });
856
+ if (skill === "installed") console.log(` + Agent Skill installed (${skillTargetDir(client)})`);
857
+ else if (skill === "skipped")
858
+ console.log(` - Agent Skill already present for ${client} (use --force to refresh)`);
729
859
  }
730
860
  if (clients.length === 0) {
731
861
  console.log(" - editor setup skipped");
@@ -751,16 +881,19 @@ export async function initProject(opts: {
751
881
  // ---------------------------------------------------------------------------
752
882
 
753
883
  /**
754
- * Pure removal of the open-memex server entry from a parsed config doc.
884
+ * Pure removal of a server entry from a parsed config doc.
755
885
  * Prunes the section when it becomes empty. Mutates `doc`.
886
+ * `key` defaults to "open-memex"; F30 passes LEGACY_PACKAGE_NAME to sweep
887
+ * the pre-rename leftovers.
756
888
  */
757
889
  export function removeServerEntry(
758
890
  doc: Record<string, unknown>,
759
891
  sectionKey: string,
892
+ key = "open-memex",
760
893
  ): "removed" | "absent" {
761
894
  const section = doc[sectionKey];
762
- if (!section || typeof section !== "object" || !("open-memex" in section)) return "absent";
763
- delete (section as Record<string, unknown>)["open-memex"];
895
+ if (!section || typeof section !== "object" || !(key in section)) return "absent";
896
+ delete (section as Record<string, unknown>)[key];
764
897
  if (Object.keys(section as Record<string, unknown>).length === 0) delete doc[sectionKey];
765
898
  return "removed";
766
899
  }
@@ -810,11 +943,15 @@ function removeServerEntryFile(file: string, sectionKey: string): "removed" | "a
810
943
  return null;
811
944
  }
812
945
  const res = removeServerEntry(doc, sectionKey);
813
- if (res === "removed") {
946
+ // F30: also sweep the pre-rename leftovers — uninstalling open-memex while
947
+ // the OLD my-o-memory entry still loads leaves a zombie server on the old data dir.
948
+ const stale = removeServerEntry(doc, sectionKey, LEGACY_PACKAGE_NAME);
949
+ if (res === "removed" || stale === "removed") {
814
950
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
815
- console.log(` - ${file} (open-memex entry removed)`);
951
+ console.log(` - ${file} (memory-plugin server entries removed)`);
952
+ return "removed";
816
953
  }
817
- return res;
954
+ return "absent";
818
955
  }
819
956
 
820
957
  /** Remove the open-memex plugin URL from one opencode user-level config file. */
@@ -827,11 +964,14 @@ function removeOpencodePluginFile(file: string): "removed" | "absent" | null {
827
964
  return null;
828
965
  }
829
966
  const res = removePluginEntry(doc, "open-memex");
830
- if (res === "removed") {
967
+ // F30: also sweep the pre-rename leftover — see removeServerEntryFile.
968
+ const stale = removePluginEntry(doc, LEGACY_PACKAGE_NAME);
969
+ if (res === "removed" || stale === "removed") {
831
970
  fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
832
- console.log(` - ${file} (open-memex plugin removed)`);
971
+ console.log(` - ${file} (memory-plugin entries removed)`);
972
+ return "removed";
833
973
  }
834
- return res;
974
+ return "absent";
835
975
  }
836
976
 
837
977
  /** Remove the open-memex section from one copilot-instructions.md file. */
@@ -918,6 +1058,8 @@ export async function uninstallProject(opts: {
918
1058
  // Solution-level only — --global is meaningless, same as init.
919
1059
  bump(removeServerEntryFile(path.join(root, ".mcp.json"), "servers"));
920
1060
  }
1061
+ // D54: remove the Agent Skill installed by init.
1062
+ bump(removeSkill(client));
921
1063
  }
922
1064
  // Copilot instructions: init may have written personal (default) or project.
923
1065
  if (clients.some((c) => c !== "opencode")) {
package/src/tools/ops.ts CHANGED
@@ -71,7 +71,7 @@ export const TOOL_DESCRIPTIONS = {
71
71
  "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
72
72
  memory_forget: "Delete a memory by id. Use when the user asks to forget something.",
73
73
  memory_status:
74
- "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start and at task checkpoints, then ask the user which drafts to sync. The user may also trigger this flow by saying 'sync memory' (or '同步记忆').",
74
+ "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync.",
75
75
  memory_submit:
76
76
  "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically.",
77
77
  memory_propose:
@@ -104,7 +104,7 @@ export const memoryAddArgs = {
104
104
  .string()
105
105
  .optional()
106
106
  .describe(
107
- "Where this memory came from. Default: tool. Pass 'inference' for agent-proposed captures at checkpoints (V2-DESIGN §3.5).",
107
+ "Where this memory came from. Default: tool. Pass 'inference' for agent-proposed captures (V2-DESIGN §3.5).",
108
108
  ),
109
109
  };
110
110
  export type MemoryAddArgs = z.infer<z.ZodObject<typeof memoryAddArgs>>;