open-memex 0.5.1 → 0.6.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -64,6 +64,13 @@ previews keyword capture without writing, `open-memex doctor` runs health checks
64
64
  (node version, config, scope resolution, storage writability, VS Code MCP enablement, MCP handshake).
65
65
  `open-memex init` with no --client auto-detects and wires every installed editor
66
66
  (`--yes` skips, scripts never prompt); `open-memex config set <key> <value>` edits settings after install.
67
+ `npm install -g` prints a pointer to `open-memex init` via a postinstall script
68
+ (print-only — postinstall must never prompt, it runs in CI/Docker; D50).
69
+ Bare `open-memex` on a machine where init never completed offers to run it on a
70
+ TTY (usage as before when non-interactive); init/uninstall maintain a
71
+ `.init.json` first-run marker at the data root so the offer is asked once (D50);
72
+ init ends with a one-line next-step hint (`open-memex add` + ask the agent to
73
+ recall it) so a first-time user sees what "it works" looks like (D51).
67
74
  `open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]`
68
75
  reverses init — removes the MCP server entry / opencode plugin line / Copilot
69
76
  instructions section; memory data never touched (D48); no --client → auto-detect
package/README.md CHANGED
@@ -189,6 +189,14 @@ project has a solution file. Prefer to pick a single editor? Pass `--client`:
189
189
  > project: init once, the wiring works in every project. It works the same
190
190
  > whether the package was installed globally or run via npx.
191
191
 
192
+ Installing the package also prints a reminder to run `open-memex init` — the
193
+ wiring is a separate step. And if you run bare `open-memex` on a machine where
194
+ init never completed, it offers to run it for you (only on an interactive
195
+ terminal; scripts and CI just see the usual usage text). When init finishes,
196
+ it prints one concrete next step — save a memory with `open-memex add`, then
197
+ ask your agent to recall it — so a first-time user sees what "it works" looks
198
+ like.
199
+
192
200
  **VS Code** (Copilot):
193
201
 
194
202
  ```sh
@@ -508,8 +516,9 @@ branches and PRs. Nothing moves without you naming it.
508
516
 
509
517
  In an AI chat with the MCP server connected, just say **"sync memory"**
510
518
  (or "同步记忆") — the agent runs the status check, summarizes the outbox drafts,
511
- and asks which ones to sync. The agent also proposes this on its own at session
512
- start and at work checkpoints.
519
+ and asks which ones to sync. The server also tells the agent on its own: at
520
+ session start the handshake reports how many drafts are waiting, and every
521
+ memory-changing tool result carries the current count when it is non-zero.
513
522
 
514
523
  ```sh
515
524
  open-memex sync-status
@@ -563,8 +572,8 @@ open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N]
563
572
  # (decisions, constraints, lessons, gotchas, howtos). Prints markdown;
564
573
  # -o writes it to a file. You review and merge by hand — open-memex
565
574
  # never rewrites your AGENTS.md on its own. The snippet ends with a
566
- # "memory hygiene" section (§3.5 checkpoint guidance) so agents reading
567
- # AGENTS.md learn to propose distilled captures at checkpoints.
575
+ # "memory hygiene" section (§3.5 distillation guidance) so agents reading
576
+ # AGENTS.md learn to propose distilled captures when a task ends.
568
577
 
569
578
  open-memex propose <id...> --to project [--local-approve]
570
579
  # propose one or several personal memories at once (one branch, one PR);
@@ -619,8 +628,9 @@ server with cwd set to your project root (`init` handles this for you).
619
628
  > **Note:** MCP is request/response — it gives the agent tools, not the opencode
620
629
  > plugin's automatic keyword capture or first-turn context injection. Proactive
621
630
  > memory use depends on the agent's instructions: the server sends session-start
622
- > guidance (call `memory_status` at session start and at checkpoints) in the MCP
623
- > handshake `instructions`, and `init` writes the fuller version into the
631
+ > guidance in the MCP handshake `instructions` (including the live outbox draft
632
+ > count at session start, plus the pending count appended to memory-changing
633
+ > tool results when non-zero), and `init` writes the fuller version into the
624
634
  > editor's instruction files. Both are advisory — no MCP consumer offers a hard
625
635
  > session-start hook.
626
636
 
@@ -636,8 +646,8 @@ outbox → `sync-status` → `submit` (local branch+commit, push/PR on your Yes)
636
646
  `export` / `import` archive for user portability (Markdown + manifest, no walled
637
647
  garden; private excluded by default, `-a` / `--all` for full migration);
638
648
  distill-to-AGENTS.md assist (`distill-agents`, propose-only — you merge by hand);
