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/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 = 6;
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 };
@@ -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,
@@ -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
- const yml = yaml.dump(fm, { lineWidth: -1, quotingType: '"' });
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 — try a few keyword variants, including the user's own language, when the first search comes up empty.",
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);