open-memex 0.5.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-alpha.4",
4
4
  "description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -18,7 +18,8 @@
18
18
  "cli": "node --experimental-strip-types src/cli.ts",
19
19
  "mcp": "node --experimental-strip-types src/mcp.ts",
20
20
  "build": "tsc -p tsconfig.build.json",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "npm run build",
22
+ "postinstall": "node scripts/postinstall.js"
22
23
  },
23
24
  "dependencies": {
24
25
  "@modelcontextprotocol/sdk": "^1.30.1",
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // D50: `npm install -g open-memex` only puts the CLI on PATH — the editor
3
+ // wiring is `init`'s job. postinstall must never prompt (it runs in CI,
4
+ // Docker builds and `npm ci` where stdin isn't a terminal), so this only
5
+ // prints the pointer. Plain JS, no dependencies.
6
+ console.log(
7
+ "\nopen-memex installed. Run `open-memex init` to wire it into your editors — " +
8
+ "it auto-detects VS Code, Cursor and opencode.\n",
9
+ );
package/src/cli.ts CHANGED
@@ -329,8 +329,9 @@ Example:
329
329
  open-memex capture --dry-run "remember: we deploy on Fridays"`,
330
330
 
331
331
  doctor: `Environment health check: Node version, config source, scope resolution,
332
- storage writability, then boots a real MCP server and runs initialize +
333
- tools/list against it — all eleven tools must show up.
332
+ storage writability, VS Code MCP enablement (settings.json + system policy),
333
+ then boots a real MCP server and runs initialize + tools/list against it —
334
+ all eleven tools must show up.
334
335
 
335
336
  Usage: open-memex doctor
336
337
 
@@ -377,7 +378,7 @@ Usage:
377
378
  Every command has its own help with description and examples:
378
379
  open-memex <command> --help (or -h)
379
380
 
380
- One-command project setup: \`open-memex init\` (or \`npx open-memex@alpha init\`) detects
381
+ One-command project setup: \`open-memex init\` (or \`npx -y open-memex init\`) detects
381
382
  your installed editors and wires them all — user-level where the editor supports it
382
383
  (VS Code / Cursor MCP config, opencode native plugin), so one init covers every project;
383
384
  Visual Studio is included when the project has a solution file (its \`.mcp.json\`
@@ -392,9 +393,9 @@ confirms the detected editors and asks a couple of settings (keyword capture,
392
393
  first-turn injection); \`--yes\` accepts all defaults, and non-terminal runs never prompt.
393
394
  \`open-memex config set <key> <value>\` changes those settings after install.
394
395
 
395
- Once installed globally (\`npm i -g open-memex@alpha\`) the \`open-memex\` command is
396
- available directly: \`open-memex mcp\` starts the stdio MCP server (same five
397
- memory_* tools as the opencode plugin); \`open-memex mcp --print-config <client>\`
396
+ Once installed globally (\`npm i -g open-memex\`) the \`open-memex\` command is
397
+ available directly: \`open-memex mcp\` starts the stdio MCP server (11 tools, a superset of
398
+ the opencode plugin's five memory_* tools); \`open-memex mcp --print-config <client>\`
398
399
  prints a copy-paste MCP client config snippet.
399
400
 
400
401
  Scope defaults to \`project\` (derived from cwd's git remote or path).
@@ -471,7 +472,7 @@ function resolveCliScope(flags: Record<string, string>, project: Scope): Scope {
471
472
  }
472
473
 
473
474
  /** Print a copy-paste MCP client config snippet. Requires a global install
474
- * (`npm i -g open-memex@alpha`) so the `open-memex` command is on PATH. */
475
+ * (`npm i -g open-memex`) so the `open-memex` command is on PATH. */
475
476
  function printMcpConfig(client: string): never {
476
477
  const c = client.toLowerCase();
477
478
  // D17: resolve the server command the same way `init` does.
@@ -547,7 +548,7 @@ function printMcpConfig(client: string): never {
547
548
  }
548
549
  if (!mc.durable) {
549
550
  console.error(
550
- `\n# note: no durable \`open-memex\` on PATH — snippet uses npx. \`npm i -g open-memex@alpha\` for faster startup.`,
551
+ `\n# note: no durable \`open-memex\` on PATH — snippet uses npx. \`npm i -g open-memex\` for faster startup.`,
551
552
  );