639
- §3.5 checkpoint distillation in the MCP handshake + init instructions (the agent
640
- proposes 1–3 captures at checkpoints, the human decides); 1–2 colleague pilot.
649
+ §3.5 distillation in the MCP handshake + init instructions (the agent proposes
650
+ 1–3 captures when a task ends, the human decides); 1–2 colleague pilot.
641
651
 
642
652
  **`0.5.0` (stable):** init UX pass — `init --global` writes the editor wiring
643
653
  once at user level (D45); bare `init` auto-detects installed editors and wires
package/README.zh-CN.md CHANGED
@@ -181,6 +181,12 @@ npx -y open-memex init --yes
181
181
  > - `init --global` 是把*编辑器配置*写到用户级而不是项目里:init 一次,
182
182
  > 每个项目都生效。不管包是全局安装的还是用 npx 临时跑的,效果一样。
183
183
 
184
+ 装完包还会打印一句提醒,让你跑 `open-memex init`——接线是独立的一步。
185
+ 如果你在从没跑过 init 的机器上直接敲 `open-memex`,它会问你要不要现在
186
+ init(只在交互终端里问;脚本和 CI 里看到的还是原来的 usage)。
187
+ init 跑完会打印一个具体的下一步——用 `open-memex add` 存一条记忆,再让
188
+ agent 回忆它——让第一次用的用户一眼看到"跑起来了"是什么样子。
189
+
184
190
  **VS Code**(Copilot):
185
191
 
186
192
  ```sh
@@ -482,8 +488,8 @@ project 草稿先住在 **appdata outbox**(git 看不见、跟分支无关)
482
488
  没经过你点名,什么都不会动。
483
489
 
484
490
  在接了 MCP 服务器的 AI 对话里,直接说 **"同步记忆"**(或 "sync memory")——
485
- agent 会查状态、把 outbox 草稿逐条摘要、问你同步哪几条。agent 也会在新对话
486
- 开始和任务检查点主动提这件事。
491
+ agent 会查状态、把 outbox 草稿逐条摘要、问你同步哪几条。服务器也会主动告诉
492
+ agent:新对话开始时握手里带待审草稿数,每次改记忆的 tool 返回里也带当前数(为零时不带)。
487
493
 
488
494
  ```sh
489
495
  open-memex sync-status
@@ -530,7 +536,7 @@ open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N]
530
536
  # 把项目记忆(decision/constraint/lesson/gotcha/howto)提炼成
531
537
  # AGENTS.md 片段。默认打印到 stdout;-o 写文件。人工审阅后手工合并——
532
538
  # open-memex 永不自动改写你的 AGENTS.md。片段末尾带一段"记忆卫生"
533
- # (§3.5 检查点指引),让读 AGENTS.md 的 agent 学会在检查点提议蒸馏捕获。
539
+ # (§3.5 蒸馏指引),让读 AGENTS.md 的 agent 学会在任务结束时提议蒸馏捕获。
534
540
 
535
541
  open-memex propose <id...> --to project [--local-approve]
536
542
  # 一次 propose 一条或多条(一个分支、一个 PR),每条独立新 id。
@@ -582,9 +588,9 @@ project scope 从进程工作目录解析,所以配置 server 时 cwd 要指
582
588
  > **注意:** MCP 是请求/响应式的——它给 agent 提供 tools,但没有 opencode
583
589
  > 插件的关键词自动捕获和首轮上下文注入。想让 agent 主动用记忆,
584
590
  > 靠的是 agent 的 instructions:服务器在 MCP 握手的 `instructions` 里自带
585
- > session-start 指引(开场调 `memory_status`、检查点再调),`init` 则把更完整
586
- > 的版本写进编辑器的 instruction 文件。两者都是建议性的——MCP 客户端没有
587
- > 强制的 session-start hook。
591
+ > session-start 指引(含开场时的 outbox 待审草稿数;改记忆的 tool 返回里也会
592
+ > 带当前数,为零时不带),`init` 则把更完整的版本写进编辑器的 instruction
593
+ > 文件。两者都是建议性的——MCP 客户端没有强制的 session-start hook。
588
594
 
589
595
  ## 路线图(Roadmap)
590
596
 
@@ -598,8 +604,8 @@ project scope 从进程工作目录解析,所以配置 server 时 cwd 要指
598
604
  `export` / `import` 归档做用户可携带(Markdown + manifest,不造围墙花园;
599
605
  private 默认不导出,`-a` / `--all` 全量迁移);
600
606
  distill-to-AGENTS.md 辅助(`distill-agents`,只提议不改写——人工合并);
601
- §3.5 检查点蒸馏写进 MCP 握手指令和 init 指令文件
602
- (agent 在检查点提议 1–3 条捕获,人来定);找 1–2 个同事做 pilot。
607
+ §3.5 蒸馏写进 MCP 握手指令和 init 指令文件
608
+ (agent 在任务结束时提议 1–3 条捕获,人来定);找 1–2 个同事做 pilot。
603
609
 
