open-memex 0.6.2 → 0.7.0-alpha.2
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 +12 -2
- package/CHANGELOG.md +24 -1
- package/README.md +21 -2
- package/README.zh-CN.md +7 -2
- package/dist/cli.js +31 -6
- package/dist/config.js +2 -0
- package/dist/distill-agents.js +1 -1
- package/dist/doctor.js +143 -32
- package/dist/index.js +20 -112
- package/dist/init.js +85 -5
- package/dist/mcp.js +12 -5
- package/dist/opencode-v2.js +43 -0
- package/dist/plugin-core.js +138 -0
- package/dist/retrieve/inject.js +1 -1
- package/dist/retrieve/search.js +11 -2
- package/dist/store/db.js +11 -9
- package/dist/store/lifecycle.js +2 -0
- package/dist/store/markdown.js +39 -1
- package/dist/store/sync.js +5 -3
- package/dist/tools/ops.js +22 -3
- package/docs/V2-DESIGN.md +94 -0
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +47 -2
- package/scripts/test-full.ts +28 -0
- package/skills/open-memex/SKILL.md +30 -1
- package/src/cli.ts +36 -5
- package/src/config.ts +10 -0
- package/src/distill-agents.ts +6 -0
- package/src/doctor.ts +140 -29
- package/src/index.ts +25 -112
- package/src/init.ts +91 -5
- package/src/mcp.ts +12 -5
- package/src/opencode-v2.ts +119 -0
- package/src/plugin-core.ts +152 -0
- package/src/retrieve/inject.ts +1 -1
- package/src/retrieve/search.ts +14 -2
- package/src/store/db.ts +11 -9
- package/src/store/lifecycle.ts +3 -0
- package/src/store/markdown.ts +45 -1
- package/src/store/sync.ts +6 -3
- package/src/tools/ops.ts +31 -2
package/src/store/db.ts
CHANGED
|
@@ -35,6 +35,7 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
35
35
|
importance TEXT NOT NULL DEFAULT 'normal',
|
|
36
36
|
status TEXT NOT NULL DEFAULT 'active',
|
|
37
37
|
tags TEXT NOT NULL DEFAULT '',
|
|
38
|
+
aliases TEXT NOT NULL DEFAULT '',
|
|
38
39
|
content TEXT NOT NULL,
|
|
39
40
|
cjk TEXT NOT NULL DEFAULT '',
|
|
40
41
|
content_hash TEXT NOT NULL DEFAULT '',
|
|
@@ -62,6 +63,7 @@ const FTS_SCHEMA = `
|
|
|
62
63
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
63
64
|
content,
|
|
64
65
|
tags,
|
|
66
|
+
aliases,
|
|
65
67
|
type,
|
|
66
68
|
cjk,
|
|
67
69
|
scope_key UNINDEXED,
|
|
@@ -71,25 +73,25 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
71
73
|
);
|
|
72
74
|
|
|
73
75
|
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
74
|
-
INSERT INTO memories_fts(rowid, content, tags, type, cjk, scope_key)
|
|
75
|
-
VALUES (new.rowid, new.content, new.tags, new.type, new.cjk, new.scope_key);
|
|
76
|
+
INSERT INTO memories_fts(rowid, content, tags, aliases, type, cjk, scope_key)
|
|
77
|
+
VALUES (new.rowid, new.content, new.tags, new.aliases, new.type, new.cjk, new.scope_key);
|
|
76
78
|
END;
|
|
77
79
|
|
|
78
80
|
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
79
|
-
INSERT INTO memories_fts(memories_fts, rowid, content, tags, type, cjk, scope_key)
|
|
80
|
-
VALUES ('delete', old.rowid, old.content, old.tags, old.type, old.cjk, old.scope_key);
|
|
81
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, tags, aliases, type, cjk, scope_key)
|
|
82
|
+
VALUES ('delete', old.rowid, old.content, old.tags, old.aliases, old.type, old.cjk, old.scope_key);
|
|
81
83
|
END;
|
|
82
84
|
|
|
83
85
|
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
84
|
-
INSERT INTO memories_fts(memories_fts, rowid, content, tags, type, cjk, scope_key)
|
|
85
|
-
VALUES ('delete', old.rowid, old.content, old.tags, old.type, old.cjk, old.scope_key);
|
|
86
|
-
INSERT INTO memories_fts(rowid, content, tags, type, cjk, scope_key)
|
|
87
|
-
VALUES (new.rowid, new.content, new.tags, new.type, new.cjk, new.scope_key);
|
|
86
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, tags, aliases, type, cjk, scope_key)
|
|
87
|
+
VALUES ('delete', old.rowid, old.content, old.tags, old.aliases, old.type, old.cjk, old.scope_key);
|
|
88
|
+
INSERT INTO memories_fts(rowid, content, tags, aliases, type, cjk, scope_key)
|
|
89
|
+
VALUES (new.rowid, new.content, new.tags, new.aliases, new.type, new.cjk, new.scope_key);
|
|
88
90
|
END;
|
|
89
91
|
`;
|
|
90
92
|
|
|
91
93
|
/** Current index schema version. Bump when TABLE_SCHEMA/FTS_SCHEMA change. */
|
|
92
|
-
const SCHEMA_VERSION =
|
|
94
|
+
const SCHEMA_VERSION = 7;
|
|
93
95
|
|
|
94
96
|
function userVersion(d: AnyDatabase): number {
|
|
95
97
|
const row = d.prepare("PRAGMA user_version").get() as { user_version: number };
|
package/src/store/lifecycle.ts
CHANGED
|
@@ -206,6 +206,7 @@ export interface SupersedeInput {
|
|
|
206
206
|
body: string;
|
|
207
207
|
type?: string;
|
|
208
208
|
tags?: string[];
|
|
209
|
+
aliases?: string[];
|
|
209
210
|
source?: string;
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -232,6 +233,8 @@ export function supersede(
|
|
|
232
233
|
id: ulid(),
|
|
233
234
|
type: input.type ?? oldMf.fm.type,
|
|
234
235
|
tags: input.tags ?? oldMf.fm.tags,
|
|
236
|
+
// D61: aliases carry forward unless the caller replaces them.
|
|
237
|
+
aliases: input.aliases ?? oldMf.fm.aliases,
|
|
235
238
|
source: input.source ?? oldMf.fm.source,
|
|
236
239
|
status: "active",
|
|
237
240
|
created_at: now,
|
package/src/store/markdown.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface Frontmatter {
|
|
|
53
53
|
importance: Importance;
|
|
54
54
|
status: MemoryStatus;
|
|
55
55
|
tags: string[];
|
|
56
|
+
/**
|
|
57
|
+
* D61: capture-time aliases — alternate phrasings / other-language
|
|
58
|
+
* equivalents of this memory, indexed alongside the body so a
|
|
59
|
+
* differently-worded question still matches (retrieval layer 3).
|
|
60
|
+
* Optional and additive: files without it parse unchanged.
|
|
61
|
+
*/
|
|
62
|
+
aliases?: string[];
|
|
56
63
|
source: string;
|
|
57
64
|
created_at: string; // RFC 3339, never bare epoch (§3)
|
|
58
65
|
updated_at: string; // RFC 3339
|
|
@@ -232,6 +239,14 @@ export function normalizeFrontmatter(
|
|
|
232
239
|
tags: Array.isArray(raw.tags)
|
|
233
240
|
? raw.tags.filter((t): t is string => typeof t === "string")
|
|
234
241
|
: [],
|
|
242
|
+
...(Array.isArray(raw.aliases)
|
|
243
|
+
? (() => {
|
|
244
|
+
const aliases = normalizeAliases(
|
|
245
|
+
raw.aliases.filter((a): a is string => typeof a === "string"),
|
|
246
|
+
);
|
|
247
|
+
return aliases.length > 0 ? { aliases } : {};
|
|
248
|
+
})()
|
|
249
|
+
: {}),
|
|
235
250
|
source: typeof raw.source === "string" ? raw.source : "",
|
|
236
251
|
created_at: msToRfc3339(timeToMs(raw.created_at)),
|
|
237
252
|
updated_at: msToRfc3339(timeToMs(raw.updated_at)),
|
|
@@ -254,12 +269,41 @@ export function normalizeFrontmatter(
|
|
|
254
269
|
};
|
|
255
270
|
}
|
|
256
271
|
|
|
272
|
+
/**
|
|
273
|
+
* D61: normalize capture-time aliases — trim, drop empties, dedupe
|
|
274
|
+
* (case-insensitive), cap at 4. Bad aliases are noise, never an error.
|
|
275
|
+
* Shared by the tool ops, the CLI, and frontmatter parsing.
|
|
276
|
+
*/
|
|
277
|
+
export function normalizeAliases(input?: string[]): string[] {
|
|
278
|
+
if (!input) return [];
|
|
279
|
+
const seen = new Set<string>();
|
|
280
|
+
const out: string[] = [];
|
|
281
|
+
for (const a of input) {
|
|
282
|
+
const t = a.trim();
|
|
283
|
+
if (!t) continue;
|
|
284
|
+
const k = t.toLowerCase();
|
|
285
|
+
if (seen.has(k)) continue;
|
|
286
|
+
seen.add(k);
|
|
287
|
+
out.push(t);
|
|
288
|
+
if (out.length >= 4) break;
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
257
293
|
export function isTaxonomyType(t: string): boolean {
|
|
258
294
|
return TAXONOMY.has(t);
|
|
259
295
|
}
|
|
260
296
|
|
|
261
297
|
export function serialize(fm: Frontmatter, body: string): string {
|
|
262
|
-
|
|
298
|
+
// D61: an empty aliases list is noise in the file — drop the key entirely
|
|
299
|
+
// rather than relying on the YAML dumper to skip an undefined value
|
|
300
|
+
// (parse only sets the field when non-empty; keep both ends consistent).
|
|
301
|
+
let out: Frontmatter = fm;
|
|
302
|
+
if (fm.aliases && fm.aliases.length === 0) {
|
|
303
|
+
out = { ...fm };
|
|
304
|
+
delete out.aliases;
|
|
305
|
+
}
|
|
306
|
+
const yml = yaml.dump(out, { lineWidth: -1, quotingType: '"' });
|
|
263
307
|
return `---\n${yml}---\n\n${body.trimEnd()}\n`;
|
|
264
308
|
}
|
|
265
309
|
|
package/src/store/sync.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { projectRoot, paths } from "../paths.ts";
|
|
|
14
14
|
import { loadConfig } from "../config.ts";
|
|
15
15
|
|
|
16
16
|
const UPSERT_SQL = `
|
|
17
|
-
INSERT INTO memories (id, scope_key, scope, visibility, project_name, type, role, importance, status, tags, content, cjk, content_hash, superseded_by, source, file_path, mtime_ms, created_at, updated_at, review_state)
|
|
18
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
17
|
+
INSERT INTO memories (id, scope_key, scope, visibility, project_name, type, role, importance, status, tags, aliases, content, cjk, content_hash, superseded_by, source, file_path, mtime_ms, created_at, updated_at, review_state)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
19
19
|
ON CONFLICT(id) DO UPDATE SET
|
|
20
20
|
scope_key = excluded.scope_key,
|
|
21
21
|
scope = excluded.scope,
|
|
@@ -26,6 +26,7 @@ const UPSERT_SQL = `
|
|
|
26
26
|
importance = excluded.importance,
|
|
27
27
|
status = excluded.status,
|
|
28
28
|
tags = excluded.tags,
|
|
29
|
+
aliases = excluded.aliases,
|
|
29
30
|
content = excluded.content,
|
|
30
31
|
cjk = excluded.cjk,
|
|
31
32
|
content_hash = excluded.content_hash,
|
|
@@ -62,6 +63,7 @@ export function upsertFromFile(mf: MemoryFile): void {
|
|
|
62
63
|
function writeRow(mf: MemoryFile, mtimeMs: number): void {
|
|
63
64
|
const { fm, body, filePath } = mf;
|
|
64
65
|
const tags = (fm.tags ?? []).join(",");
|
|
66
|
+
const aliases = (fm.aliases ?? []).join(",");
|
|
65
67
|
db().prepare(UPSERT_SQL).run(
|
|
66
68
|
fm.id,
|
|
67
69
|
fm.scope_key,
|
|
@@ -73,8 +75,9 @@ function writeRow(mf: MemoryFile, mtimeMs: number): void {
|
|
|
73
75
|
fm.importance,
|
|
74
76
|
fm.status,
|
|
75
77
|
tags,
|
|
78
|
+
aliases,
|
|
76
79
|
body,
|
|
77
|
-
cjkIndexText(body + "\n" + tags),
|
|
80
|
+
cjkIndexText(body + "\n" + tags + "\n" + aliases),
|
|
78
81
|
contentHash(body),
|
|
79
82
|
fm.superseded_by ?? null,
|
|
80
83
|
fm.source ?? "",
|
package/src/tools/ops.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
ulid,
|
|
19
19
|
msToRfc3339,
|
|
20
20
|
MEMORY_TYPE_TAXONOMY,
|
|
21
|
+
normalizeAliases,
|
|
21
22
|
type Frontmatter,
|
|
22
23
|
} from "../store/markdown.ts";
|
|
23
24
|
import { upsertFromFile, deleteFromIndex } from "../store/sync.ts";
|
|
@@ -62,9 +63,9 @@ export async function withOutboxNote(
|
|
|
62
63
|
/** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
|
|
63
64
|
export const TOOL_DESCRIPTIONS = {
|
|
64
65
|
memory_add:
|
|
65
|
-
"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.",
|
|
66
|
+
"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. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
|
|
66
67
|
memory_search:
|
|
67
|
-
"Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before
|
|
68
|
+
"Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
|
|
68
69
|
memory_list:
|
|
69
70
|
"List memories in a scope, newest first. Useful for browsing what is remembered, or verifying that a save landed.",
|
|
70
71
|
memory_supersede:
|
|
@@ -92,6 +93,13 @@ export const scopeArg = z
|
|
|
92
93
|
"Memory scope. `project` = tied to this repo. `personal` = global across all your projects. `user` is a deprecated alias of `personal`. Default: project.",
|
|
93
94
|
);
|
|
94
95
|
|
|
96
|
+
export const aliasesArg = z
|
|
97
|
+
.array(z.string())
|
|
98
|
+
.optional()
|
|
99
|
+
.describe(
|
|
100
|
+
"Optional: alternate phrasings of this fact — synonyms, another way a question might be worded, equivalents in the user's other language (e.g. 节假日 for 'public holidays'). They are indexed with the memory so differently-worded questions still match. Only used when the install has capture aliases enabled (init default). Send at most 4; extras and blanks are dropped silently, never an error.",
|
|
101
|
+
);
|
|
102
|
+
|
|
95
103
|
export const memoryAddArgs = {
|
|
96
104
|
content: z.string().min(1).describe("The fact to remember. One idea per memory."),
|
|
97
105
|
type: z
|
|
@@ -100,6 +108,7 @@ export const memoryAddArgs = {
|
|
|
100
108
|
.describe("Category of memory. Default: fact."),
|
|
101
109
|
scope: scopeArg,
|
|
102
110
|
tags: z.array(z.string()).optional().describe("Optional tags for filtering."),
|
|
111
|
+
aliases: aliasesArg,
|
|
103
112
|
source: z
|
|
104
113
|
.string()
|
|
105
114
|
.optional()
|
|
@@ -138,6 +147,7 @@ export const memorySupersedeArgs = {
|
|
|
138
147
|
.optional()
|
|
139
148
|
.describe("Category of the new memory. Defaults to the old memory's type."),
|
|
140
149
|
tags: z.array(z.string()).optional().describe("Optional tags for the new memory."),
|
|
150
|
+
aliases: aliasesArg,
|
|
141
151
|
};
|
|
142
152
|
export type MemorySupersedeArgs = z.infer<z.ZodObject<typeof memorySupersedeArgs>>;
|
|
143
153
|
|
|
@@ -210,6 +220,7 @@ function buildFrontmatter(
|
|
|
210
220
|
body: {
|
|
211
221
|
type: Frontmatter["type"];
|
|
212
222
|
tags: string[];
|
|
223
|
+
aliases?: string[];
|
|
213
224
|
source: Frontmatter["source"];
|
|
214
225
|
},
|
|
215
226
|
): Frontmatter {
|
|
@@ -226,6 +237,7 @@ function buildFrontmatter(
|
|
|
226
237
|
importance: "normal",
|
|
227
238
|
status: "active",
|
|
228
239
|
tags: body.tags,
|
|
240
|
+
...(body.aliases && body.aliases.length > 0 ? { aliases: body.aliases } : {}),
|
|
229
241
|
source: body.source,
|
|
230
242
|
created_at: rfc,
|
|
231
243
|
updated_at: rfc,
|
|
@@ -265,6 +277,15 @@ export async function addMemory(
|
|
|
265
277
|
const fm = buildFrontmatter(s, {
|
|
266
278
|
type: args.type ?? "fact",
|
|
267
279
|
tags: args.tags ?? [],
|
|
280
|
+
// D61: aliases are stored only when the install enables them (init
|
|
281
|
+
// asks once, default on); normalize caps at 4 and drops junk silently.
|
|
282
|
+
// Aliases pass through the same redaction as content (write-path
|
|
283
|
+
// invariant) — an alias carrying a token gets masked, not stored raw.
|
|
284
|
+
aliases: cfg.captureAliases
|
|
285
|
+
? normalizeAliases(
|
|
286
|
+
(args.aliases ?? []).map((a) => redact(a, cfg.redactPatterns).content),
|
|
287
|
+
)
|
|
288
|
+
: [],
|
|
268
289
|
source: args.source ?? "tool",
|
|
269
290
|
});
|
|
270
291
|
const { filePath } = writeMemoryFile(fm, redacted);
|
|
@@ -335,6 +356,14 @@ export async function supersedeMemory(
|
|
|
335
356
|
body: redacted,
|
|
336
357
|
type: args.type,
|
|
337
358
|
tags: args.tags,
|
|
359
|
+
// D61: explicit aliases replace the carried-over ones, only when
|
|
360
|
+
// the install enables capture aliases. Same redaction as content.
|
|
361
|
+
aliases:
|
|
362
|
+
cfg.captureAliases && args.aliases !== undefined
|
|
363
|
+
? normalizeAliases(
|
|
364
|
+
args.aliases.map((a) => redact(a, cfg.redactPatterns).content),
|
|
365
|
+
)
|
|
366
|
+
: undefined,
|
|
338
367
|
source: "tool",
|
|
339
368
|
});
|
|
340
369
|
upsertFromFile(oldMf);
|