552
553
  }
553
554
  process.exit(0);
@@ -555,7 +556,28 @@ function printMcpConfig(client: string): never {
555
556
 
556
557
  async function main() {
557
558
  const [cmd, ...rest] = process.argv.slice(2);
558
- if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") usage(0);
559
+ if (!cmd) {
560
+ // D50: fresh machine + interactive terminal → offer init instead of bare usage.
561
+ // Non-interactive (CI/scripts/pipes) prints usage exactly as before.
562
+ const { offerFirstRunInit } = await import("./first-run.ts");
563
+ const outcome = await offerFirstRunInit(async () => {
564
+ // Re-exec `open-memex init` as a child with inherited stdio instead of
565
+ // calling initProject() in-process: the offer's readline already
566
+ // consumed stdin's buffer, and a second readline on the same stream
567
+ // would see EOF on burst input instead of the user's next answers.
568
+ const { spawnSync } = await import("node:child_process");
569
+ const r = spawnSync(
570
+ process.execPath,
571
+ [...process.execArgv, fileURLToPath(import.meta.url), "init"],
572
+ { stdio: "inherit" },
573
+ );
574
+ if (r.error) throw r.error;
575
+ if ((r.status ?? 1) !== 0) process.exit(r.status ?? 1);
576
+ });
577
+ if (outcome !== "initialized") usage(0);
578
+ return;
579
+ }
580
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") usage(0);
559
581
 
560
582
  if (cmd === "--version" || cmd === "-v") {
561
583
  // package.json sits two levels above this file in both layouts
@@ -724,7 +746,7 @@ async function main() {
724
746
  return;
725
747
  }
726
748
 
727
- // `mcp` starts the stdio MCP server (same tools as the opencode plugin).
749
+ // `mcp` starts the stdio MCP server (11 tools; the opencode plugin exposes 5).
728
750
  // Branched before db() — runMcpServer() does its own init, and stdout must
729
751
  // stay clean for the MCP protocol.
730
752
  if (cmd === "mcp") {
@@ -68,12 +68,13 @@ export function distillAgentsMarkdown(opts: DistillAgentsOptions): string {
68
68
  }
69
69
  // D43 — §3.5 memory-hygiene footer (double insurance for opencode users,
70
70
  // who never see the MCP handshake / init instructions): teach the agent
71
- // reading this AGENTS.md to propose distilled captures at checkpoints.
71
+ // reading this AGENTS.md to propose distilled captures when a task ends
72
+ // (D53: the checkpoint mechanism is gone).
72
73
  lines.push(`### Memory hygiene (open-memex)`);
73
74
  lines.push(``);
74
75
  lines.push(
75
- `- At checkpoints (session start, end of a work chunk, after the user commits),`,
76
- ` distill the session: propose 1–3 short memories capturing the useful`,
76
+ `- When you finish a task the user would describe in one sentence, distill`,
77
+ ` the session: propose 1–3 short memories capturing the useful`,
77
78
  ` conclusion — what was learned or decided, how an issue was resolved, what`,
78
79
  ` to avoid, where the authoritative doc lives — not the raw transcript.`,
79
80
  ` Save nothing without user approval.`,
package/src/doctor.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  // Read-only except that paths() and the MCP handshake may create the (empty)
4
4
  // data directories, exactly like a normal `open-memex mcp` start would.
5
5
  import fs from "node:fs";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
- import { spawn } from "node:child_process";
8
+ import { spawn, execFileSync } from "node:child_process";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { loadConfig, configSource, DEFAULT_CONFIG } from "./config.ts";
10
11
  import { paths } from "./paths.ts";
@@ -158,9 +159,145 @@ function mcpCheck(): Promise<Check> {
158
159
  });
159
160
  }
160
161
 
162
+ /** Parse JSON tolerating line/block comments plus trailing commas
163
+ * (VS Code's settings.json is JSONC). Health-check grade, not a full parser. */
164
+ function parseLenientJson(raw: string): Record<string, unknown> {
165
+ try {
166
+ return JSON.parse(raw) as Record<string, unknown>;
167
+ } catch {
168
+ /* fall through to comment stripping */
169
+ }
170
+ let out = "";
171
+ let i = 0;
172
+ let inStr = false;
173
+ let esc = false;
174
+ while (i < raw.length) {
175
+ const c = raw[i];
176
+ const n = raw[i + 1];
177
+ if (inStr) {
178
+ out += c;
179
+ if (esc) esc = false;
180
+ else if (c === "\\") esc = true;
181
+ else if (c === '"') inStr = false;
182
+ i++;
183
+ continue;
184
+ }
185
+ if (c === '"') {
186
+ inStr = true;
187
+ out += c;
188
+ i++;
189
+ continue;
190
+ }
191
+ if (c === "/" && n === "/") {
192
+ while (i < raw.length && raw[i] !== "\n") i++;
193
+ continue;
194
+ }
195
+ if (c === "/" && n === "*") {
196
+ i += 2;
197
+ while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
198
+ i += 2;
199
+ continue;
200
+ }
201
+ out += c;
202
+ i++;
203
+ }
204
+ out = out.replace(/,\s*([}\]])/g, "$1");
205
+ return JSON.parse(out) as Record<string, unknown>;
206
+ }
207
+
208
+ function vscodeSettingsPath(): string | null {
209
+ const home = os.homedir();
210
+ if (process.platform === "win32") {
211
+ const appdata = process.env.APPDATA;
212
+ return appdata ? path.join(appdata, "Code", "User", "settings.json") : null;
213
+ }
214
+ if (process.platform === "darwin") {
215
+ return path.join(home, "Library", "Application Support", "Code", "User", "settings.json");
216
+ }
217
+ return path.join(home, ".config", "Code", "User", "settings.json");
218
+ }
219
+
220
+ /** True when a Windows system policy disables MCP (value name contains "mcp",
221
+ * data is 0/0x0/false). Checks HKLM and HKCU policy keys. */
222
+ function windowsMcpPolicyDisabled(): string | null {
223
+ if (process.platform !== "win32") return null;
224
+ for (const hive of ["HKLM", "HKCU"]) {
225
+ try {
226
+ const stdout = execFileSync(
227
+ "reg",
228
+ ["query", `${hive}\\SOFTWARE\\Policies\\Microsoft\\VSCode`],
229
+ { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] },
230
+ ) as string;
231
+ for (const line of stdout.split("\n")) {
232
+ const m = line.match(/^\s{2,}(\S+)\s+REG_\w+\s+(\S+)/);
233
+ if (m && /mcp/i.test(m[1]) && /^(0x0|0|false)$/i.test(m[2])) {
234
+ return `${hive}\\SOFTWARE\\Policies\\Microsoft\\VSCode!${m[1]}`;
235
+ }
236
+ }
237
+ } catch {
238
+ /* policy key absent — no policy */
239
+ }
240
+ }
241
+ return null;
242
+ }
243
+
244
+ function settingsMcpDisabled(file: string): boolean {
245
+ try {
246
+ const parsed = parseLenientJson(fs.readFileSync(file, "utf8"));
247
+ return parsed["chat.mcp.enabled"] === false;
248
+ } catch {
249
+ return false; // unreadable file: don't claim anything
250
+ }
251
+ }
252
+
253
+ /** VS Code ignores MCP server entries entirely when MCP is switched off —
254
+ * either in settings.json or, on managed machines, by system policy. */
255
+ function vscodeMcpCheck(): Check {
256
+ const name = "vscode-mcp";
257
+ const policyHit = windowsMcpPolicyDisabled();
258
+ if (policyHit) {
259
+ return {
260
+ name,
261
+ ok: false,
262
+ detail:
263
+ `MCP is disabled by system policy (${policyHit}) — managed by your organization. ` +
264
+ `VS Code will ignore open-memex's MCP entry on this machine.`,
265
+ };
266
+ }
267
+ const files: string[] = [];
268
+ const user = vscodeSettingsPath();
269
+ if (user) files.push(user);
270
+ const ws = path.join(process.cwd(), ".vscode", "settings.json");
271
+ if (!files.includes(ws)) files.push(ws);
272
+ const found: string[] = [];
273
+ const disabled: string[] = [];
274
+ for (const f of files) {
275
+ if (!fs.existsSync(f)) continue;
276
+ found.push(f);
277
+ if (settingsMcpDisabled(f)) disabled.push(f);
278
+ }
279
+ if (disabled.length > 0) {
280
+ return {
281
+ name,
282
+ ok: false,
283
+ detail:
284
+ `chat.mcp.enabled is false in ${disabled.join(", ")} — VS Code will not load ` +
285
+ `MCP servers. Set it to true (or ask IT if the setting shows as managed).`,
286
+ };
287
+ }
288
+ return {
289
+ name,
290
+ ok: true,
291
+ detail:
292
+ found.length > 0
293
+ ? `MCP enabled (checked ${found.join(", ")})`
294
+ : "VS Code settings not found on this machine — nothing to check",
295
+ };
296
+ }
297
+
161
298
  export async function runDoctor(): Promise<boolean> {
162
299
  console.log("open-memex doctor");
163
- const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck()];
300
+ const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), vscodeMcpCheck()];
164
301
  checks.push(await mcpCheck());
