open-memex 0.6.2 → 0.7.0-alpha.11
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 +25 -7
- package/CHANGELOG.md +103 -1
- package/README.md +54 -16
- package/README.zh-CN.md +30 -15
- package/dist/.build-stamp +1 -0
- package/dist/capture/keywords.js +41 -8
- package/dist/cli.js +259 -40
- package/dist/config.js +102 -18
- package/dist/distill-agents.js +9 -1
- package/dist/doctor.js +180 -79
- package/dist/export.js +12 -2
- package/dist/index.js +20 -112
- package/dist/init.js +103 -11
- package/dist/inventory-html.js +150 -0
- package/dist/inventory.js +176 -0
- package/dist/mcp.js +21 -5
- package/dist/opencode-v2.js +43 -0
- package/dist/paths.js +34 -2
- package/dist/plugin-core.js +138 -0
- package/dist/retrieve/display.js +161 -0
- package/dist/retrieve/inject.js +1 -1
- package/dist/retrieve/search.js +44 -5
- package/dist/review.js +9 -3
- package/dist/store/db.js +11 -9
- package/dist/store/lifecycle.js +20 -4
- package/dist/store/markdown.js +53 -7
- package/dist/store/migrate.js +14 -3
- package/dist/store/sync.js +5 -3
- package/dist/store/v2migrate.js +2 -2
- package/dist/submit.js +13 -5
- package/dist/tools/memory.js +28 -0
- package/dist/tools/ops.js +127 -16
- package/docs/CURATOR.md +7 -3
- package/docs/V2-DESIGN.md +335 -32
- package/docs/memory-visibility-design.md +130 -0
- package/package.json +2 -2
- package/scripts/build-stamp.mjs +63 -0
- package/scripts/smoke-mcp.ts +47 -1
- package/scripts/smoke-pure.ts +237 -3
- package/scripts/test-full.ts +362 -5
- package/skills/open-memex/SKILL.md +53 -1
- package/src/capture/keywords.ts +56 -6
- package/src/cli.ts +282 -39
- package/src/config.ts +104 -17
- package/src/distill-agents.ts +19 -0
- package/src/doctor.ts +181 -71
- package/src/export.ts +14 -2
- package/src/index.ts +25 -112
- package/src/init.ts +108 -11
- package/src/inventory-html.ts +191 -0
- package/src/inventory.ts +242 -0
- package/src/mcp.ts +21 -5
- package/src/opencode-v2.ts +119 -0
- package/src/paths.ts +35 -1
- package/src/plugin-core.ts +152 -0
- package/src/retrieve/display.ts +174 -0
- package/src/retrieve/inject.ts +1 -1
- package/src/retrieve/search.ts +63 -6
- package/src/review.ts +9 -2
- package/src/store/db.ts +11 -9
- package/src/store/lifecycle.ts +24 -3
- package/src/store/markdown.ts +60 -7
- package/src/store/migrate.ts +15 -2
- package/src/store/sync.ts +6 -3
- package/src/store/v2migrate.ts +2 -1
- package/src/submit.ts +12 -4
- package/src/tools/memory.ts +28 -0
- package/src/tools/ops.ts +147 -17
package/dist/doctor.js
CHANGED
|
@@ -7,17 +7,25 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { spawn, execFileSync } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { loadConfig, configSource, DEFAULT_CONFIG } from "./config.js";
|
|
11
|
-
import { paths } from "./paths.js";
|
|
10
|
+
import { loadConfig, configSource, DEFAULT_CONFIG, stripJsonComments } from "./config.js";
|
|
11
|
+
import { paths, homeOverride } from "./paths.js";
|
|
12
12
|
import { resolveCwdScope } from "./scope.js";
|
|
13
13
|
import { db, backendName } from "./store/db.js";
|
|
14
14
|
import { LEGACY_PACKAGE_NAME, opencodeGlobalConfigPath, userMcpConfigPath } from "./init.js";
|
|
15
|
+
// D64: the MCP server exposes eleven tools; checking only the five the
|
|
16
|
+
// opencode plugin carries let a half-registered server pass as healthy.
|
|
15
17
|
const EXPECTED_TOOLS = [
|
|
16
18
|
"memory_add",
|
|
17
19
|
"memory_search",
|
|
18
20
|
"memory_list",
|
|
19
21
|
"memory_supersede",
|
|
20
22
|
"memory_forget",
|
|
23
|
+
"memory_status",
|
|
24
|
+
"memory_submit",
|
|
25
|
+
"memory_propose",
|
|
26
|
+
"memory_promote",
|
|
27
|
+
"memory_resolve",
|
|
28
|
+
"memory_pr_status",
|
|
21
29
|
];
|
|
22
30
|
function nodeCheck() {
|
|
23
31
|
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
@@ -54,7 +62,13 @@ function storageCheck() {
|
|
|
54
62
|
try {
|
|
55
63
|
const p = paths();
|
|
56
64
|
fs.accessSync(p.memories, fs.constants.W_OK);
|
|
57
|
-
|
|
65
|
+
// D62: when the root came from the legacy env var, say so (still ok —
|
|
66
|
+
// the fallback is supported, doctor just names the current one).
|
|
67
|
+
const via = homeOverride()?.via;
|
|
68
|
+
const note = via === "MY_O_MEMORY_HOME"
|
|
69
|
+
? " (via legacy MY_O_MEMORY_HOME — OPEN_MEMEX_HOME is the current name)"
|
|
70
|
+
: "";
|
|
71
|
+
return { name: "storage", ok: true, detail: `${p.root} (writable)${note}` };
|
|
58
72
|
}
|
|
59
73
|
catch (err) {
|
|
60
74
|
return { name: "storage", ok: false, detail: String(err) };
|
|
@@ -82,14 +96,17 @@ function lockingCheck() {
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
/**
|
|
85
|
-
* D59: patrol the opencode native-plugin
|
|
86
|
-
* silent — the entry was configured, the host never loaded it, and
|
|
87
|
-
* anywhere said why.
|
|
88
|
-
* 1. the entry's target
|
|
89
|
-
*
|
|
90
|
-
* 2. the plugin's import closure runtime-imports
|
|
91
|
-
* (
|
|
92
|
-
*
|
|
99
|
+
* D59/D60: patrol the opencode native-plugin entries. The D58 field failure
|
|
100
|
+
* was silent — the entry was configured, the host never loaded it, and
|
|
101
|
+
* nothing anywhere said why. Death modes to catch:
|
|
102
|
+
* 1. the entry's target does not exist (package moved, nvm rolled to a
|
|
103
|
+
* new version dir, checkout deleted);
|
|
104
|
+
* 2. the plugin's import closure runtime-imports a host SDK package
|
|
105
|
+
* (@opencode-ai/plugin on v1, @opencode/plugin on v2) — unresolvable
|
|
106
|
+
* from a global install, the host silently skips the plugin (D58);
|
|
107
|
+
* 3. (D60) the config wires only the other host generation: an
|
|
108
|
+
* opencode 2 host never reads the v1 `plugin` file entry (it demands
|
|
109
|
+
* a directory under `plugins`), and opencode 1 cannot read `plugins`.
|
|
93
110
|
* Read-only; only inspects the user-level opencode configs.
|
|
94
111
|
*/
|
|
95
112
|
function opencodePluginCheck() {
|
|
@@ -97,8 +114,18 @@ function opencodePluginCheck() {
|
|
|
97
114
|
try {
|
|
98
115
|
const jsonPath = opencodeGlobalConfigPath();
|
|
99
116
|
const candidates = [jsonPath, jsonPath.replace(/\.json$/, ".jsonc")];
|
|
100
|
-
const
|
|
117
|
+
const v1Entries = [];
|
|
118
|
+
const v2Entries = [];
|
|
101
119
|
let sawConfig = false;
|
|
120
|
+
const v2EntryPackage = (entry) => {
|
|
121
|
+
if (typeof entry === "string")
|
|
122
|
+
return entry;
|
|
123
|
+
if (entry !== null && typeof entry === "object" && "package" in entry) {
|
|
124
|
+
const p = entry.package;
|
|
125
|
+
return typeof p === "string" ? p : null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
102
129
|
for (const f of candidates) {
|
|
103
130
|
if (!fs.existsSync(f))
|
|
104
131
|
continue;
|
|
@@ -106,17 +133,24 @@ function opencodePluginCheck() {
|
|
|
106
133
|
const text = fs.readFileSync(f, "utf8");
|
|
107
134
|
try {
|
|
108
135
|
const doc = JSON.parse(text);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
if (Array.isArray(doc.plugin)) {
|
|
137
|
+
for (const e of doc.plugin)
|
|
138
|
+
if (typeof e === "string")
|
|
139
|
+
v1Entries.push(e);
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(doc.plugins)) {
|
|
142
|
+
for (const e of doc.plugins) {
|
|
143
|
+
const p = v2EntryPackage(e);
|
|
144
|
+
if (p !== null)
|
|
145
|
+
v2Entries.push(p);
|
|
146
|
+
}
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
catch {
|
|
116
150
|
// JSONC (comments/trailing commas): fall back to scanning quoted
|
|
117
151
|
// strings for plugin entries.
|
|
118
152
|
for (const m of text.matchAll(/"(file:[^"]+|[^"]+\.(?:ts|js|mts|mjs))"/g)) {
|
|
119
|
-
|
|
153
|
+
v1Entries.push(m[1]);
|
|
120
154
|
}
|
|
121
155
|
}
|
|
122
156
|
}
|
|
@@ -124,24 +158,71 @@ function opencodePluginCheck() {
|
|
|
124
158
|
return { name, ok: true, detail: "no global opencode config found (skipped)" };
|
|
125
159
|
}
|
|
126
160
|
const problems = [];
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
// Host SDK packages per generation; a runtime import of either from a
|
|
162
|
+
// globally installed plugin cannot resolve (D58).
|
|
163
|
+
const RUNTIME_SDK_IMPORT = /^\s*import\s+(?!type\b)[^;]*?["']@opencode(?:-ai)?\/plugin(?:\/[^"']*)?["']/m;
|
|
164
|
+
const RUNTIME_SDK_REQUIRE = /require\(\s*["']@opencode(?:-ai)?\/plugin(?:\/[^"']*)?["']\s*\)/;
|
|
165
|
+
const toPath = (entry) => {
|
|
166
|
+
try {
|
|
167
|
+
return entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
129
173
|
// An entry is ours when its path names the package, or when the target
|
|
130
174
|
// file carries the plugin's "[open-memex]" log marker (source checkouts
|
|
131
175
|
// can live at any path).
|
|
132
|
-
const
|
|
133
|
-
if (entry.includes("open-memex"))
|
|
134
|
-
return true;
|
|
176
|
+
const fileHasMarker = (target) => {
|
|
135
177
|
try {
|
|
136
|
-
const target = entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
137
|
-
if (!fs.existsSync(target))
|
|
138
|
-
return false;
|
|
139
178
|
return fs.readFileSync(target, "utf8").slice(0, 65536).includes("[open-memex]");
|
|
140
179
|
}
|
|
141
180
|
catch {
|
|
142
181
|
return false;
|
|
143
182
|
}
|
|
144
183
|
};
|
|
184
|
+
/** Resolve an OpenCode 2 directory entry to its entry file, if any. */
|
|
185
|
+
const resolveV2EntryFile = (target) => {
|
|
186
|
+
try {
|
|
187
|
+
if (!fs.existsSync(target))
|
|
188
|
+
return null;
|
|
189
|
+
if (fs.statSync(target).isFile())
|
|
190
|
+
return target;
|
|
191
|
+
for (const idx of ["index.ts", "index.js", "index.mts", "index.mjs"]) {
|
|
192
|
+
const p = path.join(target, idx);
|
|
193
|
+
if (fs.existsSync(p))
|
|
194
|
+
return p;
|
|
195
|
+
}
|
|
196
|
+
const pkgJson = path.join(target, "package.json");
|
|
197
|
+
if (fs.existsSync(pkgJson)) {
|
|
198
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
|
|
199
|
+
if (typeof pkg.main === "string") {
|
|
200
|
+
const main = path.join(target, pkg.main);
|
|
201
|
+
if (fs.existsSync(main))
|
|
202
|
+
return main;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
};
|
|
211
|
+
const isOursV1 = (entry) => {
|
|
212
|
+
if (entry.includes("open-memex"))
|
|
213
|
+
return true;
|
|
214
|
+
const target = toPath(entry);
|
|
215
|
+
return target !== null && fs.existsSync(target) && fileHasMarker(target);
|
|
216
|
+
};
|
|
217
|
+
const isOursV2 = (entry) => {
|
|
218
|
+
if (entry.includes("open-memex"))
|
|
219
|
+
return true;
|
|
220
|
+
const target = toPath(entry);
|
|
221
|
+
if (target === null)
|
|
222
|
+
return false;
|
|
223
|
+
const file = resolveV2EntryFile(target);
|
|
224
|
+
return file !== null && fileHasMarker(file);
|
|
225
|
+
};
|
|
145
226
|
const scanSdkImports = (entryFile) => {
|
|
146
227
|
// Walk the entry's relative-import closure looking for host-SDK
|
|
147
228
|
// runtime imports (type-only imports are erased and fine).
|
|
@@ -176,16 +257,14 @@ function opencodePluginCheck() {
|
|
|
176
257
|
}
|
|
177
258
|
return offenders;
|
|
178
259
|
};
|
|
179
|
-
const
|
|
180
|
-
|
|
260
|
+
const oursV1 = v1Entries.filter(isOursV1);
|
|
261
|
+
const oursV2 = v2Entries.filter(isOursV2);
|
|
262
|
+
if (oursV1.length === 0 && oursV2.length === 0) {
|
|
181
263
|
return { name, ok: true, detail: "no global plugin entry (per-project or MCP wiring is not checked)" };
|
|
182
264
|
}
|
|
183
|
-
for (const entry of
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
target = entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
265
|
+
for (const entry of oursV1) {
|
|
266
|
+
const target = toPath(entry);
|
|
267
|
+
if (target === null) {
|
|
189
268
|
problems.push(`entry "${entry}" is not a parseable file URL`);
|
|
190
269
|
continue;
|
|
191
270
|
}
|
|
@@ -194,16 +273,62 @@ function opencodePluginCheck() {
|
|
|
194
273
|
continue;
|
|
195
274
|
}
|
|
196
275
|
for (const f of scanSdkImports(target)) {
|
|
197
|
-
problems.push(`${f} runtime-imports
|
|
276
|
+
problems.push(`${f} runtime-imports a host SDK package — opencode silently skips plugins whose imports it cannot resolve from a global install (D58)`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const entry of oursV2) {
|
|
280
|
+
// A bare package name ("open-memex" via `opencode plugin add`) is
|
|
281
|
+
// resolved by the host's plugin manager; there is no local path to
|
|
282
|
+
// verify.
|
|
283
|
+
const isPackageName = !entry.startsWith("file:") &&
|
|
284
|
+
!entry.startsWith(".") &&
|
|
285
|
+
!path.isAbsolute(entry) &&
|
|
286
|
+
!/^[a-zA-Z]:[\\/]/.test(entry);
|
|
287
|
+
if (isPackageName)
|
|
288
|
+
continue;
|
|
289
|
+
const target = toPath(entry);
|
|
290
|
+
if (target === null || !fs.existsSync(target)) {
|
|
291
|
+
problems.push(`opencode 2 entry "${entry}" does not resolve to an existing path — run \`open-memex init --client opencode --global --force\` to re-point it`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const entryFile = resolveV2EntryFile(target);
|
|
295
|
+
if (entryFile === null) {
|
|
296
|
+
problems.push(`opencode 2 entry "${entry}" has no loadable plugin file (looked for index.ts/index.js and package.json main)`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
for (const f of scanSdkImports(entryFile)) {
|
|
300
|
+
problems.push(`${f} runtime-imports a host SDK package — opencode silently skips plugins whose imports it cannot resolve from a global install (D58)`);
|
|
198
301
|
}
|
|
199
302
|
}
|
|
303
|
+
// D60: generation mismatch — the config wires only the generation the
|
|
304
|
+
// installed host cannot read. Best-effort: no binary, no opinion.
|
|
305
|
+
let hostMajor = null;
|
|
306
|
+
try {
|
|
307
|
+
const out = execFileSync("opencode", ["--version"], {
|
|
308
|
+
timeout: 3000,
|
|
309
|
+
encoding: "utf8",
|
|
310
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
311
|
+
});
|
|
312
|
+
const m = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
313
|
+
if (m)
|
|
314
|
+
hostMajor = Number(m[1]);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
hostMajor = null;
|
|
318
|
+
}
|
|
319
|
+
if (hostMajor !== null && hostMajor >= 2 && oursV1.length > 0 && oursV2.length === 0) {
|
|
320
|
+
problems.push(`the installed opencode is v${hostMajor} but the config only has the opencode 1 "plugin" entry — v2 loads directories from "plugins" and will silently skip this plugin; run \`open-memex init --client opencode --global --force\` to add the v2 entry`);
|
|
321
|
+
}
|
|
322
|
+
else if (hostMajor === 1 && oursV2.length > 0 && oursV1.length === 0) {
|
|
323
|
+
problems.push(`the installed opencode is v1 but the config only has the opencode 2 "plugins" entry — v1 reads the "plugin" key and will not load this plugin; run \`open-memex init --client opencode --global --force\` to add the v1 entry`);
|
|
324
|
+
}
|
|
200
325
|
if (problems.length > 0) {
|
|
201
326
|
return { name, ok: false, detail: problems.join("; ") };
|
|
202
327
|
}
|
|
203
328
|
return {
|
|
204
329
|
name,
|
|
205
330
|
ok: true,
|
|
206
|
-
detail: `${
|
|
331
|
+
detail: `${oursV1.length + oursV2.length} plugin ${oursV1.length + oursV2.length === 1 ? "entry" : "entries"}: target exists, no host-SDK runtime imports`,
|
|
207
332
|
};
|
|
208
333
|
}
|
|
209
334
|
catch (err) {
|
|
@@ -290,7 +415,8 @@ function mcpCheck() {
|
|
|
290
415
|
});
|
|
291
416
|
}
|
|
292
417
|
/** Parse JSON tolerating line/block comments plus trailing commas
|
|
293
|
-
* (VS Code's settings.json is JSONC).
|
|
418
|
+
* (VS Code's settings.json is JSONC). Uses config.ts's string-aware
|
|
419
|
+
* stripper — one parser, not two (P0 review, 2026-10-03). */
|
|
294
420
|
function parseLenientJson(raw) {
|
|
295
421
|
try {
|
|
296
422
|
return JSON.parse(raw);
|
|
@@ -298,47 +424,7 @@ function parseLenientJson(raw) {
|
|
|
298
424
|
catch {
|
|
299
425
|
/* fall through to comment stripping */
|
|
300
426
|
}
|
|
301
|
-
|
|
302
|
-
let i = 0;
|
|
303
|
-
let inStr = false;
|
|
304
|
-
let esc = false;
|
|
305
|
-
while (i < raw.length) {
|
|
306
|
-
const c = raw[i];
|
|
307
|
-
const n = raw[i + 1];
|
|
308
|
-
if (inStr) {
|
|
309
|
-
out += c;
|
|
310
|
-
if (esc)
|
|
311
|
-
esc = false;
|
|
312
|
-
else if (c === "\\")
|
|
313
|
-
esc = true;
|
|
314
|
-
else if (c === '"')
|
|
315
|
-
inStr = false;
|
|
316
|
-
i++;
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
if (c === '"') {
|
|
320
|
-
inStr = true;
|
|
321
|
-
out += c;
|
|
322
|
-
i++;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (c === "/" && n === "/") {
|
|
326
|
-
while (i < raw.length && raw[i] !== "\n")
|
|
327
|
-
i++;
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
if (c === "/" && n === "*") {
|
|
331
|
-
i += 2;
|
|
332
|
-
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/"))
|
|
333
|
-
i++;
|
|
334
|
-
i += 2;
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
out += c;
|
|
338
|
-
i++;
|
|
339
|
-
}
|
|
340
|
-
out = out.replace(/,\s*([}\]])/g, "$1");
|
|
341
|
-
return JSON.parse(out);
|
|
427
|
+
return JSON.parse(stripJsonComments(raw));
|
|
342
428
|
}
|
|
343
429
|
function vscodeSettingsPath() {
|
|
344
430
|
const home = os.homedir();
|
|
@@ -448,13 +534,28 @@ export async function runDoctor() {
|
|
|
448
534
|
function legacyCheck() {
|
|
449
535
|
const name = "legacy my-o-memory";
|
|
450
536
|
const found = [];
|
|
451
|
-
|
|
537
|
+
// D62: the override may arrive via either env var name; check the value
|
|
538
|
+
// in effect, not just the legacy one.
|
|
539
|
+
const envHome = homeOverride()?.dir;
|
|
452
540
|
if (envHome && envHome.includes(LEGACY_PACKAGE_NAME))
|
|
453
|
-
found.push(`
|
|
541
|
+
found.push(`memory home points at a pre-rename dir (${envHome}) — unset it, then merge old data with \`open-memex migrate --to-v2\``);
|
|
454
542
|
const root = paths().root;
|
|
455
543
|
const legacyDir = path.join(path.dirname(root), LEGACY_PACKAGE_NAME);
|
|
456
544
|
if (fs.existsSync(legacyDir) && fs.statSync(legacyDir).isDirectory())
|
|
457
545
|
found.push(`legacy data dir ${legacyDir} — merge it with \`open-memex migrate --to-v2\``);
|
|
546
|
+
// D64: v1 stored personal memories under the scope dir "user"; v2 reads
|
|
547
|
+
// only "personal". Files left there are invisible to every read path —
|
|
548
|
+
// an upgrader silently sees an empty personal scope. Surface them here;
|
|
549
|
+
// the migration itself stays explicit (no silent moves).
|
|
550
|
+
try {
|
|
551
|
+
const userDir = path.join(root, "memories", "user");
|
|
552
|
+
if (fs.existsSync(userDir) &&
|
|
553
|
+
fs.readdirSync(userDir).some((f) => f.endsWith(".md")))
|
|
554
|
+
found.push("v1 personal memories under " + userDir + " are invisible to v2 reads — convert them with `open-memex migrate --to-v2` (it moves them to the personal scope; preview with --dry-run)");
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
/* unreadable — not this check's problem */
|
|
558
|
+
}
|
|
458
559
|
for (const f of [opencodeGlobalConfigPath(), userMcpConfigPath("vscode"), userMcpConfigPath("cursor")]) {
|
|
459
560
|
try {
|
|
460
561
|
if (fs.existsSync(f) && fs.readFileSync(f, "utf8").includes(LEGACY_PACKAGE_NAME))
|
package/dist/export.js
CHANGED
|
@@ -117,10 +117,20 @@ export function importBundle(bundlePath, opts) {
|
|
|
117
117
|
if (manifest.format !== EXPORT_FORMAT) {
|
|
118
118
|
fail(`unsupported bundle format: ${manifest.format} (expected ${EXPORT_FORMAT})`);
|
|
119
119
|
}
|
|
120
|
-
const result = { imported: 0, skippedIdentical: 0, skippedConflict: [] };
|
|
120
|
+
const result = { imported: 0, skippedIdentical: 0, skippedConflict: [], rejectedPaths: [] };
|
|
121
121
|
const existingStmt = db().prepare(`SELECT content_hash FROM memories WHERE id = ?`);
|
|
122
|
+
// The manifest comes from whoever sent the bundle: entry.file must
|
|
123
|
+
// stay inside the staging dir (P0 review, 2026-10-03 — "../" entries
|
|
124
|
+
// otherwise read arbitrary .md files into the store). realpath also
|
|
125
|
+
// catches symlinks planted by the tarball.
|
|
126
|
+
const stageRoot = fs.realpathSync(stage);
|
|
122
127
|
for (const entry of manifest.memories ?? []) {
|
|
123
|
-
const
|
|
128
|
+
const joined = path.resolve(path.join(stage, entry.file));
|
|
129
|
+
const src = fs.existsSync(joined) ? fs.realpathSync(joined) : joined;
|
|
130
|
+
if (!src.startsWith(stageRoot + path.sep)) {
|
|
131
|
+
result.rejectedPaths.push(entry.file);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
124
134
|
const mf = readMemoryFile(src);
|
|
125
135
|
if (!mf)
|
|
126
136
|
continue;
|
package/dist/index.js
CHANGED
|
@@ -1,127 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveProjectScope, resolveCwdScope, PERSONAL_SCOPE } from "./scope.js";
|
|
3
|
-
import { db } from "./store/db.js";
|
|
4
|
-
import { syncScope, upsertFromFile } from "./store/sync.js";
|
|
5
|
-
import { scopeHasFiles } from "./store/migrate.js";
|
|
6
|
-
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, } from "./store/markdown.js";
|
|
7
|
-
import { buildContextBlock } from "./retrieve/inject.js";
|
|
8
|
-
import { detectKeywords } from "./capture/keywords.js";
|
|
9
|
-
import { findDuplicates } from "./store/lifecycle.js";
|
|
10
|
-
import { redact } from "./redact.js";
|
|
1
|
+
import { PLUGIN_ID, bootstrapPlugin, captureFromText, injectOnce, } from "./plugin-core.js";
|
|
11
2
|
import { makeTools } from "./tools/memory.js";
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import { v2Plugin } from "./opencode-v2.js";
|
|
4
|
+
/**
|
|
5
|
+
* One entrypoint, both opencode generations (D60):
|
|
6
|
+
* - OpenCode 2 structurally decodes the default export as `{ id, setup }`
|
|
7
|
+
* and ignores `server` (see src/opencode-v2.ts).
|
|
8
|
+
* - OpenCode 1 (≥1.18.29) reads `server` off the same default export and
|
|
9
|
+
* ignores `setup` — the function below is the v1 plugin exactly as it
|
|
10
|
+
* behaved before dual support.
|
|
11
|
+
* Neither host runtime-imports an SDK package from this file: v1's SDK is
|
|
12
|
+
* type-only (D58), and v2's decode is structural.
|
|
13
|
+
*/
|
|
14
|
+
const server = async ({ worktree, directory }) => {
|
|
14
15
|
const roots = worktree || directory || process.cwd();
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
db();
|
|
19
|
-
syncScope(scope.key);
|
|
20
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
21
|
-
if (cfg.logLevel === "debug") {
|
|
22
|
-
console.log(`[open-memex] loaded. scope=${scope.key}`);
|
|
23
|
-
}
|
|
24
|
-
// If a git remote was added *after* memories were first captured, the
|
|
25
|
-
// scope key changes (cwd-hash -> origin-hash). Detect the legacy dir on
|
|
26
|
-
// disk and tell the user how to migrate. We do NOT auto-migrate: two
|
|
27
|
-
// different repos at the same cwd would collide.
|
|
28
|
-
const cwdScope = resolveCwdScope(roots);
|
|
29
|
-
if (cwdScope.key !== scope.key && scopeHasFiles(cwdScope.key)) {
|
|
30
|
-
console.warn(`[open-memex] found memories under legacy scope key "${cwdScope.key}". ` +
|
|
31
|
-
`Current project scope is "${scope.key}". ` +
|
|
32
|
-
`To move them: npm run cli -- migrate --from ${cwdScope.key}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
console.error("[open-memex] init failed:", err);
|
|
37
|
-
}
|
|
38
|
-
const tools = makeTools(() => scope, cfg);
|
|
39
|
-
const injectedSessions = new Set();
|
|
16
|
+
const state = bootstrapPlugin(roots);
|
|
17
|
+
const tools = makeTools(() => state.scope, state.cfg);
|
|
40
18
|
return {
|
|
41
19
|
tool: tools,
|
|
42
20
|
async "chat.message"(_input, output) {
|
|
43
|
-
if (!cfg.keywordCaptureEnabled)
|
|
44
|
-
return;
|
|
45
21
|
const parts = (output.parts ?? []);
|
|
46
22
|
const text = parts
|
|
47
23
|
.map((p) => (p?.type === "text" ? p.text ?? "" : ""))
|
|
48
24
|
.filter(Boolean)
|
|
49
25
|
.join("\n");
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
const hits = detectKeywords(text, cfg);
|
|
53
|
-
for (const h of hits) {
|
|
54
|
-
const { content, hadSecret, matchedPattern } = redact(h.content, cfg.redactPatterns);
|
|
55
|
-
// Secrets are masked (first 4 chars kept) and the capture proceeds;
|
|
56
|
-
// skip only when nothing usable remains.
|
|
57
|
-
if (content.length === 0)
|
|
58
|
-
continue;
|
|
59
|
-
if (hadSecret && cfg.logLevel === "debug") {
|
|
60
|
-
console.log(`[open-memex] keyword capture masked secret (${matchedPattern})`);
|
|
61
|
-
}
|
|
62
|
-
// Personal patterns ("remember for me" / "记住(个人)") force the personal scope.
|
|
63
|
-
const target = h.personal ? PERSONAL_SCOPE : scope;
|
|
64
|
-
// Dedup (§3.4): skip exact duplicates captured before.
|
|
65
|
-
if (findDuplicates(target.key, content).exact)
|
|
66
|
-
continue;
|
|
67
|
-
const now = Date.now();
|
|
68
|
-
const rfc = msToRfc3339(now);
|
|
69
|
-
const fm = {
|
|
70
|
-
id: ulid(),
|
|
71
|
-
schema_version: 2,
|
|
72
|
-
scope_key: target.key,
|
|
73
|
-
scope: target.kind === "project" ? "project" : "personal",
|
|
74
|
-
visibility: target.kind === "project" ? "internal" : "private",
|
|
75
|
-
project_name: target.projectName,
|
|
76
|
-
type: "fact",
|
|
77
|
-
role: "knowledge",
|
|
78
|
-
importance: "normal",
|
|
79
|
-
status: "active",
|
|
80
|
-
tags: ["keyword"],
|
|
81
|
-
source: "keyword",
|
|
82
|
-
created_at: rfc,
|
|
83
|
-
updated_at: rfc,
|
|
84
|
-
supersedes: null,
|
|
85
|
-
superseded_by: null,
|
|
86
|
-
review_state: "draft",
|
|
87
|
-
proposed_by: null,
|
|
88
|
-
approved_by: null,
|
|
89
|
-
derived_from: null,
|
|
90
|
-
review_note: null,
|
|
91
|
-
review_history: [],
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
const { filePath } = writeMemoryFile(fm, content);
|
|
95
|
-
const mf = readMemoryFile(filePath);
|
|
96
|
-
if (mf)
|
|
97
|
-
upsertFromFile(mf);
|
|
98
|
-
// Capture feedback: always visible (not debug-only) — the user said
|
|
99
|
-
// "记住…", they should see that it landed. The opencode plugin API
|
|
100
|
-
// offers no toast channel, so the plugin log is the feedback surface.
|
|
101
|
-
const preview = content.length > 60 ? content.slice(0, 60) + "…" : content;
|
|
102
|
-
console.log(`[open-memex] remembered → ${target.kind} scope: "${preview}"`);
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
console.error("[open-memex] keyword capture failed:", err);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
26
|
+
captureFromText(text, state);
|
|
108
27
|
},
|
|
109
28
|
async "experimental.chat.system.transform"(input, output) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (injectedSessions.has(sid))
|
|
114
|
-
return;
|
|
115
|
-
injectedSessions.add(sid);
|
|
116
|
-
try {
|
|
117
|
-
const block = buildContextBlock(scope, cfg);
|
|
118
|
-
if (block)
|
|
119
|
-
output.system.push(block);
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
console.error("[open-memex] context injection failed:", err);
|
|
123
|
-
}
|
|
29
|
+
injectOnce(input.sessionID ?? "", state, (block) => {
|
|
30
|
+
output.system.push(block);
|
|
31
|
+
});
|
|
124
32
|
},
|
|
125
33
|
};
|
|
126
34
|
};
|
|
127
|
-
export default
|
|
35
|
+
export default { id: PLUGIN_ID, setup: v2Plugin.setup, server };
|