604
610
  **`0.5.0`(稳定版):** init 体验整修——`init --global` 一次写好用户级编辑器接线(D45);裸 `init` 自动检测已装编辑器并一次全接上(D46);非标准 JSON 配置不再报错,而是原样保留并打印手贴片段(D47);`uninstall` 逆转 `init` 且永不碰记忆数据(D48);空配置文件按空白处理、不再误判为损坏(D49)。"一份记忆,所有 Agent 通用":同一台机器上的每个编辑器,经由同一个 MCP 接口读写同一份记忆。
605
611
 
package/dist/cli.js CHANGED
@@ -496,7 +496,27 @@ function printMcpConfig(client) {
496
496
  }
497
497
  async function main() {
498
498
  const [cmd, ...rest] = process.argv.slice(2);
499
- if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help")
499
+ if (!cmd) {
500
+ // D50: fresh machine + interactive terminal → offer init instead of bare usage.
501
+ // Non-interactive (CI/scripts/pipes) prints usage exactly as before.
502
+ const { offerFirstRunInit } = await import("./first-run.js");
503
+ const outcome = await offerFirstRunInit(async () => {
504
+ // Re-exec `open-memex init` as a child with inherited stdio instead of
505
+ // calling initProject() in-process: the offer's readline already
506
+ // consumed stdin's buffer, and a second readline on the same stream
507
+ // would see EOF on burst input instead of the user's next answers.
508
+ const { spawnSync } = await import("node:child_process");
509
+ const r = spawnSync(process.execPath, [...process.execArgv, fileURLToPath(import.meta.url), "init"], { stdio: "inherit" });
510
+ if (r.error)
511
+ throw r.error;
512
+ if ((r.status ?? 1) !== 0)
513
+ process.exit(r.status ?? 1);
514
+ });
515
+ if (outcome !== "initialized")
516
+ usage(0);
517
+ return;
518
+ }
519
+ if (cmd === "--help" || cmd === "-h" || cmd === "help")
500
520
  usage(0);
501
521
  if (cmd === "--version" || cmd === "-v") {
502
522
  // package.json sits two levels above this file in both layouts
@@ -53,10 +53,11 @@ export function distillAgentsMarkdown(opts) {
53
53
  }
54
54
  // D43 — §3.5 memory-hygiene footer (double insurance for opencode users,
55
55
  // who never see the MCP handshake / init instructions): teach the agent
56
- // reading this AGENTS.md to propose distilled captures at checkpoints.
56
+ // reading this AGENTS.md to propose distilled captures when a task ends
57
+ // (D53: the checkpoint mechanism is gone).
57
58
  lines.push(`### Memory hygiene (open-memex)`);
58
59
  lines.push(``);
59
- lines.push(`- At checkpoints (session start, end of a work chunk, after the user commits),`, ` distill the session: propose 1–3 short memories capturing the useful`, ` conclusion — what was learned or decided, how an issue was resolved, what`, ` to avoid, where the authoritative doc lives — not the raw transcript.`, ` Save nothing without user approval.`, `- If the knowledge already lives in project docs, save a \`reference\` memory`, ` pointing at the doc instead of copying it.`);
60
+ lines.push(`- When you finish a task the user would describe in one sentence, distill`, ` the session: propose 1–3 short memories capturing the useful`, ` conclusion — what was learned or decided, how an issue was resolved, what`, ` to avoid, where the authoritative doc lives — not the raw transcript.`, ` Save nothing without user approval.`, `- If the knowledge already lives in project docs, save a \`reference\` memory`, ` pointing at the doc instead of copying it.`);
60
61
  lines.push(``);
61
62
  return lines.join("\n");
62
63
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * D50: first-run init offer. `npm install -g open-memex` only puts the CLI on
3
+ * PATH — the editor wiring is `init`'s job, and a clean reinstall wipes it.
4
+ * When bare `open-memex` runs on a machine where init never completed, offer
5
+ * to run it instead of just printing usage.
6
+ *
7
+ * The "asked" state is a marker file at the data root (`init` writes it on
8
+ * success, a declined offer writes it too), so the question is asked exactly
9
+ * once. `uninstall` removes it — unwiring is the reverse of init, so the next
10
+ * bare run offers to wire again. The marker is a dotfile: export builds from
11
+ * DB rows, never by walking the data root, so it can't leak into bundles.
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import readline from "node:readline";
16
+ import { dataRootPath } from "./paths.js";
17
+ const MARKER = ".init.json";
18
+ export function firstRunMarkerPath(root = dataRootPath()) {
19
+ return path.join(root, MARKER);
20
+ }
21
+ /** True when init has neither run nor been declined on this machine. */
22
+ export function isFirstRun(root) {
23
+ return !fs.existsSync(firstRunMarkerPath(root ?? dataRootPath()));
24
+ }
25
+ /** Record the outcome — best effort; a missing marker just asks again next time. */
26
+ export function markFirstRunDone(state) {
27
+ const file = firstRunMarkerPath();
28
+ try {
29
+ fs.mkdirSync(path.dirname(file), { recursive: true });
30
+ fs.writeFileSync(file, JSON.stringify({ v: 1, state, at: new Date().toISOString() }) + "\n");
31
+ }
32
+ catch {
33
+ /* ignore */
34
+ }
35
+ }
36
+ export function clearFirstRunMarker() {
37
+ try {
38
+ fs.rmSync(firstRunMarkerPath(), { force: true });
39
+ }
40
+ catch {
41
+ /* ignore */
42
+ }
43
+ }
44
+ /** Pure decision, kept separate for tests: prompt only on an interactive
45
+ * terminal — scripts, CI and piped runs never get the question. */
46
+ export function shouldOfferFirstRun(tty = { stdinTTY: process.stdin.isTTY, stdoutTTY: process.stdout.isTTY }, firstRun = isFirstRun()) {
47
+ return firstRun && !!tty.stdinTTY && !!tty.stdoutTTY;
48
+ }
49
+ function askYesNo(question) {
50
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
51
+ return new Promise((resolve) => {
52
+ rl.question(question, (ans) => {
53
+ rl.close();
54
+ resolve(!/^\s*(n|no)\s*$/i.test(ans));
55
+ });
56
+ });
57
+ }
58
+ /**
59
+ * Bare-`open-memex` first-run flow. `runInit` is injected so tests don't need
60
+ * the real init. Returns "skipped" when there's nothing to ask (already set
61
+ * up, or non-interactive) — the caller then prints usage as before.
62
+ */
63
+ export async function offerFirstRunInit(runInit) {
64
+ if (!shouldOfferFirstRun())
65
+ return "skipped";
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");
68
+ if (await askYesNo("Run it now? [Y/n] ")) {
69
+ await runInit();
70
+ return "initialized";
71
+ }
72
+ markFirstRunDone("declined");
73
+ console.log("No problem — run `open-memex init` any time.");
74
+ return "declined";
75
+ }
package/dist/init.js CHANGED
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises";
7
7
  import { pathToFileURL, fileURLToPath } from "node:url";
8
8
  import { DEFAULT_CONFIG, saveConfig } from "./config.js";
9
9
  import { projectRoot } from "./paths.js";
10
+ import { markFirstRunDone, clearFirstRunMarker } from "./first-run.js";
10
11
  const MARKER = "<!-- open-memex -->";
11
12
  /** npm dist-tag carrying the 0.3.x preview line. */
12
13
  const ALPHA_TAG = "open-memex@alpha";
@@ -40,54 +41,70 @@ const INSTRUCTIONS = `${MARKER}
40
41
  > Applies only when the \`open-memex\` MCP server is available in this session
41
42
  > (the \`memory_*\` tools exist). Otherwise ignore this section.
42
43
 
43
- You have a local memory MCP server (\`open-memex\`) with eleven tools:
44
- \`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`, \`memory_forget\`,
45
- \`memory_status\`, \`memory_submit\`, \`memory_propose\`, \`memory_promote\`, \`memory_resolve\`,
46
- \`memory_pr_status\`.
44
+ You have a local memory MCP server (\`open-memex\`). Its tools are
45
+ \`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`,
46
+ \`memory_forget\`, \`memory_status\`, \`memory_submit\`, \`memory_propose\`,
47
+ \`memory_promote\`, \`memory_resolve\`, \`memory_pr_status\` — always call them by
48
+ these full names.
47
49
 
48
- - BE PROACTIVE. When the user shares something worth remembering across sessions
49
- (a decision, a preference, a project convention, a fix and its cause), call
50
- \`memory_add\` without being asked. Keep each memory to one self-contained statement.
51
- - At checkpoints (session start, end of a work chunk, after the user commits, after
52
- any memory_* action), DISTILL the session: propose 1–3 short memories capturing the
53
- useful conclusion — what was learned or decided, how an issue was resolved, what to
54
- avoid, where the authoritative doc lives — not the raw transcript. Save NOTHING the
55
- user did not approve; on approval call \`memory_add\` with source "inference" at the
56
- confirmed scope. If the knowledge already lives in project docs, save a \`reference\`
57
- memory pointing at the doc instead of copying it. Long-form notes are fine ONLY when
58
- the user explicitly asks to save one.
59
- - Before asking the user about past decisions, conventions, or preferences they may
60
- have told you before, call \`memory_search\` first — try a few keyword variants
61
- (including the user's own language) when the first search comes up empty.
62
- - Memories default to this project's scope; use the \`personal\` scope for facts about
63
- the user that hold across all projects. When a saved fact becomes outdated, call
64
- \`memory_supersede\` instead of adding a duplicate.
50
+ - The server tells you when project outbox drafts are waiting for review — in
51
+ tool results. At session start, call \`memory_status\` once to check. When
52
+ drafts are waiting, summarize them (one line each) and ask the user which
53
+ ones to sync into the repo; sync NOTHING the user did not name. If you
54
+ already asked about these drafts this session, don't ask again. When the
55
+ server reports none waiting, do nothing.
56
+ - BE PROACTIVE about facts the user states directly: when the user shares a
57
+ decision, preference, project convention, or fix-and-cause worth remembering
58
+ across sessions, call \`memory_add\` without being asked. Keep each memory to
59
+ one self-contained statement, and add a brief "(noted in memory)" so the
60
+ user sees it worked.
61
+ - For conclusions YOU infer (the user never stated them): when you finish a
62
+ task the user would describe in one sentence, consider distilling the
63
+ session — if there is something worth keeping,
64
+ propose 1–3 short memories capturing the useful conclusion (what was learned
65
+ or decided, how an issue was resolved, what to avoid, where the authoritative
66
+ doc lives — not the raw transcript), each with its proposed scope. Save
67
+ NOTHING the user did not approve; on approval call \`memory_add\` with source
68
+ "inference" at the approved scope. If the knowledge already lives in project
69
+ docs, save it as type "reference" pointing at the doc instead of copying it.
70
+ Long-form notes are fine ONLY when the user explicitly asks to save one.
71
+ - Before asking the user about past decisions, conventions, or preferences
72
+ they may have told you before, call \`memory_search\` first — try a few
73
+ keyword variants (including the user's own language) when the first search
74
+ comes up empty.
75
+ - Memories default to this project's scope; use the \`personal\` scope for facts
76
+ about the user that hold across all projects. When a saved fact becomes
77
+ outdated, call \`memory_supersede\` (find the old memory's id with
78
+ \`memory_search\` first) instead of adding a duplicate.
65
79
 
66
80
  ## Syncing project memories for review (D26)
67
81
 
68
- Project memories you save land in a local outbox first — they are NOT in git yet.
69
- Syncing them into the repo for review is an explicit, user-approved step:
82
+ Project memories you save land in a local outbox first — they are NOT in git
83
+ yet. Syncing them into the repo for review is an explicit, user-approved step:
70
84
 
71
- - At session start, when you finish a meaningful chunk of work, after the user
72
- commits (git commit), and after any memory_* action completes, call
73
- \`memory_status\`. If the outbox has drafts, summarize them (one line each) and ask
74
- the user which ones to sync. Sync NOTHING the user did not name.
75
- - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
76
- the sync flow above: call \`memory_status\`, summarize the outbox drafts, and ask
77
- which ones to sync.
78
- - When the user approves, call \`memory_submit\` with the approved ids. It copies
79
- the drafts into the repo as \`proposed\`, commits locally on the CURRENT branch,
80
- and prints the push + PR commands. It NEVER creates a branch on its own.
85
+ - When the server reports drafts waiting for review, call \`memory_status\` to
86
+ see them. Summarize the drafts (one line each) and ask the user which ones
87
+ to sync. Sync NOTHING the user did not name.
88
+ - When the user says "sync memory" (or "同步记忆"), run the sync
89
+ flow above: call \`memory_status\`, summarize the outbox drafts, and ask which
90
+ ones to sync. ALWAYS use the \`memory_status\` tool for this — never browse
91
+ the memory data directory directly.
92
+ - When the user approves, call \`memory_submit\` with the approved ids. It
93
+ copies the drafts into the current project's \`.ai/open-memex/\` directory as
94
+ \`proposed\` and commits locally on the CURRENT branch. It NEVER creates a
95
+ branch on its own.
81
96
  - After the submit, ask ONE follow-up: "want me to create a branch + push +
82
- open the PR, or will you handle it yourself?" A "yes, you do it" answer covers
83
- the whole chain — branch creation, push, PR creation — do NOT re-ask at each
84
- step. If the user says they will do it themselves, hand them the printed
85
- push/PR commands and do nothing. NEVER create branches, push, or open PRs
86
- without their explicit approval.
87
- - Base branch for the memory PR defaults to the branch you are on; the user may
88
- redirect it to the integration branch (main) for branch-independent knowledge.
89
- - If anything conflicts (same id with different content, push rejected), STOP and
90
- let the user judge — never overwrite.
97
+ open the PR, or will you handle it yourself?" A "yes, you do it" answer
98
+ covers the whole chain — branch creation, push, PR creation — do NOT re-ask
99
+ at each step. If the user says they will do it themselves, hand them the
100
+ printed push/PR commands and do nothing. NEVER create branches, push, or open
101
+ PRs without their explicit approval.
102
+ - If the user wants the memories reviewed on a separate branch, create the
103
+ branch first (the commit comes along), then push and open the PR. The PR base
104
+ defaults to the branch submit ran on; \`--base\` overrides it (e.g. \`main\` for
105
+ branch-independent knowledge).
106
+ - If anything conflicts (same id with different content, push rejected), STOP
107
+ and let the user judge — never overwrite.
91
108
  - After the PR merges, call \`memory_pr_status\` (with \`apply\` when the user
92
109
  approves) to map the PR's review state back onto each memory — merged means
93
110
  \`published\`, an approval means \`approved\` (credited to the reviewer).
@@ -656,6 +673,11 @@ export async function initProject(opts) {
656
673
  writeInstructions(root, scope, clients.find((c) => c !== "opencode"));
657
674
  }
658
675
  console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
676
+ // D51: close the init→first-use gap — one concrete next step so a new user
677
+ // sees what "it works" looks like instead of stopping at "it's wired".
678
+ console.log(` Next step: \`open-memex add "standup is at 9:30"\` — then ask your agent what it remembers.`);
679
+ // D50: init completed — the bare-`open-memex` first-run offer won't ask again.
680
+ markFirstRunDone("initialized");
659
681
  }
