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/dist/tools/ops.js CHANGED
@@ -15,9 +15,24 @@ import { upsertFromFile, deleteFromIndex } from "../store/sync.js";
15
15
  import { findDuplicates, supersede } from "../store/lifecycle.js";
16
16
  import { db } from "../store/db.js";
17
17
  import { redact } from "../redact.js";
18
- import { getSyncStatus, formatSyncStatus, submitMemories } from "../submit.js";
18
+ import { getSyncStatus, formatSyncStatus, submitMemories, outboxDraftCount } from "../submit.js";
19
19
  import { getPrStatus, formatPrStatus, applyPrStatus } from "../github.js";
20
20
  import { proposeMemories, promoteMemory, listConflicts, resolveConflict, formatReviewHistory, } from "../review.js";
21
+ /**
22
+ * D53: push, don't poll. Append the project-outbox pending count to mutating
23
+ * tool results so agents learn about drafts waiting for review without a
24
+ * checkpoint poll. Silent when the outbox is empty. `reviewHint` names the
25
+ * tool to call (MCP); transports without that tool leave it generic.
26
+ */
27
+ export async function withOutboxNote(scopeKey, p, reviewHint) {
28
+ const r = await p;
29
+ const n = outboxDraftCount(scopeKey);
30
+ if (n > 0) {
31
+ const tail = reviewHint ? ` — ${reviewHint}` : " for review";
32
+ r.output += `\n[open-memex: ${n} draft${n === 1 ? "" : "s"} waiting in the project outbox${tail}]`;
33
+ }
34
+ return r;
35
+ }
21
36
  /** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
22
37
  export const TOOL_DESCRIPTIONS = {
23
38
  memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Do not wait to be asked. Keep each memory to one self-contained statement. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
package/docs/V2-DESIGN.md CHANGED
@@ -546,6 +546,15 @@ requirement: personal data never touches third-party services). Benchmarks to tr
546
546
  - **0.5.1 (stable).** `--help` accuracy: `mcp` help states the server exposes 11
547
547
  tools (a superset of the opencode plugin's five memory tools); install hints point
548
548
  at the stable line instead of `@alpha` (F27).
549
+ - **0.6.0 (in development).** Close the install→init gap (D50): postinstall
550
+ prints the `open-memex init` pointer (never prompts — CI-safe); bare
551
+ `open-memex` on a fresh machine offers to run init on a TTY. Close the
552
+ init→first-use gap (D51): init ends with a one-line next-step hint
553
+ (`open-memex add` + ask the agent to recall it). Agent-prompt clarity pass
554
+ (D52): rewrite both agent-facing prompts (MCP handshake + init template) so
555
+ agents execute them correctly. Push-not-poll outbox (D53): the checkpoint
556
+ mechanism is retired — the server reports the outbox draft count at session
557
+ start and appends it to mutating tool results when non-zero.
549
558
  - **Phase 4 — Future, signal-gated.** Cloud `RemoteProvider` customization only on: multi-private-repo
550
559
  sharing needs, fine-grained ACL, audit/compliance mandates · optional API-backed exporters/providers
551
560
  for enterprise knowledge systems.
@@ -975,6 +984,78 @@ requirement: personal data never touches third-party services). Benchmarks to tr
975
984
  *Rationale: an empty file is the safest write target, not a corrupt file;
976
985
  refusing it sent the user down a manual path for no reason. Triggered by
977
986
  Stone's report 2026-09-29.*
987
+ - **D50** — close the install→init gap (0.6.0-alpha.1). `npm install -g`
988
+ only puts the CLI on PATH; the editor wiring is `init`'s job, and a clean
989
+ reinstall wipes it — Stone hit exactly this on 2026-09-30 (fresh opencode
990
+ reinstall + `npm i -g open-memex`, then no open-memex in `opencode.jsonc`).
991
+ Two changes, both CI-safe: (1) a `postinstall` script that **prints**
992
+ `Run \`open-memex init\`…` — postinstall must never prompt, it runs in CI /
993
+ Docker / `npm ci` where stdin isn't a terminal; (2) bare `open-memex` on a
994
+ machine where init never completed **offers** to run it (default yes) when
995
+ stdin+stdout are TTYs, otherwise prints usage exactly as before. Asked-state
996
+ is a `.init.json` marker at the data root — init writes it on success, a
997
+ declined offer writes it too, so the question is asked once; `uninstall`
998
+ removes it (unwiring is the reverse of init, so the next bare run offers to
999
+ wire again). The marker is a dotfile and export builds from DB rows, so it
1000
+ can't leak into bundles. The offer re-execs `open-memex init` as a child
1001
+ with inherited stdio rather than calling init in-process — the offer's own
1002
+ readline already consumed stdin's buffer, and a second readline on the same
1003
+ stream would see EOF on burst input (verified with a pty test).
1004
+ *Rationale: install ≠ setup, and the gap only shows up on a fresh machine —
1005
+ exactly when the user has the least context. A printed hint covers the
1006
+ install moment; the interactive offer covers the first-run moment; neither
1007
+ can hang a pipeline. Approved 2026-09-30.*
1008
+ - **D51** — close the init→first-use gap (0.6.0-alpha.2). D50 gets the user to
1009
+ a wired editor; a first-time user then stops at "it's wired" with no idea
1010
+ what to do next. `init` now ends with one concrete next step:
1011
+ ``Next step: `open-memex add "standup is at 9:30"` — then ask your agent what
1012
+ it remembers.`` One line, printed unconditionally — the cheapest possible
1013
+ onboarding after wiring. *Rationale: the CLI is editor-independent, so the
1014
+ hint works no matter which client was wired; `add` + recall is the smallest
1015
+ loop that proves the whole system works. Approved 2026-09-30.*
1016
+
1017
+ - **D52** — agent-prompt clarity rewrite (0.6.0-alpha.3). Both agent-facing
1018
+ prompts (MCP `initialize` instructions in `src/mcp.ts`, init instruction
1019
+ template in `src/init.ts`) are rewritten to fix ambiguities found on review:
1020
+ (1) the proactive-save vs approval-gate contradiction is resolved by naming
1021
+ the two cases — facts the user *states* are saved proactively, conclusions
1022
+ the agent *infers* are proposed first and saved only on approval;
1023
+ (2) `memory_status`/`memory_search` are excluded from the "after any
1024
+ memory_* action" checkpoint trigger (self-trigger loop);
1025
+ (3) tool names use the full `memory_*` form in both prompts;
1026
+ (4) the four checkpoints are defined once as "Checkpoints" and referenced,
1027
+ with an operational heuristic ("a task the user would describe in one
1028
+ sentence") replacing "meaningful chunk of work";
1029
+ (5) empty outbox → do nothing; "the user commits" → "any git commit in this
1030
+ session"; `type "reference"` named explicitly; PR-base mechanics spelled out
1031
+ per D28. *Rationale: these prompts are the product's UI for agents — a
1032
+ literal-minded agent must execute them correctly without guessing.
1033
+ Approved 2026-10-01.*
1034
+
1035
+ - **D53** — push-not-poll outbox: the checkpoint mechanism is retired; code
1036
+ pushes state to the agent instead (0.6.0-alpha.4). `src/submit.ts` gains
1037
+ `outboxDraftCount()` (one indexed SQLite COUNT on the current scope's outbox,
1038
+ no git I/O); `src/tools/ops.ts` gains `withOutboxNote()`, which appends
1039
+ `[open-memex: N draft(s) waiting in the project outbox — call memory_status
1040
+ to review]` to mutating tool results, only when N > 0. It is wired into the
1041
+ five MCP mutating tools (`memory_add`, `memory_supersede`, `memory_forget`,
1042
+ `memory_submit`, `memory_propose`) and the three opencode-plugin mutating
1043
+ tools (`memory_add`, `memory_supersede`, `memory_forget` — there the note
1044
+ stays generic because the plugin has no `memory_status`). The MCP server
1045
+ also appends the live draft count to the `initialize` instructions when N >
1046
+ 0 (stdio servers start fresh per session, so construction-time state is
1047
+ session-start state). Both agent-facing prompts drop the Checkpoints section
1048
+ and the post-`memory_submit`/`memory_propose` status checks; the sync rule
1049
+ becomes one line ("when the server reports drafts waiting, call
1050
+ `memory_status`") plus the existing "sync memory" trigger. The static init
1051
+ template keeps one explicit session-start `memory_status` call, since a
1052
+ static file cannot carry live state. An anti-nag clause is added: if the
1053
+ agent already asked about these drafts this session, it does not ask again.
1054
+ *Rationale: the server knows the outbox state; making the agent poll for it
1055
+ on a timer wastes tool calls and teaches a habit that scales badly. The note
1056
+ is silent when the outbox is empty, so the common case costs nothing.
1057
+ `memory_submit` drains the outbox, so its own note is naturally silent.
1058
+ Approved 2026-10-01.*
978
1059
 
979
1060
  ## Open Questions
980
1061
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.5.1",
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
@@ -556,7 +556,28 @@ function printMcpConfig(client: string): never {
556
556
 
557
557
  async function main() {
558
558
  const [cmd, ...rest] = process.argv.slice(2);
559
- 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);
560
581
 
561
582
  if (cmd === "--version" || cmd === "-v") {
562
583
  // package.json sits two levels above this file in both layouts
@@ -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.`,
@@ -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
  }
package/src/mcp.ts CHANGED
@@ -52,8 +52,10 @@ import {
52
52
  memoryResolveArgs,
53
53
  memoryPrStatusArgs,
54
54
  TOOL_DESCRIPTIONS,
55
+ withOutboxNote,
55
56
  type ToolResult,
56
57
  } from "./tools/ops.ts";
58
+ import { outboxDraftCount } from "./submit.ts";
57
59
 
58
60
  // Server version tracks package.json — never hardcode it here again.
59
61
  // package.json sits two levels above this file in both layouts
@@ -75,40 +77,49 @@ const SERVER_VERSION: string = (() => {
75
77
  * client at connect time. Still advisory — no MCP consumer offers a hard
76
78
  * session-start hook — but it is the strongest signal available.
77
79
  */
