open-memex 0.5.0 → 0.6.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -4
- package/CONTRIBUTING.md +8 -5
- package/README.md +49 -11
- package/README.zh-CN.md +35 -11
- package/dist/cli.js +31 -10
- package/dist/distill-agents.js +3 -2
- package/dist/doctor.js +140 -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/SCOPES.md +7 -6
- package/docs/TEST-PLAN.md +10 -3
- package/docs/USER-GUIDE.md +4 -5
- package/docs/USER-GUIDE.zh-CN.md +3 -4
- package/docs/V2-DESIGN.md +90 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +9 -0
- package/src/cli.ts +32 -10
- package/src/distill-agents.ts +4 -3
- package/src/doctor.ts +139 -2
- 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/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
|
@@ -329,8 +329,9 @@ Example:
|
|
|
329
329
|
open-memex capture --dry-run "remember: we deploy on Fridays"`,
|
|
330
330
|
|
|
331
331
|
doctor: `Environment health check: Node version, config source, scope resolution,
|
|
332
|
-
storage writability,
|
|
333
|
-
|
|
332
|
+
storage writability, VS Code MCP enablement (settings.json + system policy),
|
|
333
|
+
then boots a real MCP server and runs initialize + tools/list against it —
|
|
334
|
+
all eleven tools must show up.
|
|
334
335
|
|
|
335
336
|
Usage: open-memex doctor
|
|
336
337
|
|
|
@@ -377,7 +378,7 @@ Usage:
|
|
|
377
378
|
Every command has its own help with description and examples:
|
|
378
379
|
open-memex <command> --help (or -h)
|
|
379
380
|
|
|
380
|
-
One-command project setup: \`open-memex init\` (or \`npx open-memex
|
|
381
|
+
One-command project setup: \`open-memex init\` (or \`npx -y open-memex init\`) detects
|
|
381
382
|
your installed editors and wires them all — user-level where the editor supports it
|
|
382
383
|
(VS Code / Cursor MCP config, opencode native plugin), so one init covers every project;
|
|
383
384
|
Visual Studio is included when the project has a solution file (its \`.mcp.json\`
|
|
@@ -392,9 +393,9 @@ confirms the detected editors and asks a couple of settings (keyword capture,
|
|
|
392
393
|
first-turn injection); \`--yes\` accepts all defaults, and non-terminal runs never prompt.
|
|
393
394
|
\`open-memex config set <key> <value>\` changes those settings after install.
|
|
394
395
|
|
|
395
|
-
Once installed globally (\`npm i -g open-memex
|
|
396
|
-
available directly: \`open-memex mcp\` starts the stdio MCP server (
|
|
397
|
-
|
|
396
|
+
Once installed globally (\`npm i -g open-memex\`) the \`open-memex\` command is
|
|
397
|
+
available directly: \`open-memex mcp\` starts the stdio MCP server (11 tools, a superset of
|
|
398
|
+
the opencode plugin's five memory_* tools); \`open-memex mcp --print-config <client>\`
|
|
398
399
|
prints a copy-paste MCP client config snippet.
|
|
399
400
|
|
|
400
401
|
Scope defaults to \`project\` (derived from cwd's git remote or path).
|
|
@@ -471,7 +472,7 @@ function resolveCliScope(flags: Record<string, string>, project: Scope): Scope {
|
|
|
471
472
|
}
|
|
472
473
|
|
|
473
474
|
/** Print a copy-paste MCP client config snippet. Requires a global install
|
|
474
|
-
* (`npm i -g open-memex
|
|
475
|
+
* (`npm i -g open-memex`) so the `open-memex` command is on PATH. */
|
|
475
476
|
function printMcpConfig(client: string): never {
|
|
476
477
|
const c = client.toLowerCase();
|
|
477
478
|
// D17: resolve the server command the same way `init` does.
|
|
@@ -547,7 +548,7 @@ function printMcpConfig(client: string): never {
|
|
|
547
548
|
}
|
|
548
549
|
if (!mc.durable) {
|
|
549
550
|
console.error(
|
|
550
|
-
`\n# note: no durable \`open-memex\` on PATH — snippet uses npx. \`npm i -g open-memex
|
|
551
|
+
`\n# note: no durable \`open-memex\` on PATH — snippet uses npx. \`npm i -g open-memex\` for faster startup.`,
|
|
551
552
|
);
|
|
552
553
|
}
|
|
553
554
|
process.exit(0);
|
|
@@ -555,7 +556,28 @@ function printMcpConfig(client: string): never {
|
|
|
555
556
|
|
|
556
557
|
async function main() {
|
|
557
558
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
558
|
-
if (!cmd
|
|
559
|
+
if (!cmd) {
|
|
560
|
+
// D50: fresh machine + interactive terminal → offer init instead of bare usage.
|
|
561
|
+
// Non-interactive (CI/scripts/pipes) prints usage exactly as before.
|
|
562
|
+
const { offerFirstRunInit } = await import("./first-run.ts");
|
|
563
|
+
const outcome = await offerFirstRunInit(async () => {
|
|
564
|
+
// Re-exec `open-memex init` as a child with inherited stdio instead of
|
|
565
|
+
// calling initProject() in-process: the offer's readline already
|
|
566
|
+
// consumed stdin's buffer, and a second readline on the same stream
|
|
567
|
+
// would see EOF on burst input instead of the user's next answers.
|
|
568
|
+
const { spawnSync } = await import("node:child_process");
|
|
569
|
+
const r = spawnSync(
|
|
570
|
+
process.execPath,
|
|
571
|
+
[...process.execArgv, fileURLToPath(import.meta.url), "init"],
|
|
572
|
+
{ stdio: "inherit" },
|
|
573
|
+
);
|
|
574
|
+
if (r.error) throw r.error;
|
|
575
|
+
if ((r.status ?? 1) !== 0) process.exit(r.status ?? 1);
|
|
576
|
+
});
|
|
577
|
+
if (outcome !== "initialized") usage(0);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") usage(0);
|
|
559
581
|
|
|
560
582
|
if (cmd === "--version" || cmd === "-v") {
|
|
561
583
|
// package.json sits two levels above this file in both layouts
|
|
@@ -724,7 +746,7 @@ async function main() {
|
|
|
724
746
|
return;
|
|
725
747
|
}
|
|
726
748
|
|
|
727
|
-
// `mcp` starts the stdio MCP server (
|
|
749
|
+
// `mcp` starts the stdio MCP server (11 tools; the opencode plugin exposes 5).
|
|
728
750
|
// Branched before db() — runMcpServer() does its own init, and stdout must
|
|
729
751
|
// stay clean for the MCP protocol.
|
|
730
752
|
if (cmd === "mcp") {
|
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/doctor.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// Read-only except that paths() and the MCP handshake may create the (empty)
|
|
4
4
|
// data directories, exactly like a normal `open-memex mcp` start would.
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { loadConfig, configSource, DEFAULT_CONFIG } from "./config.ts";
|
|
10
11
|
import { paths } from "./paths.ts";
|
|
@@ -158,9 +159,145 @@ function mcpCheck(): Promise<Check> {
|
|
|
158
159
|
});
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
/** Parse JSON tolerating line/block comments plus trailing commas
|
|
163
|
+
* (VS Code's settings.json is JSONC). Health-check grade, not a full parser. */
|
|
164
|
+
function parseLenientJson(raw: string): Record<string, unknown> {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
167
|
+
} catch {
|
|
168
|
+
/* fall through to comment stripping */
|
|
169
|
+
}
|
|
170
|
+
let out = "";
|
|
171
|
+
let i = 0;
|
|
172
|
+
let inStr = false;
|
|
173
|
+
let esc = false;
|
|
174
|
+
while (i < raw.length) {
|
|
175
|
+
const c = raw[i];
|
|
176
|
+
const n = raw[i + 1];
|
|
177
|
+
if (inStr) {
|
|
178
|
+
out += c;
|
|
179
|
+
if (esc) esc = false;
|
|
180
|
+
else if (c === "\\") esc = true;
|
|
181
|
+
else if (c === '"') inStr = false;
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (c === '"') {
|
|
186
|
+
inStr = true;
|
|
187
|
+
out += c;
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (c === "/" && n === "/") {
|
|
192
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (c === "/" && n === "*") {
|
|
196
|
+
i += 2;
|
|
197
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
198
|
+
i += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
out += c;
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
out = out.replace(/,\s*([}\]])/g, "$1");
|
|
205
|
+
return JSON.parse(out) as Record<string, unknown>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function vscodeSettingsPath(): string | null {
|
|
209
|
+
const home = os.homedir();
|
|
210
|
+
if (process.platform === "win32") {
|
|
211
|
+
const appdata = process.env.APPDATA;
|
|
212
|
+
return appdata ? path.join(appdata, "Code", "User", "settings.json") : null;
|
|
213
|
+
}
|
|
214
|
+
if (process.platform === "darwin") {
|
|
215
|
+
return path.join(home, "Library", "Application Support", "Code", "User", "settings.json");
|
|
216
|
+
}
|
|
217
|
+
return path.join(home, ".config", "Code", "User", "settings.json");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** True when a Windows system policy disables MCP (value name contains "mcp",
|
|
221
|
+
* data is 0/0x0/false). Checks HKLM and HKCU policy keys. */
|
|
222
|
+
function windowsMcpPolicyDisabled(): string | null {
|
|
223
|
+
if (process.platform !== "win32") return null;
|
|
224
|
+
for (const hive of ["HKLM", "HKCU"]) {
|
|
225
|
+
try {
|
|
226
|
+
const stdout = execFileSync(
|
|
227
|
+
"reg",
|
|
228
|
+
["query", `${hive}\\SOFTWARE\\Policies\\Microsoft\\VSCode`],
|
|
229
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] },
|
|
230
|
+
) as string;
|
|
231
|
+
for (const line of stdout.split("\n")) {
|
|
232
|
+
const m = line.match(/^\s{2,}(\S+)\s+REG_\w+\s+(\S+)/);
|
|
233
|
+
if (m && /mcp/i.test(m[1]) && /^(0x0|0|false)$/i.test(m[2])) {
|
|
234
|
+
return `${hive}\\SOFTWARE\\Policies\\Microsoft\\VSCode!${m[1]}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
/* policy key absent — no policy */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function settingsMcpDisabled(file: string): boolean {
|
|
245
|
+
try {
|
|
246
|
+
const parsed = parseLenientJson(fs.readFileSync(file, "utf8"));
|
|
247
|
+
return parsed["chat.mcp.enabled"] === false;
|
|
248
|
+
} catch {
|
|
249
|
+
return false; // unreadable file: don't claim anything
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** VS Code ignores MCP server entries entirely when MCP is switched off —
|
|
254
|
+
* either in settings.json or, on managed machines, by system policy. */
|
|
255
|
+
function vscodeMcpCheck(): Check {
|
|
256
|
+
const name = "vscode-mcp";
|
|
257
|
+
const policyHit = windowsMcpPolicyDisabled();
|
|
258
|
+
if (policyHit) {
|
|
259
|
+
return {
|
|
260
|
+
name,
|
|
261
|
+
ok: false,
|
|
262
|
+
detail:
|
|
263
|
+
`MCP is disabled by system policy (${policyHit}) — managed by your organization. ` +
|
|
264
|
+
`VS Code will ignore open-memex's MCP entry on this machine.`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const files: string[] = [];
|
|
268
|
+
const user = vscodeSettingsPath();
|
|
269
|
+
if (user) files.push(user);
|
|
270
|
+
const ws = path.join(process.cwd(), ".vscode", "settings.json");
|
|
271
|
+
if (!files.includes(ws)) files.push(ws);
|
|
272
|
+
const found: string[] = [];
|
|
273
|
+
const disabled: string[] = [];
|
|
274
|
+
for (const f of files) {
|
|
275
|
+
if (!fs.existsSync(f)) continue;
|
|
276
|
+
found.push(f);
|
|
277
|
+
if (settingsMcpDisabled(f)) disabled.push(f);
|
|
278
|
+
}
|
|
279
|
+
if (disabled.length > 0) {
|
|
280
|
+
return {
|
|
281
|
+
name,
|
|
282
|
+
ok: false,
|
|
283
|
+
detail:
|
|
284
|
+
`chat.mcp.enabled is false in ${disabled.join(", ")} — VS Code will not load ` +
|
|
285
|
+
`MCP servers. Set it to true (or ask IT if the setting shows as managed).`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
name,
|
|
290
|
+
ok: true,
|
|
291
|
+
detail:
|
|
292
|
+
found.length > 0
|
|
293
|
+
? `MCP enabled (checked ${found.join(", ")})`
|
|
294
|
+
: "VS Code settings not found on this machine — nothing to check",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
161
298
|
export async function runDoctor(): Promise<boolean> {
|
|
162
299
|
console.log("open-memex doctor");
|
|
163
|
-
const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck()];
|
|
300
|
+
const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), vscodeMcpCheck()];
|
|
164
301
|
checks.push(await mcpCheck());
|
|
165
302
|
let allOk = true;
|
|
166
303
|
for (const c of checks) {
|
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
|
}
|