660
682
  // ---------------------------------------------------------------------------
661
683
  // uninstall — the reverse of init (D48). Removes the editor wiring init wrote:
@@ -844,4 +866,8 @@ export async function uninstallProject(opts) {
844
866
  ? `\nDone. Removed open-memex wiring from ${changed} file(s).`
845
867
  : "\nDone. Nothing to remove — no open-memex wiring found.");
846
868
  console.log("Your memories are untouched (uninstall never deletes data).");
869
+ // D50: unwiring is the reverse of init — drop the first-run marker so the
870
+ // next bare `open-memex` offers to wire again.
871
+ if (changed > 0)
872
+ clearFirstRunMarker();
847
873
  }
package/dist/mcp.js CHANGED
@@ -28,7 +28,8 @@ import { loadConfig } from "./config.js";
28
28
  import { resolveProjectScope, PERSONAL_SCOPE } from "./scope.js";
29
29
  import { db } from "./store/db.js";
30
30
  import { syncScope } from "./store/sync.js";
31
- import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, statusMemories, submitMemoriesOp, proposeMemoriesOp, promoteMemoryOp, resolveMemoryOp, prStatusOp, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, memoryStatusArgs, memorySubmitArgs, memoryProposeArgs, memoryPromoteArgs, memoryResolveArgs, memoryPrStatusArgs, TOOL_DESCRIPTIONS, } from "./tools/ops.js";
31
+ import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, statusMemories, submitMemoriesOp, proposeMemoriesOp, promoteMemoryOp, resolveMemoryOp, prStatusOp, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, memoryStatusArgs, memorySubmitArgs, memoryProposeArgs, memoryPromoteArgs, memoryResolveArgs, memoryPrStatusArgs, TOOL_DESCRIPTIONS, withOutboxNote, } from "./tools/ops.js";
32
+ import { outboxDraftCount } from "./submit.js";
32
33
  // Server version tracks package.json — never hardcode it here again.