78
- const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
79
- (eleven memory_* tools: add, search, list, supersede, forget, status, submit,
80
- propose, promote, resolve, pr_status).
80
+ const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server.
81
+ Its tools are memory_add, memory_search, memory_list, memory_supersede,
82
+ memory_forget, memory_status, memory_submit, memory_propose, memory_promote,
83
+ memory_resolve, memory_pr_status — always call them by these full names.
81
84
 
82
- - At the START of this session, when you finish a meaningful chunk of work,
83
- after the user commits (git commit), and after any memory_* action completes,
84
- call memory_status. If the project outbox has drafts waiting for review,
85
+ - The server tells you when project outbox drafts are waiting for review —
86
+ in tool results, and in these session-start instructions. When it does,
85
87
  summarize them (one line each) and ask the user which ones to sync into the
86
- repo. Sync NOTHING the user did not name.
87
- - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
88
- the sync flow: call memory_status, summarize the outbox drafts, and ask which
89
- ones to sync. ALWAYS use the memory_status tool for this — never browse the
90
- appdata directory directly.
88
+ repo; sync NOTHING the user did not name. If you already asked about these
89
+ drafts this session, don't ask again. When the server reports none waiting,
90
+ do nothing.
91
+ - When the user says "sync memory" (or "同步记忆"), call memory_status,
92
+ summarize the outbox drafts (one line each), and ask which ones to sync.
93
+ ALWAYS use the memory_status tool for this — never browse the memory data
94
+ directory directly.
91
95
  - After memory_submit, ask ONE follow-up: "want me to create a branch + push +