165
302
  let allOk = true;
166
303
  for (const c of checks) {
@@ -0,0 +1,96 @@
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.ts";
17
+
18
+ const MARKER = ".init.json";
19
+
20
+ export type FirstRunState = "initialized" | "declined";
21
+
22
+ export function firstRunMarkerPath(root: string = dataRootPath()): string {
23
+ return path.join(root, MARKER);
24
+ }
25
+
26
+ /** True when init has neither run nor been declined on this machine. */
27
+ export function isFirstRun(root?: string): boolean {
28
+ return !fs.existsSync(firstRunMarkerPath(root ?? dataRootPath()));
29
+ }
30
+
31
+ /** Record the outcome — best effort; a missing marker just asks again next time. */
32
+ export function markFirstRunDone(state: FirstRunState): void {
33
+ const file = firstRunMarkerPath();
34
+ try {
35
+ fs.mkdirSync(path.dirname(file), { recursive: true });
36
+ fs.writeFileSync(file, JSON.stringify({ v: 1, state, at: new Date().toISOString() }) + "\n");
37
+ } catch {
38
+ /* ignore */
39
+ }
40
+ }
41
+
42
+ export function clearFirstRunMarker(): void {
43
+ try {
44
+ fs.rmSync(firstRunMarkerPath(), { force: true });
45
+ } catch {
46
+ /* ignore */
47
+ }
48
+ }
49
+
50
+ export interface TtyProbe {
51
+ stdinTTY: boolean | undefined;
52
+ stdoutTTY: boolean | undefined;
53
+ }
54
+
55
+ /** Pure decision, kept separate for tests: prompt only on an interactive
56
+ * terminal — scripts, CI and piped runs never get the question. */
57
+ export function shouldOfferFirstRun(
58
+ tty: TtyProbe = { stdinTTY: process.stdin.isTTY, stdoutTTY: process.stdout.isTTY },
59
+ firstRun: boolean = isFirstRun(),
60
+ ): boolean {
61
+ return firstRun && !!tty.stdinTTY && !!tty.stdoutTTY;
62
+ }
63
+
64
+ function askYesNo(question: string): Promise<boolean> {
65
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
+ return new Promise((resolve) => {
67
+ rl.question(question, (ans) => {
68
+ rl.close();
69
+ resolve(!/^\s*(n|no)\s*$/i.test(ans));
70
+ });
71
+ });
72
+ }
73
+
74
+ export type FirstRunOutcome = "initialized" | "declined" | "skipped";
75
+
76
+ /**
77
+ * Bare-`open-memex` first-run flow. `runInit` is injected so tests don't need
78
+ * the real init. Returns "skipped" when there's nothing to ask (already set
79
+ * up, or non-interactive) — the caller then prints usage as before.
80
+ */
81
+ export async function offerFirstRunInit(
82
+ runInit: () => Promise<void>,
83
+ ): Promise<FirstRunOutcome> {
84
+ if (!shouldOfferFirstRun()) return "skipped";
85
+ console.log(
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",
88
+ );
89
+ if (await askYesNo("Run it now? [Y/n] ")) {
90
+ await runInit();
91
+ return "initialized";
92
+ }
93
+ markFirstRunDone("declined");
94
+ console.log("No problem — run `open-memex init` any time.");
95
+ return "declined";
96
+ }
package/src/init.ts 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.ts";
9
9
  import { projectRoot } from "./paths.ts";
10
+ import { markFirstRunDone, clearFirstRunMarker } from "./first-run.ts";
10
11
 
11
12
  const MARKER = "<!-- open-memex -->";
12
13
 
@@ -51,54 +52,70 @@ const INSTRUCTIONS = `${MARKER}
51
52
  > Applies only when the \`open-memex\` MCP server is available in this session
52
53
  > (the \`memory_*\` tools exist). Otherwise ignore this section.
53
54
 
54
- You have a local memory MCP server (\`open-memex\`) with eleven tools:
55
- \`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`, \`memory_forget\`,
56
- \`memory_status\`, \`memory_submit\`, \`memory_propose\`, \`memory_promote\`, \`memory_resolve\`,
57
- \`memory_pr_status\`.
58
-
59
- - BE PROACTIVE. When the user shares something worth remembering across sessions
60
- (a decision, a preference, a project convention, a fix and its cause), call
61
- \`memory_add\` without being asked. Keep each memory to one self-contained statement.
62
- - At checkpoints (session start, end of a work chunk, after the user commits, after
63
- any memory_* action), DISTILL the session: propose 1–3 short memories capturing the
64
- useful conclusion — what was learned or decided, how an issue was resolved, what to
65
- avoid, where the authoritative doc lives — not the raw transcript. Save NOTHING the
66
- user did not approve; on approval call \`memory_add\` with source "inference" at the
67
- confirmed scope. If the knowledge already lives in project docs, save a \`reference\`
68
- memory pointing at the doc instead of copying it. Long-form notes are fine ONLY when
69
- the user explicitly asks to save one.
70
- - Before asking the user about past decisions, conventions, or preferences they may
71
- have told you before, call \`memory_search\` first — try a few keyword variants
72
- (including the user's own language) when the first search comes up empty.
73
- - Memories default to this project's scope; use the \`personal\` scope for facts about
74
- the user that hold across all projects. When a saved fact becomes outdated, call
75
- \`memory_supersede\` instead of adding a duplicate.
55
+ You have a local memory MCP server (\`open-memex\`). Its tools are
56
+ \`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`,
57
+ \`memory_forget\`, \`memory_status\`, \`memory_submit\`, \`memory_propose\`,
58
+ \`memory_promote\`, \`memory_resolve\`, \`memory_pr_status\` — always call them by
59
+ these full names.
60
+
61
+ - The server tells you when project outbox drafts are waiting for review — in
62
+ tool results. At session start, call \`memory_status\` once to check. When
63
+ drafts are waiting, summarize them (one line each) and ask the user which
64
+ ones to sync into the repo; sync NOTHING the user did not name. If you
65
+ already asked about these drafts this session, don't ask again. When the
66
+ server reports none waiting, do nothing.
67
+ - BE PROACTIVE about facts the user states directly: when the user shares a
68
+ decision, preference, project convention, or fix-and-cause worth remembering
69
+ across sessions, call \`memory_add\` without being asked. Keep each memory to
70
+ one self-contained statement, and add a brief "(noted in memory)" so the
71
+ user sees it worked.
72
+ - For conclusions YOU infer (the user never stated them): when you finish a
73
+ task the user would describe in one sentence, consider distilling the
74
+ session — if there is something worth keeping,
75
+ propose 1–3 short memories capturing the useful conclusion (what was learned
76
+ or decided, how an issue was resolved, what to avoid, where the authoritative
77
+ doc lives — not the raw transcript), each with its proposed scope. Save
78
+ NOTHING the user did not approve; on approval call \`memory_add\` with source
79
+ "inference" at the approved scope. If the knowledge already lives in project
80
+ docs, save it as type "reference" pointing at the doc instead of copying it.
81
+ Long-form notes are fine ONLY when the user explicitly asks to save one.
82
+ - Before asking the user about past decisions, conventions, or preferences
83
+ they may have told you before, call \`memory_search\` first — try a few
84
+ keyword variants (including the user's own language) when the first search
85
+ comes up empty.
86
+ - Memories default to this project's scope; use the \`personal\` scope for facts
87
+ about the user that hold across all projects. When a saved fact becomes
88
+ outdated, call \`memory_supersede\` (find the old memory's id with
89
+ \`memory_search\` first) instead of adding a duplicate.
76
90
 
77
91
  ## Syncing project memories for review (D26)
78
92
 
79
- Project memories you save land in a local outbox first — they are NOT in git yet.
80
- Syncing them into the repo for review is an explicit, user-approved step:
81
-
82
- - At session start, when you finish a meaningful chunk of work, after the user
83
- commits (git commit), and after any memory_* action completes, call
84
- \`memory_status\`. If the outbox has drafts, summarize them (one line each) and ask
85
- the user which ones to sync. Sync NOTHING the user did not name.
86
- - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
87
- the sync flow above: call \`memory_status\`, summarize the outbox drafts, and ask
88
- which ones to sync.
89
- - When the user approves, call \`memory_submit\` with the approved ids. It copies
90
- the drafts into the repo as \`proposed\`, commits locally on the CURRENT branch,
91
- and prints the push + PR commands. It NEVER creates a branch on its own.
93
+ Project memories you save land in a local outbox first — they are NOT in git
94
+ yet. Syncing them into the repo for review is an explicit, user-approved step:
95
+
96
+ - When the server reports drafts waiting for review, call \`memory_status\` to
97
+ see them. Summarize the drafts (one line each) and ask the user which ones
98
+ to sync. Sync NOTHING the user did not name.
99
+ - When the user says "sync memory" (or "同步记忆"), run the sync
100
+ flow above: call \`memory_status\`, summarize the outbox drafts, and ask which
101
+ ones to sync. ALWAYS use the \`memory_status\` tool for this — never browse
102
+ the memory data directory directly.
103
+ - When the user approves, call \`memory_submit\` with the approved ids. It
104
+ copies the drafts into the current project's \`.ai/open-memex/\` directory as
105
+ \`proposed\` and commits locally on the CURRENT branch. It NEVER creates a
106
+ branch on its own.
92
107
  - After the submit, ask ONE follow-up: "want me to create a branch + push +
93
- open the PR, or will you handle it yourself?" A "yes, you do it" answer covers
94
- the whole chain — branch creation, push, PR creation — do NOT re-ask at each
95
- step. If the user says they will do it themselves, hand them the printed
96
- push/PR commands and do nothing. NEVER create branches, push, or open PRs
97
- without their explicit approval.
98
- - Base branch for the memory PR defaults to the branch you are on; the user may
99
- redirect it to the integration branch (main) for branch-independent knowledge.
100
- - If anything conflicts (same id with different content, push rejected), STOP and
101
- let the user judge — never overwrite.
108
+ open the PR, or will you handle it yourself?" A "yes, you do it" answer
109
+ covers the whole chain — branch creation, push, PR creation — do NOT re-ask
110
+ at each step. If the user says they will do it themselves, hand them the
111
+ printed push/PR commands and do nothing. NEVER create branches, push, or open
112
+ PRs without their explicit approval.
113
+ - If the user wants the memories reviewed on a separate branch, create the
114
+ branch first (the commit comes along), then push and open the PR. The PR base
115
+ defaults to the branch submit ran on; \`--base\` overrides it (e.g. \`main\` for
116
+ branch-independent knowledge).
117
+ - If anything conflicts (same id with different content, push rejected), STOP
118
+ and let the user judge — never overwrite.
102
119
  - After the PR merges, call \`memory_pr_status\` (with \`apply\` when the user
103
120
  approves) to map the PR's review state back onto each memory — merged means
104
121
  \`published\`, an approval means \`approved\` (credited to the reviewer).
@@ -720,6 +737,11 @@ export async function initProject(opts: {
720
737
  writeInstructions(root, scope, clients.find((c) => c !== "opencode")!);
721
738
  }
722
739
  console.log(`\nDone. Reload your editor window to start the open-memex MCP server.`);
740
+ // D51: close the init→first-use gap — one concrete next step so a new user
741
+ // sees what "it works" looks like instead of stopping at "it's wired".
742
+ console.log(` Next step: \`open-memex add "standup is at 9:30"\` — then ask your agent what it remembers.`);
743
+ // D50: init completed — the bare-`open-memex` first-run offer won't ask again.
744
+ markFirstRunDone("initialized");
723
745
  }
724
746
 
725
747
  // ---------------------------------------------------------------------------
@@ -909,4 +931,7 @@ export async function uninstallProject(opts: {
909
931
  : "\nDone. Nothing to remove — no open-memex wiring found.",
910
932
  );
911
933
  console.log("Your memories are untouched (uninstall never deletes data).");
934
+ // D50: unwiring is the reverse of init — drop the first-run marker so the
935
+ // next bare `open-memex` offers to wire again.
936
+ if (changed > 0) clearFirstRunMarker();
912
937
  }