33
34
  // package.json sits two levels above this file in both layouts
34
35
  // (src/mcp.ts and dist/mcp.js), same convention as cli.ts --version.
@@ -49,40 +50,49 @@ const SERVER_VERSION = (() => {
49
50
  * client at connect time. Still advisory — no MCP consumer offers a hard
50
51
  * session-start hook — but it is the strongest signal available.
51
52
  */
52
- const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
53
- (eleven memory_* tools: add, search, list, supersede, forget, status, submit,
54
- propose, promote, resolve, pr_status).
53
+ const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server.
54
+ Its tools are memory_add, memory_search, memory_list, memory_supersede,
55
+ memory_forget, memory_status, memory_submit, memory_propose, memory_promote,
56
+ memory_resolve, memory_pr_status — always call them by these full names.
55
57
 
56
- - At the START of this session, when you finish a meaningful chunk of work,
57
- after the user commits (git commit), and after any memory_* action completes,
58
- call memory_status. If the project outbox has drafts waiting for review,
58
+ - The server tells you when project outbox drafts are waiting for review —
59
+ in tool results, and in these session-start instructions. When it does,
59
60
  summarize them (one line each) and ask the user which ones to sync into the
60
- repo. Sync NOTHING the user did not name.
61
- - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
62
- the sync flow: call memory_status, summarize the outbox drafts, and ask which
63
- ones to sync. ALWAYS use the memory_status tool for this — never browse the
64
- appdata directory directly.
61
+ repo; sync NOTHING the user did not name. If you already asked about these
62
+ drafts this session, don't ask again. When the server reports none waiting,
63
+ do nothing.
64
+ - When the user says "sync memory" (or "同步记忆"), call memory_status,
65
+ summarize the outbox drafts (one line each), and ask which ones to sync.
66
+ ALWAYS use the memory_status tool for this — never browse the memory data
67
+ directory directly.
65
68
  - After memory_submit, ask ONE follow-up: "want me to create a branch + push +
66
69
  open the PR, or will you handle it yourself?" NEVER create branches, push, or
67
70
  open PRs without the user's explicit approval. A "yes, you do it" covers the
68
71
  whole chain — do NOT re-ask at each step.
69
- - BE PROACTIVE: when the user shares something worth remembering across sessions
70
- (a decision, a preference, a project convention, a fix and its cause), call
71
- memory_add without being asked. Keep each memory to one self-contained statement.
72
- - At the same checkpoints (session start, end of a work chunk, after the user
73
- commits, after any memory_* action), DISTILL the session: propose 1–3 short
74
- memories capturing the useful conclusion — what was learned or decided, how an
75
- issue was resolved, what to avoid, where the authoritative doc lives — not the
76
- raw transcript. Save NOTHING the user did not approve; on approval call
77
- memory_add with source "inference" at the confirmed scope. If the knowledge
78
- already lives in project docs, save a \`reference\` memory pointing at the doc
79
- instead of copying it. Long-form notes are fine ONLY when the user explicitly
80
- asks to save one.
72
+ - BE PROACTIVE about facts the user states directly: when the user shares a
73
+ decision, preference, project convention, or fix-and-cause worth remembering
74
+ across sessions, call memory_add without being asked. Keep each memory to one
75
+ self-contained statement, and add a brief "(noted in memory)" so the user
76
+ sees it worked.
77
+ - For conclusions YOU infer (the user never stated them): when you finish a
78
+ task the user would describe in one sentence, consider distilling the
79
+ session — if there is something worth keeping,
80
+ propose 1–3 short memories capturing the useful conclusion (what was learned
81
+ or decided, how an issue was resolved, what to avoid, where the authoritative
82
+ doc lives — not the raw transcript), each with its proposed scope. Save
83
+ NOTHING the user did not approve; on approval call memory_add with source
84
+ "inference" at the approved scope. If the knowledge already lives in project
85
+ docs, save it as type "reference" pointing at the doc instead of copying it.
86
+ Long-form notes are fine ONLY when the user explicitly asks to save one.
81
87
  - Before asking the user about past decisions, conventions, or preferences they
82
- may have told you before, call memory_search first.
88
+ may have told you before, call memory_search first — try a few keyword
89
+ variants (including the user's own language) when the first search comes up
90
+ empty.
83
91
  - Memories default to this project's scope; use the personal scope for facts
84
- about the user that hold across all projects.
85
- - personal scope memories NEVER leave this machine.`;
92
+ about the user that hold across all projects. When a saved fact becomes
93
+ outdated, call memory_supersede (find the old memory's id with memory_search
94
+ first) instead of adding a duplicate.
95
+ `;
86
96
  /** Adapt a framework-agnostic op result to an MCP tool response. */
87
97
  function toMcp(p) {
88
98
  return p.then((r) => ({ content: [{ type: "text", text: r.output }] }), (e) => ({
@@ -128,11 +138,17 @@ export async function runMcpServer() {
128
138
  return fn(args);
129
139
  };
130
140
  };
131
- const server = new McpServer({ name: "open-memex", version: SERVER_VERSION }, { instructions: SERVER_INSTRUCTIONS });
141
+ // D53: session-start outbox state, pushed. The stdio server starts fresh per
142
+ // session, so construction-time state ≈ session-start state.
143
+ const n = outboxDraftCount(scope.key);
144
+ const instructions = n > 0
145
+ ? `${SERVER_INSTRUCTIONS}\n\nSession start: the project outbox has ${n} draft${n === 1 ? "" : "s"} waiting for review — call memory_status to see them.`
146
+ : SERVER_INSTRUCTIONS;
147
+ const server = new McpServer({ name: "open-memex", version: SERVER_VERSION }, { instructions });
132
148
  server.registerTool("memory_add", {
133
149
  description: TOOL_DESCRIPTIONS.memory_add,
134
150
  inputSchema: z.object(memoryAddArgs),
135
- }, withSync((args) => toMcp(addMemory(getScope, cfg, args))));
151
+ }, withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))));
136
152
  server.registerTool("memory_search", {
137
153
  description: TOOL_DESCRIPTIONS.memory_search,
138
154
  inputSchema: z.object(memorySearchArgs),
@@ -146,12 +162,12 @@ export async function runMcpServer() {
146
162
  server.registerTool("memory_supersede", {
147
163
  description: TOOL_DESCRIPTIONS.memory_supersede,
148
164
  inputSchema: z.object(memorySupersedeArgs),
149
- }, withSync((args) => toMcp(supersedeMemory(cfg, args))));
165
+ }, withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))));
150
166
  server.registerTool("memory_forget", {
151
167
  description: TOOL_DESCRIPTIONS.memory_forget,
152
168
  inputSchema: z.object(memoryForgetArgs),
153
169
  annotations: { destructiveHint: true },
154
- }, withSync((args) => toMcp(forgetMemory(args))));
170
+ }, withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))));
155
171
  server.registerTool("memory_status", {
156
172
  description: TOOL_DESCRIPTIONS.memory_status,
157
173
  inputSchema: z.object(memoryStatusArgs),
@@ -160,11 +176,11 @@ export async function runMcpServer() {
160
176
  server.registerTool("memory_submit", {
161
177
  description: TOOL_DESCRIPTIONS.memory_submit,
162
178
  inputSchema: z.object(memorySubmitArgs),
163
- }, withSync((args) => toMcp(submitMemoriesOp(args))));
179
+ }, withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))));
164
180
  server.registerTool("memory_propose", {
165
181
  description: TOOL_DESCRIPTIONS.memory_propose,
166
182
  inputSchema: z.object(memoryProposeArgs),
167
- }, withSync((args) => toMcp(proposeMemoriesOp(args))));
183
+ }, withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))));
168
184
  server.registerTool("memory_promote", {
169
185
  description: TOOL_DESCRIPTIONS.memory_promote,
170
186
  inputSchema: z.object(memoryPromoteArgs),
package/dist/paths.js CHANGED
@@ -13,6 +13,14 @@ function dataRoot() {
13
13
  return path.join(xdg, "open-memex");
14
14
  }
15
15
  let _cached = null;
16
+ /**
17
+ * Read-only data-root path — never creates the directory. For existence
18
+ * checks (D50 first-run detection) that must not pollute storage; contrast
19
+ * `paths()`, which mkdirs as a side effect.
20
+ */
21
+ export function dataRootPath() {
22
+ return dataRoot();
23
+ }
16
24
  export function paths() {
17
25
  if (_cached)
18
26
  return _cached;
package/dist/submit.js CHANGED
@@ -114,6 +114,19 @@ export function getSyncStatus() {
114
114
  uncommitted: uncommittedList,
115
115
  };
116
116
  }
117
+ /**
118
+ * D53: cheap outbox draft count — one indexed query, no git I/O — for the
119
+ * push-not-poll note appended to mutating tool results and to the MCP
120
+ * session-start instructions. Same outbox definition as getSyncStatus
121
+ * (file under the scope's outbox dir), without the expensive parts.
122
+ */
123
+ export function outboxDraftCount(scopeKey) {
124
+ const outboxDir = memoriesDirPath(scopeKey);
125
+ const row = db()
126
+ .prepare(`SELECT COUNT(*) AS n FROM memories WHERE scope_key = ? AND instr(file_path, ?) = 1`)
127
+ .get(scopeKey, outboxDir + path.sep);
128
+ return row?.n ?? 0;
129
+ }
117
130
  export function formatSyncStatus(st) {
118
131
  const lines = [];
119
132
  lines.push(`project: ${st.projectName} (${st.scopeKey})`);
@@ -1,11 +1,11 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
- import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, TOOL_DESCRIPTIONS, } from "./ops.js";
2
+ import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, TOOL_DESCRIPTIONS, withOutboxNote, } from "./ops.js";
3
3
  export function makeTools(getScope, cfg) {
4
4
  const memory_add = tool({
5
5
  description: TOOL_DESCRIPTIONS.memory_add,
6
6
  args: memoryAddArgs,
7
7
  async execute(args) {
8
- return addMemory(getScope, cfg, args);
8
+ return withOutboxNote(getScope().key, addMemory(getScope, cfg, args));
9
9
  },
10
10
  });
11
11
  const memory_search = tool({
@@ -26,14 +26,14 @@ export function makeTools(getScope, cfg) {
26
26
  description: TOOL_DESCRIPTIONS.memory_supersede,
27
27
  args: memorySupersedeArgs,
28
28
  async execute(args) {
29
- return supersedeMemory(cfg, args);
29
+ return withOutboxNote(getScope().key, supersedeMemory(cfg, args));
30
30
  },
31
31
  });
32
32
  const memory_forget = tool({
33
33
  description: TOOL_DESCRIPTIONS.memory_forget,
34
34
  args: memoryForgetArgs,
35
35
  async execute(args) {
36
- return forgetMemory(args);
36
+ return withOutboxNote(getScope().key, forgetMemory(args));
37
37
  },
38
38
  });
39
39
  return { memory_add, memory_search, memory_list, memory_forget, memory_supersede };