92
96
  open the PR, or will you handle it yourself?" NEVER create branches, push, or
93
97
  open PRs without the user's explicit approval. A "yes, you do it" covers the
94
98
  whole chain — do NOT re-ask at each step.
95
- - BE PROACTIVE: when the user shares something worth remembering across sessions
96
- (a decision, a preference, a project convention, a fix and its cause), call
97
- memory_add without being asked. Keep each memory to one self-contained statement.
98
- - At the same checkpoints (session start, end of a work chunk, after the user
99
- commits, after any memory_* action), DISTILL the session: propose 1–3 short
100
- memories capturing the useful conclusion — what was learned or decided, how an
101
- issue was resolved, what to avoid, where the authoritative doc lives — not the
102
- raw transcript. Save NOTHING the user did not approve; on approval call
103
- memory_add with source "inference" at the confirmed scope. If the knowledge
104
- already lives in project docs, save a \`reference\` memory pointing at the doc
105
- instead of copying it. Long-form notes are fine ONLY when the user explicitly
106
- asks to save one.
99
+ - BE PROACTIVE about facts the user states directly: when the user shares a
100
+ decision, preference, project convention, or fix-and-cause worth remembering
101
+ across sessions, call memory_add without being asked. Keep each memory to one
102
+ self-contained statement, and add a brief "(noted in memory)" so the user
103
+ sees it worked.
104
+ - For conclusions YOU infer (the user never stated them): when you finish a
105
+ task the user would describe in one sentence, consider distilling the
106
+ session — if there is something worth keeping,
107
+ propose 1–3 short memories capturing the useful conclusion (what was learned
108
+ or decided, how an issue was resolved, what to avoid, where the authoritative
109
+ doc lives — not the raw transcript), each with its proposed scope. Save
110
+ NOTHING the user did not approve; on approval call memory_add with source
111
+ "inference" at the approved scope. If the knowledge already lives in project
112
+ docs, save it as type "reference" pointing at the doc instead of copying it.
113
+ Long-form notes are fine ONLY when the user explicitly asks to save one.
107
114
  - Before asking the user about past decisions, conventions, or preferences they
108
- may have told you before, call memory_search first.
115
+ may have told you before, call memory_search first — try a few keyword
116
+ variants (including the user's own language) when the first search comes up
117
+ empty.
109
118
  - Memories default to this project's scope; use the personal scope for facts
110
- about the user that hold across all projects.
111
- - personal scope memories NEVER leave this machine.`;
119
+ about the user that hold across all projects. When a saved fact becomes
120
+ outdated, call memory_supersede (find the old memory's id with memory_search
121
+ first) instead of adding a duplicate.
122
+ `;
112
123
 
113
124
  /** Adapt a framework-agnostic op result to an MCP tool response. */
114
125
  function toMcp(p: Promise<ToolResult>) {
@@ -164,9 +175,17 @@ export async function runMcpServer() {
164
175
  };
165
176
  };
166
177
 
178
+ // D53: session-start outbox state, pushed. The stdio server starts fresh per
179
+ // session, so construction-time state ≈ session-start state.
180
+ const n = outboxDraftCount(scope.key);
181
+ const instructions =
182
+ n > 0
183
+ ? `${SERVER_INSTRUCTIONS}\n\nSession start: the project outbox has ${n} draft${n === 1 ? "" : "s"} waiting for review — call memory_status to see them.`
184
+ : SERVER_INSTRUCTIONS;
185
+
167
186
  const server = new McpServer(
168
187
  { name: "open-memex", version: SERVER_VERSION },
169
- { instructions: SERVER_INSTRUCTIONS },
188
+ { instructions },
170
189
  );
171
190
 
172
191
  server.registerTool(
@@ -175,7 +194,7 @@ export async function runMcpServer() {
175
194
  description: TOOL_DESCRIPTIONS.memory_add,
176
195
  inputSchema: z.object(memoryAddArgs),
177
196
  },
178
- withSync((args) => toMcp(addMemory(getScope, cfg, args))),
197
+ withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))),
179
198
  );
180
199
 
181
200
  server.registerTool(
@@ -204,7 +223,7 @@ export async function runMcpServer() {
204
223
  description: TOOL_DESCRIPTIONS.memory_supersede,
205
224
  inputSchema: z.object(memorySupersedeArgs),
206
225
  },
207
- withSync((args) => toMcp(supersedeMemory(cfg, args))),
226
+ withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))),
208
227
  );
209
228
 
210
229
  server.registerTool(
@@ -214,7 +233,7 @@ export async function runMcpServer() {
214
233
  inputSchema: z.object(memoryForgetArgs),
215
234
  annotations: { destructiveHint: true },
216
235
  },
217
- withSync((args) => toMcp(forgetMemory(args))),
236
+ withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))),
218
237
  );
219
238
 
220
239
  server.registerTool(
@@ -233,7 +252,7 @@ export async function runMcpServer() {
233
252
  description: TOOL_DESCRIPTIONS.memory_submit,
234
253
  inputSchema: z.object(memorySubmitArgs),
235
254
  },
236
- withSync((args) => toMcp(submitMemoriesOp(args))),
255
+ withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))),
237
256
  );
238
257
 
239
258
  server.registerTool(
@@ -242,7 +261,7 @@ export async function runMcpServer() {
242
261
  description: TOOL_DESCRIPTIONS.memory_propose,
243
262
  inputSchema: z.object(memoryProposeArgs),
244
263
  },
245
- withSync((args) => toMcp(proposeMemoriesOp(args))),
264
+ withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))),
246
265
  );
247
266
 
248
267
  server.registerTool(
package/src/paths.ts CHANGED
@@ -21,6 +21,15 @@ export interface Paths {
21
21
 
22
22
  let _cached: Paths | null = null;
23
23
 
24
+ /**
25
+ * Read-only data-root path — never creates the directory. For existence
26
+ * checks (D50 first-run detection) that must not pollute storage; contrast
27
+ * `paths()`, which mkdirs as a side effect.
28
+ */
29
+ export function dataRootPath(): string {
30
+ return dataRoot();
31
+ }
32
+
24
33
  export function paths(): Paths {
25
34
  if (_cached) return _cached;
26
35
  const root = dataRoot();