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