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 +7 -0
- package/README.md +18 -8
- package/README.zh-CN.md +14 -8
- package/dist/cli.js +21 -1
- package/dist/distill-agents.js +3 -2
- package/dist/first-run.js +75 -0
- package/dist/init.js +68 -42
- package/dist/mcp.js +49 -33
- package/dist/paths.js +8 -0
- package/dist/submit.js +13 -0
- package/dist/tools/memory.js +4 -4
- package/dist/tools/ops.js +16 -1
- package/docs/V2-DESIGN.md +81 -0
- package/package.json +3 -2
- package/scripts/postinstall.js +9 -0
- package/src/cli.ts +22 -1
- package/src/distill-agents.ts +4 -3
- package/src/first-run.ts +96 -0
- package/src/init.ts +69 -44
- package/src/mcp.ts +51 -32
- package/src/paths.ts +9 -0
- package/src/submit.ts +16 -0
- package/src/tools/memory.ts +4 -3
- package/src/tools/ops.ts +21 -1
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.
|
|
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
|
|
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
|
package/src/distill-agents.ts
CHANGED
|
@@ -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
|
|
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
|
-
`-
|
|
76
|
-
`
|
|
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/first-run.ts
ADDED
|
@@ -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\`)
|
|
55
|
-
\`memory_add\`, \`memory_search\`, \`memory_list\`, \`memory_supersede\`,
|
|
56
|
-
\`memory_status\`, \`memory_submit\`, \`memory_propose\`,
|
|
57
|
-
\`memory_pr_status
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
80
|
-
Syncing them into the repo for review is an explicit, user-approved step:
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
- When the user approves, call \`memory_submit\` with the approved ids. It
|
|
90
|
-
the drafts into the
|
|
91
|
-
and
|
|
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
|
|
94
|
-
the whole chain — branch creation, push, PR creation — do NOT re-ask
|
|
95
|
-
step. If the user says they will do it themselves, hand them the
|
|
96
|
-
push/PR commands and do nothing. NEVER create branches, push, or open
|
|
97
|
-
without their explicit approval.
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
-
|
|
83
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
memory_add without being asked. Keep each memory to one
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
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();
|