open-memex 0.7.0-alpha.1 → 0.7.0-alpha.3
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 -2
- package/CHANGELOG.md +9 -0
- package/README.md +16 -3
- package/README.zh-CN.md +5 -2
- package/dist/cli.js +31 -6
- package/dist/config.js +9 -2
- package/dist/distill-agents.js +1 -1
- package/dist/doctor.js +12 -4
- package/dist/init.js +5 -0
- package/dist/mcp.js +12 -5
- package/dist/paths.js +16 -2
- 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 +68 -3
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +59 -1
- 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 +16 -1
- package/src/distill-agents.ts +6 -0
- package/src/doctor.ts +13 -4
- package/src/init.ts +8 -0
- package/src/mcp.ts +12 -5
- package/src/paths.ts +16 -1
- 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/AGENTS.md
CHANGED
|
@@ -102,7 +102,7 @@ The published `open-memex` bin points at `dist/cli.js` (compiled at publish time
|
|
|
102
102
|
From a source checkout, `npm run cli` / `npm run mcp` still run `src/` directly
|
|
103
103
|
with type-stripping — no build step needed for development.
|
|
104
104
|
|
|
105
|
-
There is **no `npm test`** and no CI. Verification loop is: `npm run typecheck` + `smoke-pure.ts` + (if touching sqlite) `npm run cli -- reindex` against a scratch `
|
|
105
|
+
There is **no `npm test`** and no CI. Verification loop is: `npm run typecheck` + `smoke-pure.ts` + (if touching sqlite) `npm run cli -- reindex` against a scratch `OPEN_MEMEX_HOME`.
|
|
106
106
|
|
|
107
107
|
To load the plugin in opencode locally, `~/.config/opencode/opencode.jsonc` must have:
|
|
108
108
|
```
|
|
@@ -115,7 +115,7 @@ Restart opencode after any change — the plugin is not hot-reloaded.
|
|
|
115
115
|
Storage root:
|
|
116
116
|
- Windows: `%APPDATA%\open-memex\`
|
|
117
117
|
- Linux/macOS: `$XDG_DATA_HOME/open-memex/` (fallback `~/.local/share/open-memex/`)
|
|
118
|
-
- Override with `
|
|
118
|
+
- Override with `OPEN_MEMEX_HOME` (use this for tests to avoid clobbering real data). The pre-rename `MY_O_MEMORY_HOME` is still honored as a fallback (D62).
|
|
119
119
|
|
|
120
120
|
Layout: `memories/<scope_key>/<id>.md` (YAML frontmatter + body) + `index.db` (SQLite FTS5).
|
|
121
121
|
|
|
@@ -137,6 +137,15 @@ Every write path (tool, keyword hook, CLI `add`) must:
|
|
|
137
137
|
3. Check `findDuplicates` (design §3.4): identical content is idempotent (return existing id); near-duplicates (similarity ≥ 0.8) warn but save — suggest `supersede` when the new content replaces the old.
|
|
138
138
|
4. `writeMemoryFile` first, then `readMemoryFile` + `upsertFromFile` to keep FTS in sync.
|
|
139
139
|
|
|
140
|
+
Capture-time aliases (D61): `memory_add`/`supersede` may carry up to 4
|
|
141
|
+
`aliases` (alternate phrasings) into frontmatter; they index in their own FTS
|
|
142
|
+
column. Only when `cfg.captureAliases` is on (init asks once, default on) —
|
|
143
|
+
when off, passed aliases are dropped. Always pass them through
|
|
144
|
+
`normalizeAliases`; the keyword-capture path (no agent in the loop) never
|
|
145
|
+
invents aliases. Checkpoint-proposal guidance (propose, never auto-draft) is
|
|
146
|
+
copy, not mechanism: keep the rubric wording in sync across the skill, the
|
|
147
|
+
AGENTS.md hygiene footer, the MCP handshake, and the tool descriptions.
|
|
148
|
+
|
|
140
149
|
## Lifecycle invariants (design §3.3)
|
|
141
150
|
|
|
142
151
|
- Statuses: `active → superseded | deprecated | retracted | archived`. Retrieval excludes `retracted`/`archived`, ranks `active` above `deprecated`.
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,15 @@ the `@alpha` tag are development snapshots and are not listed individually.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## Environment variable names (D62)
|
|
13
|
+
|
|
14
|
+
- The override variables now answer to `OPEN_MEMEX_HOME` (storage root) and `OPEN_MEMEX_CONFIG` (config path). The pre-rename `MY_O_MEMORY_HOME` / `MY_O_MEMORY_CONFIG` names keep working as fallbacks — nothing to migrate, the data root itself does not move. `doctor` notes when the storage root came from the legacy variable.
|
|
15
|
+
|
|
16
|
+
## Proactive capture: checkpoint proposals and aliases (D61)
|
|
17
|
+
|
|
18
|
+
- At task checkpoints the agent now proposes instead of waiting to be asked: when a task wraps up, it offers 1–3 short candidate memories distilled from the session (decisions and their reasons, conventions, gotchas, approaches tried and abandoned) and saves only the ones you approve — approved inferences are marked `source: "inference"`. One memorability rubric and one search discipline (break the question into concepts, try several phrasings, check both scopes) are written into every guidance surface — the bundled skill, the AGENTS.md memory-hygiene footer, the MCP handshake, and the tool descriptions — so behavior is consistent across editors. Nothing is auto-drafted in the background; that posture waits on memory visibility.
|
|
19
|
+
- Memories can carry up to 4 **aliases** — alternate phrasings, synonyms, other-language equivalents — stored in the memory file and indexed with it, so a differently-worded question still matches ("vacation days" finds the holiday policy; "节假日" too). `init` asks once (default on); `open-memex config set captureAliases false` turns it off. Existing memory files are untouched — the index rebuilds itself on first use.
|
|
20
|
+
|
|
12
21
|
## One plugin, both opencode generations (D60)
|
|
13
22
|
|
|
14
23
|
- The native plugin now loads on **opencode 2** as well as opencode 1. `open-memex init --client opencode --global` writes both config spellings — the v1 `"plugin"` file entry and the v2 `"plugins"` directory entry — and each host reads its own. Previously an opencode 2 host silently skipped the plugin (a file entry is not a directory it can load), leaving no tools and no explanation.
|
package/README.md
CHANGED
|
@@ -373,7 +373,19 @@ Three ways memories get in:
|
|
|
373
373
|
(or it saves on its own when you state a fact worth keeping) — it calls
|
|
374
374
|
`memory_add`. The routing above is a heuristic; you can always say "save this
|
|
375
375
|
to my personal memory" or use the CLI with `--scope` to be explicit.
|
|
376
|
-
- **
|
|
376
|
+
- **Checkpoint proposals**: when a task wraps up, the agent proposes 1–3 short
|
|
377
|
+
candidate memories distilled from the session — decisions and their reasons,
|
|
378
|
+
conventions, gotchas, approaches tried and abandoned — and saves only the
|
|
379
|
+
ones you approve. Nothing is written silently: no draft is created behind
|
|
380
|
+
your back.
|
|
381
|
+
- **The CLI**: `open-memex add "…"` with optional `--scope` / `--tags` / `--type`
|
|
382
|
+
/ `--aliases`.
|
|
383
|
+
|
|
384
|
+
**Aliases.** Each saved memory can carry up to 4 alternate phrasings
|
|
385
|
+
(synonyms, another language's equivalent) that are indexed with it, so a
|
|
386
|
+
question worded differently still finds the memory — "vacation days" finds the
|
|
387
|
+
holiday policy. `init` asks once whether to enable this (default on); turn it
|
|
388
|
+
off any time with `open-memex config set captureAliases false`.
|
|
377
389
|
|
|
378
390
|
**Redaction.** Wrap anything sensitive in `<private>…</private>` and it is
|
|
379
391
|
stripped before saving. Recognized secrets (API keys, tokens, high-entropy
|
|
@@ -566,8 +578,9 @@ branches and PRs like any other file.
|
|
|
566
578
|
|
|
567
579
|
Settings live in `~/.config/opencode/open-memex.jsonc` — the `opencode` in the
|
|
568
580
|
path is historical; this one file is shared by every client. Override the config
|
|
569
|
-
path with `
|
|
570
|
-
(
|
|
581
|
+
path with `OPEN_MEMEX_CONFIG` and the storage root with `OPEN_MEMEX_HOME`
|
|
582
|
+
(the pre-rename `MY_O_MEMORY_CONFIG` / `MY_O_MEMORY_HOME` names are still
|
|
583
|
+
honored as fallbacks).
|
|
571
584
|
|
|
572
585
|
Defaults:
|
|
573
586
|
|
package/README.zh-CN.md
CHANGED
|
@@ -254,7 +254,10 @@ open-memex uninstall --yes
|
|
|
254
254
|
|
|
255
255
|
- **关键词触发**(仅 opencode 原生插件):说 `remember …`、`note that …`、`don't forget …`、`TIL …`、`save this …`——中文 `记住…` / `记一下` / `记录一下` / `别忘了…`——这句话就被存下来,不需要任何工具调用。捕获默认进当前 **project**;`remember for me …`、`记住我…`、`替我记…`、`我觉得…`、`我喜欢…` 这类"这是关于我"的信号会改为进 **personal**;`我们决定…`、`帮我们记住…` 等团队语境仍留在项目里。
|
|
256
256
|
- **助手主动保存**:在任何编辑器里,你让助手记住某件事(或者你陈述了一个值得记的事实、它自己判断该保存),它会调用 `memory_add`。上面的路由是启发式规则;你可以明说"存到我的个人记忆",或用 CLI 加 `--scope` 明确指定。
|
|
257
|
-
-
|
|
257
|
+
- **检查点提议**:一个任务收尾时,助手会提议 1–3 条从这轮工作里提炼的候选记忆——决定及其理由、约定、踩过的坑、走过又放弃的路——只有你点头的那几条才存。不会有任何东西在你不知情时被写下来。
|
|
258
|
+
- **CLI**:`open-memex add "…"`,可选 `--scope` / `--tags` / `--type` / `--aliases`。
|
|
259
|
+
|
|
260
|
+
**别名。** 每条记忆可以带最多 4 个换一种说法(同义词、另一种语言的对应词),和记忆一起进索引,这样问法不同也能找到——比如搜 "vacation days" 能找到假期政策那条。`init` 时会问一次是否开启(默认开);随时可用 `open-memex config set captureAliases false` 关掉。
|
|
258
261
|
|
|
259
262
|
**脱敏。** 把敏感内容包在 `<private>…</private>` 里,它在保存前会被整段剥离。识别出的密钥(API key、token、高熵凭证)会被就地掩码——保留前 4 个字符(让你能认出是哪个 key),其余替换——记忆仍会保存。随时安全地预览一句话会被捕获成什么样:
|
|
260
263
|
|
|
@@ -388,7 +391,7 @@ npm install -g open-memex@latest # 或 @alpha
|
|
|
388
391
|
|
|
389
392
|
## 配置
|
|
390
393
|
|
|
391
|
-
设置放在 `~/.config/opencode/open-memex.jsonc`——路径里的 `opencode` 是历史名;这一个文件由所有客户端共享。用 `
|
|
394
|
+
设置放在 `~/.config/opencode/open-memex.jsonc`——路径里的 `opencode` 是历史名;这一个文件由所有客户端共享。用 `OPEN_MEMEX_CONFIG` 覆盖配置路径,用 `OPEN_MEMEX_HOME` 覆盖存储根目录(改名之前的旧变量名 `MY_O_MEMORY_CONFIG` / `MY_O_MEMORY_HOME` 仍然有效,作为回退)。
|
|
392
395
|
|
|
393
396
|
默认值:
|
|
394
397
|
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { proposeMemories, promoteMemory, listConflicts, resolveConflict, formatR
|
|
|
10
10
|
import { getSyncStatus, formatSyncStatus, submitMemories } from "./submit.js";
|
|
11
11
|
import { getPrStatus, formatPrStatus, applyPrStatus } from "./github.js";
|
|
12
12
|
import { search, list, hitStateLabel } from "./retrieve/search.js";
|
|
13
|
-
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, } from "./store/markdown.js";
|
|
13
|
+
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, normalizeAliases, } from "./store/markdown.js";
|
|
14
14
|
import { loadConfig } from "./config.js";
|
|
15
15
|
import { paths, projectRoot } from "./paths.js";
|
|
16
16
|
import { redact } from "./redact.js";
|
|
@@ -54,18 +54,25 @@ Example:
|
|
|
54
54
|
open-memex search "deploy checklist" --scope both`,
|
|
55
55
|
add: `Save a fact, preference, decision, or note to local memory.
|
|
56
56
|
|
|
57
|
-
Usage: open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2]
|
|
57
|
+
Usage: open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2] [--aliases "a;b"]
|
|
58
58
|
|
|
59
59
|
Flags:
|
|
60
|
-
--scope
|
|
61
|
-
--type
|
|
62
|
-
--tag
|
|
60
|
+
--scope project (default) or personal (personal never leaves this machine)
|
|
61
|
+
--type memory type (default: fact)
|
|
62
|
+
--tag comma-separated tags
|
|
63
|
+
--aliases alternate phrasings of this fact (up to 4), indexed with the
|
|
64
|
+
memory so differently-worded searches still match; separate
|
|
65
|
+
with ; (commas also accepted — use ; when an alias itself
|
|
66
|
+
contains one). Stored only when capture aliases are enabled
|
|
67
|
+
(init default)
|
|
63
68
|
|
|
64
69
|
Example:
|
|
65
70
|
open-memex add "We deploy on Fridays" --scope project --tag process`,
|
|
66
71
|
supersede: `Replace a memory with a newer version. The old one is kept as history.
|
|
67
72
|
|
|
68
|
-
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
73
|
+
Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2] [--aliases "a;b"]
|
|
74
|
+
|
|
75
|
+
Without --aliases, the old memory's aliases carry over.
|
|
69
76
|
|
|
70
77
|
Example:
|
|
71
78
|
open-memex supersede 01ABC "We deploy on Thursdays now"`,
|
|
@@ -849,6 +856,18 @@ async function main() {
|
|
|
849
856
|
.map((t) => t.trim())
|
|
850
857
|
.filter(Boolean)
|
|
851
858
|
: [],
|
|
859
|
+
// D61: --aliases "phrase one; phrase two" (comma also accepted; use
|
|
860
|
+
// ; when an alias itself contains a comma). A bare --aliases with no
|
|
861
|
+
// value makes parseFlags yield "true" — treat that as absent. Stored
|
|
862
|
+
// only when the install enables capture aliases, redacted like content.
|
|
863
|
+
...(typeof flags.aliases === "string" && flags.aliases !== "true" && cfg.captureAliases
|
|
864
|
+
? {
|
|
865
|
+
aliases: normalizeAliases(flags.aliases
|
|
866
|
+
.split(/[;,]/)
|
|
867
|
+
.map((a) => redact(a.trim(), cfg.redactPatterns).content)
|
|
868
|
+
.filter(Boolean)),
|
|
869
|
+
}
|
|
870
|
+
: {}),
|
|
852
871
|
source: "cli",
|
|
853
872
|
created_at: rfc,
|
|
854
873
|
updated_at: rfc,
|
|
@@ -888,6 +907,12 @@ async function main() {
|
|
|
888
907
|
tags: flags.tag
|
|
889
908
|
? flags.tag.split(",").map((t) => t.trim()).filter(Boolean)
|
|
890
909
|
: undefined,
|
|
910
|
+
aliases: typeof flags.aliases === "string" && flags.aliases !== "true" && cfg.captureAliases
|
|
911
|
+
? normalizeAliases(flags.aliases
|
|
912
|
+
.split(/[;,]/)
|
|
913
|
+
.map((a) => redact(a.trim(), cfg.redactPatterns).content)
|
|
914
|
+
.filter(Boolean))
|
|
915
|
+
: undefined,
|
|
891
916
|
source: "cli",
|
|
892
917
|
});
|
|
893
918
|
upsertFromFile(oldMf);
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,7 @@ export const DEFAULT_CONFIG = {
|
|
|
6
6
|
maxProfileItems: 5,
|
|
7
7
|
injectOnFirstTurn: true,
|
|
8
8
|
keywordCaptureEnabled: true,
|
|
9
|
+
captureAliases: true,
|
|
9
10
|
keywordPatterns: [
|
|
10
11
|
"^\\s*remember(?!\\s+for\\s+me)(?:\\s+that)?[:,]?\\s+(.+)$",
|
|
11
12
|
"^\\s*(?:please\\s+)?(?:note|don'?t\\s+forget)(?:\\s+that)?[:,]?\\s+(.+)$",
|
|
@@ -45,9 +46,13 @@ function stripJsonComments(raw) {
|
|
|
45
46
|
out = out.replace(/,(\s*[}\]])/g, "$1");
|
|
46
47
|
return out;
|
|
47
48
|
}
|
|
48
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Where `config set` / interactive `init` persist. Respects OPEN_MEMEX_CONFIG
|
|
51
|
+
* (D62); MY_O_MEMORY_CONFIG is the pre-rename fallback, still honored.
|
|
52
|
+
*/
|
|
49
53
|
export function configFilePath() {
|
|
50
|
-
return (process.env.
|
|
54
|
+
return (process.env.OPEN_MEMEX_CONFIG ??
|
|
55
|
+
process.env.MY_O_MEMORY_CONFIG ??
|
|
51
56
|
path.join(os.homedir(), ".config", "opencode", "open-memex.jsonc"));
|
|
52
57
|
}
|
|
53
58
|
function toBool(v) {
|
|
@@ -79,6 +84,7 @@ export const SETTABLE_KEYS = {
|
|
|
79
84
|
maxProfileItems: toNonNegInt,
|
|
80
85
|
injectOnFirstTurn: toBool,
|
|
81
86
|
keywordCaptureEnabled: toBool,
|
|
87
|
+
captureAliases: toBool,
|
|
82
88
|
memoryDir: toRelativeDir,
|
|
83
89
|
"sync.autoPull": toBool,
|
|
84
90
|
logLevel: (v) => {
|
|
@@ -109,6 +115,7 @@ export function saveConfig(patch) {
|
|
|
109
115
|
/** Path of the config file in effect, or null when using built-in defaults. */
|
|
110
116
|
export function configSource() {
|
|
111
117
|
const candidates = [
|
|
118
|
+
process.env.OPEN_MEMEX_CONFIG,
|
|
112
119
|
process.env.MY_O_MEMORY_CONFIG,
|
|
113
120
|
path.join(os.homedir(), ".config", "opencode", "open-memex.jsonc"),
|
|
114
121
|
path.join(os.homedir(), ".config", "opencode", "open-memex.json"),
|
package/dist/distill-agents.js
CHANGED
|
@@ -57,7 +57,7 @@ export function distillAgentsMarkdown(opts) {
|
|
|
57
57
|
// (D53: the checkpoint mechanism is gone).
|
|
58
58
|
lines.push(`### Memory hygiene (open-memex)`);
|
|
59
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
|
+
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.`, ` Worth keeping: decisions and their reasons, preferences, conventions,`, ` gotchas, approaches tried and abandoned. Not worth keeping: one-off task`, ` details or anything re-derivable from the code.`, `- If the knowledge already lives in project docs, save a \`reference\` memory`, ` pointing at the doc instead of copying it.`, `- When saving via \`memory_add\`, attach 2–4 aliases (alternate phrasings,`, ` synonyms, equivalents in the user's other language) when the install has`, ` capture aliases enabled — they make reworded questions find the memory.`);
|
|
61
61
|
lines.push(``);
|
|
62
62
|
return lines.join("\n");
|
|
63
63
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import { spawn, execFileSync } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { loadConfig, configSource, DEFAULT_CONFIG } from "./config.js";
|
|
11
|
-
import { paths } from "./paths.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";
|
|
@@ -54,7 +54,13 @@ function storageCheck() {
|
|
|
54
54
|
try {
|
|
55
55
|
const p = paths();
|
|
56
56
|
fs.accessSync(p.memories, fs.constants.W_OK);
|
|
57
|
-
|
|
57
|
+
// D62: when the root came from the legacy env var, say so (still ok —
|
|
58
|
+
// the fallback is supported, doctor just names the current one).
|
|
59
|
+
const via = homeOverride()?.via;
|
|
60
|
+
const note = via === "MY_O_MEMORY_HOME"
|
|
61
|
+
? " (via legacy MY_O_MEMORY_HOME — OPEN_MEMEX_HOME is the current name)"
|
|
62
|
+
: "";
|
|
63
|
+
return { name: "storage", ok: true, detail: `${p.root} (writable)${note}` };
|
|
58
64
|
}
|
|
59
65
|
catch (err) {
|
|
60
66
|
return { name: "storage", ok: false, detail: String(err) };
|
|
@@ -559,9 +565,11 @@ export async function runDoctor() {
|
|
|
559
565
|
function legacyCheck() {
|
|
560
566
|
const name = "legacy my-o-memory";
|
|
561
567
|
const found = [];
|
|
562
|
-
|
|
568
|
+
// D62: the override may arrive via either env var name; check the value
|
|
569
|
+
// in effect, not just the legacy one.
|
|
570
|
+
const envHome = homeOverride()?.dir;
|
|
563
571
|
if (envHome && envHome.includes(LEGACY_PACKAGE_NAME))
|
|
564
|
-
found.push(`
|
|
572
|
+
found.push(`memory home points at a pre-rename dir (${envHome}) — unset it, then merge old data with \`open-memex migrate --to-v2\``);
|
|
565
573
|
const root = paths().root;
|
|
566
574
|
const legacyDir = path.join(path.dirname(root), LEGACY_PACKAGE_NAME);
|
|
567
575
|
if (fs.existsSync(legacyDir) && fs.statSync(legacyDir).isDirectory())
|
package/dist/init.js
CHANGED
|
@@ -805,6 +805,11 @@ export async function initProject(opts) {
|
|
|
805
805
|
const injectOnFirstTurn = await askBool("Inject relevant memories when a session starts?", DEFAULT_CONFIG.injectOnFirstTurn);
|
|
806
806
|
if (injectOnFirstTurn !== DEFAULT_CONFIG.injectOnFirstTurn)
|
|
807
807
|
patch.injectOnFirstTurn = injectOnFirstTurn;
|
|
808
|
+
// D61: capture-time aliases — agents attach alternate phrasings to each
|
|
809
|
+
// memory so differently-worded questions still find it.
|
|
810
|
+
const captureAliases = await askBool("Attach alternate phrasings (aliases) to memories so reworded questions still find them?", DEFAULT_CONFIG.captureAliases);
|
|
811
|
+
if (captureAliases !== DEFAULT_CONFIG.captureAliases)
|
|
812
|
+
patch.captureAliases = captureAliases;
|
|
808
813
|
if (Object.keys(patch).length > 0) {
|
|
809
814
|
const file = saveConfig(patch);
|
|
810
815
|
console.log(` + settings saved to ${file} (change later with \`open-memex config set <key> <value>\`)`);
|
package/dist/mcp.js
CHANGED
|
@@ -85,9 +85,10 @@ memory_resolve, memory_pr_status — always call them by these full names.
|
|
|
85
85
|
docs, save it as type "reference" pointing at the doc instead of copying it.
|
|
86
86
|
Long-form notes are fine ONLY when the user explicitly asks to save one.
|
|
87
87
|
- Before asking the user about past decisions, conventions, or preferences they
|
|
88
|
-
may have told you before, call memory_search first —
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
may have told you before, call memory_search first — search well: break the
|
|
89
|
+
question into its concepts and try 2–3 phrasings per concept (synonyms, the
|
|
90
|
+
user's other language, shorter keyword forms), and check the other scope too,
|
|
91
|
+
before concluding nothing is stored.
|
|
91
92
|
- Memories default to this project's scope; use the personal scope for facts
|
|
92
93
|
about the user that hold across all projects. When a saved fact becomes
|
|
93
94
|
outdated, call memory_supersede (find the old memory's id with memory_search
|
|
@@ -141,9 +142,15 @@ export async function runMcpServer() {
|
|
|
141
142
|
// D53: session-start outbox state, pushed. The stdio server starts fresh per
|
|
142
143
|
// session, so construction-time state ≈ session-start state.
|
|
143
144
|
const n = outboxDraftCount(scope.key);
|
|
144
|
-
|
|
145
|
+
// D61: the alias guidance rides the handshake only when the install
|
|
146
|
+
// enables capture aliases — otherwise agents would attach aliases the
|
|
147
|
+
// server then drops.
|
|
148
|
+
const aliasGuidance = cfg.captureAliases
|
|
149
|
+
? "\n- When you save a memory, attach 2–4 aliases via memory_add's aliases parameter: alternate phrasings, synonyms, equivalents in the user's other language. They are indexed with the memory so a differently-worded question still finds it."
|
|
150
|
+
: "";
|
|
151
|
+
const instructions = (n > 0
|
|
145
152
|
? `${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;
|
|
153
|
+
: SERVER_INSTRUCTIONS) + aliasGuidance;
|
|
147
154
|
const server = new McpServer({ name: "open-memex", version: SERVER_VERSION }, { instructions });
|
|
148
155
|
server.registerTool("memory_add", {
|
|
149
156
|
description: TOOL_DESCRIPTIONS.memory_add,
|
package/dist/paths.js
CHANGED
|
@@ -2,9 +2,23 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* D62: the data-root override env var. `OPEN_MEMEX_HOME` is the current
|
|
7
|
+
* name; `MY_O_MEMORY_HOME` (pre-rename package name) is still honored as a
|
|
8
|
+
* fallback so existing setups and scripts keep working. New name wins
|
|
9
|
+
* when both are set.
|
|
10
|
+
*/
|
|
11
|
+
export function homeOverride() {
|
|
12
|
+
if (process.env.OPEN_MEMEX_HOME)
|
|
13
|
+
return { dir: path.resolve(process.env.OPEN_MEMEX_HOME), via: "OPEN_MEMEX_HOME" };
|
|
6
14
|
if (process.env.MY_O_MEMORY_HOME)
|
|
7
|
-
return path.resolve(process.env.MY_O_MEMORY_HOME);
|
|
15
|
+
return { dir: path.resolve(process.env.MY_O_MEMORY_HOME), via: "MY_O_MEMORY_HOME" };
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function dataRoot() {
|
|
19
|
+
const override = homeOverride();
|
|
20
|
+
if (override)
|
|
21
|
+
return override.dir;
|
|
8
22
|
if (process.platform === "win32") {
|
|
9
23
|
const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
|
|
10
24
|
return path.join(appData, "open-memex");
|
package/dist/retrieve/inject.js
CHANGED
|
@@ -20,6 +20,6 @@ export function buildContextBlock(scope, cfg) {
|
|
|
20
20
|
for (const m of project)
|
|
21
21
|
lines.push(`- [${m.type}]${hitStateLabel(m)} ${oneLine(m.snippet)}`);
|
|
22
22
|
}
|
|
23
|
-
lines.push("", "Use the `memory_search` tool to look up more. Use `memory_add` to save new facts. Do not mention this block to the user unless asked.");
|
|
23
|
+
lines.push("", "Use the `memory_search` tool to look up more. Use `memory_add` to save new facts. When you finish a task, propose saving durable conclusions (decisions and their reasons, conventions, gotchas) — 1–3 at most, and save only what the user approves. Do not mention this block to the user unless asked.");
|
|
24
24
|
return lines.join("\n");
|
|
25
25
|
}
|
package/dist/retrieve/search.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { db } from "../store/db.js";
|
|
2
2
|
import { toFtsQuery } from "./query.js";
|
|
3
3
|
function toHit(r) {
|
|
4
|
+
const aliases = r.aliases ? r.aliases.split(",").map((a) => a.trim()).filter(Boolean) : [];
|
|
5
|
+
let snippet = r.snippet ?? "";
|
|
6
|
+
// D61: the snippet highlights matches in the body column only. When the
|
|
7
|
+
// match came through an alias (body shows no highlight), say so — the
|
|
8
|
+
// hit otherwise looks unrelated to the query.
|
|
9
|
+
if (aliases.length > 0 && !/\[[^\]]+\]/.test(snippet)) {
|
|
10
|
+
snippet += ` (aka: ${aliases.join(", ")})`;
|
|
11
|
+
}
|
|
4
12
|
return {
|
|
5
13
|
id: r.id,
|
|
6
14
|
scope_key: r.scope_key,
|
|
7
15
|
project_name: r.project_name,
|
|
8
16
|
type: r.type,
|
|
9
17
|
tags: r.tags ? r.tags.split(",").filter(Boolean) : [],
|
|
10
|
-
|
|
18
|
+
aliases,
|
|
19
|
+
snippet,
|
|
11
20
|
score: r.score,
|
|
12
21
|
updated_at: r.updated_at,
|
|
13
22
|
status: r.status,
|
|
@@ -124,7 +133,7 @@ export function search(query, opts = {}) {
|
|
|
124
133
|
: "";
|
|
125
134
|
const typeFilter = opts.type ? ` AND m.type = ?` : "";
|
|
126
135
|
const sql = `
|
|
127
|
-
SELECT m.id, m.scope_key, m.project_name, m.type, m.tags, m.updated_at,
|
|
136
|
+
SELECT m.id, m.scope_key, m.project_name, m.type, m.tags, m.aliases, m.updated_at,
|
|
128
137
|
m.status, m.superseded_by, m.review_state,
|
|
129
138
|
snippet(memories_fts, 0, '[', ']', ' ... ', 12) AS snippet,
|
|
130
139
|
bm25(memories_fts) AS score
|
package/dist/store/db.js
CHANGED
|
@@ -26,6 +26,7 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
26
26
|
importance TEXT NOT NULL DEFAULT 'normal',
|
|
27
27
|
status TEXT NOT NULL DEFAULT 'active',
|
|
28
28
|
tags TEXT NOT NULL DEFAULT '',
|
|
29
|
+
aliases TEXT NOT NULL DEFAULT '',
|
|
29
30
|
content TEXT NOT NULL,
|
|
30
31
|
cjk TEXT NOT NULL DEFAULT '',
|
|
31
32
|
content_hash TEXT NOT NULL DEFAULT '',
|
|
@@ -52,6 +53,7 @@ const FTS_SCHEMA = `
|
|
|
52
53
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
53
54
|
content,
|
|
54
55
|
tags,
|
|
56
|
+
aliases,
|
|
55
57
|
type,
|
|
56
58
|
cjk,
|
|
57
59
|
scope_key UNINDEXED,
|
|
@@ -61,24 +63,24 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
61
63
|
);
|
|
62
64
|
|
|
63
65
|
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
64
|
-
INSERT INTO memories_fts(rowid, content, tags, type, cjk, scope_key)
|
|
65
|
-
VALUES (new.rowid, new.content, new.tags, new.type, new.cjk, new.scope_key);
|
|
66
|
+
INSERT INTO memories_fts(rowid, content, tags, aliases, type, cjk, scope_key)
|
|
67
|
+
VALUES (new.rowid, new.content, new.tags, new.aliases, new.type, new.cjk, new.scope_key);
|
|
66
68
|
END;
|
|
67
69
|
|
|
68
70
|
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
69
|
-
INSERT INTO memories_fts(memories_fts, rowid, content, tags, type, cjk, scope_key)
|
|
70
|
-
VALUES ('delete', old.rowid, old.content, old.tags, old.type, old.cjk, old.scope_key);
|
|
71
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, tags, aliases, type, cjk, scope_key)
|
|
72
|
+
VALUES ('delete', old.rowid, old.content, old.tags, old.aliases, old.type, old.cjk, old.scope_key);
|
|
71
73
|
END;
|
|
72
74
|
|
|
73
75
|
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
74
|
-
INSERT INTO memories_fts(memories_fts, rowid, content, tags, type, cjk, scope_key)
|
|
75
|
-
VALUES ('delete', old.rowid, old.content, old.tags, old.type, old.cjk, old.scope_key);
|
|
76
|
-
INSERT INTO memories_fts(rowid, content, tags, type, cjk, scope_key)
|
|
77
|
-
VALUES (new.rowid, new.content, new.tags, new.type, new.cjk, new.scope_key);
|
|
76
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, tags, aliases, type, cjk, scope_key)
|
|
77
|
+
VALUES ('delete', old.rowid, old.content, old.tags, old.aliases, old.type, old.cjk, old.scope_key);
|
|
78
|
+
INSERT INTO memories_fts(rowid, content, tags, aliases, type, cjk, scope_key)
|
|
79
|
+
VALUES (new.rowid, new.content, new.tags, new.aliases, new.type, new.cjk, new.scope_key);
|
|
78
80
|
END;
|
|
79
81
|
`;
|
|
80
82
|
/** Current index schema version. Bump when TABLE_SCHEMA/FTS_SCHEMA change. */
|
|
81
|
-
const SCHEMA_VERSION =
|
|
83
|
+
const SCHEMA_VERSION = 7;
|
|
82
84
|
function userVersion(d) {
|
|
83
85
|
const row = d.prepare("PRAGMA user_version").get();
|
|
84
86
|
return row.user_version;
|
package/dist/store/lifecycle.js
CHANGED
|
@@ -171,6 +171,8 @@ export function supersede(oldId, input) {
|
|
|
171
171
|
id: ulid(),
|
|
172
172
|
type: input.type ?? oldMf.fm.type,
|
|
173
173
|
tags: input.tags ?? oldMf.fm.tags,
|
|
174
|
+
// D61: aliases carry forward unless the caller replaces them.
|
|
175
|
+
aliases: input.aliases ?? oldMf.fm.aliases,
|
|
174
176
|
source: input.source ?? oldMf.fm.source,
|
|
175
177
|
status: "active",
|
|
176
178
|
created_at: now,
|
package/dist/store/markdown.js
CHANGED
|
@@ -155,6 +155,12 @@ export function normalizeFrontmatter(raw) {
|
|
|
155
155
|
tags: Array.isArray(raw.tags)
|
|
156
156
|
? raw.tags.filter((t) => typeof t === "string")
|
|
157
157
|
: [],
|
|
158
|
+
...(Array.isArray(raw.aliases)
|
|
159
|
+
? (() => {
|
|
160
|
+
const aliases = normalizeAliases(raw.aliases.filter((a) => typeof a === "string"));
|
|
161
|
+
return aliases.length > 0 ? { aliases } : {};
|
|
162
|
+
})()
|
|
163
|
+
: {}),
|
|
158
164
|
source: typeof raw.source === "string" ? raw.source : "",
|
|
159
165
|
created_at: msToRfc3339(timeToMs(raw.created_at)),
|
|
160
166
|
updated_at: msToRfc3339(timeToMs(raw.updated_at)),
|
|
@@ -170,11 +176,43 @@ export function normalizeFrontmatter(raw) {
|
|
|
170
176
|
review_history: asReviewHistory(raw.review_history),
|
|
171
177
|
};
|
|
172
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* D61: normalize capture-time aliases — trim, drop empties, dedupe
|
|
181
|
+
* (case-insensitive), cap at 4. Bad aliases are noise, never an error.
|
|
182
|
+
* Shared by the tool ops, the CLI, and frontmatter parsing.
|
|
183
|
+
*/
|
|
184
|
+
export function normalizeAliases(input) {
|
|
185
|
+
if (!input)
|
|
186
|
+
return [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const a of input) {
|
|
190
|
+
const t = a.trim();
|
|
191
|
+
if (!t)
|
|
192
|
+
continue;
|
|
193
|
+
const k = t.toLowerCase();
|
|
194
|
+
if (seen.has(k))
|
|
195
|
+
continue;
|
|
196
|
+
seen.add(k);
|
|
197
|
+
out.push(t);
|
|
198
|
+
if (out.length >= 4)
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
173
203
|
export function isTaxonomyType(t) {
|
|
174
204
|
return TAXONOMY.has(t);
|
|
175
205
|
}
|
|
176
206
|
export function serialize(fm, body) {
|
|
177
|
-
|
|
207
|
+
// D61: an empty aliases list is noise in the file — drop the key entirely
|
|
208
|
+
// rather than relying on the YAML dumper to skip an undefined value
|
|
209
|
+
// (parse only sets the field when non-empty; keep both ends consistent).
|
|
210
|
+
let out = fm;
|
|
211
|
+
if (fm.aliases && fm.aliases.length === 0) {
|
|
212
|
+
out = { ...fm };
|
|
213
|
+
delete out.aliases;
|
|
214
|
+
}
|
|
215
|
+
const yml = yaml.dump(out, { lineWidth: -1, quotingType: '"' });
|
|
178
216
|
return `---\n${yml}---\n\n${body.trimEnd()}\n`;
|
|
179
217
|
}
|
|
180
218
|
/** Raw frontmatter parse (no normalization) — for migration tooling. */
|
package/dist/store/sync.js
CHANGED
|
@@ -7,8 +7,8 @@ import { iterMemoryFiles, iterInRepoMemoryFiles, readMemoryFile, timeToMs, } fro
|
|
|
7
7
|
import { projectRoot, paths } from "../paths.js";
|
|
8
8
|
import { loadConfig } from "../config.js";
|
|
9
9
|
const UPSERT_SQL = `
|
|
10
|
-
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)
|
|
11
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10
|
+
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)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
12
12
|
ON CONFLICT(id) DO UPDATE SET
|
|
13
13
|
scope_key = excluded.scope_key,
|
|
14
14
|
scope = excluded.scope,
|
|
@@ -19,6 +19,7 @@ const UPSERT_SQL = `
|
|
|
19
19
|
importance = excluded.importance,
|
|
20
20
|
status = excluded.status,
|
|
21
21
|
tags = excluded.tags,
|
|
22
|
+
aliases = excluded.aliases,
|
|
22
23
|
content = excluded.content,
|
|
23
24
|
cjk = excluded.cjk,
|
|
24
25
|
content_hash = excluded.content_hash,
|
|
@@ -53,7 +54,8 @@ export function upsertFromFile(mf) {
|
|
|
53
54
|
function writeRow(mf, mtimeMs) {
|
|
54
55
|
const { fm, body, filePath } = mf;
|
|
55
56
|
const tags = (fm.tags ?? []).join(",");
|
|
56
|
-
|
|
57
|
+
const aliases = (fm.aliases ?? []).join(",");
|
|
58
|
+
db().prepare(UPSERT_SQL).run(fm.id, fm.scope_key, fm.scope, fm.visibility, fm.project_name, fm.type, fm.role, fm.importance, fm.status, tags, aliases, body, cjkIndexText(body + "\n" + tags + "\n" + aliases), contentHash(body), fm.superseded_by ?? null, fm.source ?? "", filePath, mtimeMs, timeToMs(fm.created_at), timeToMs(fm.updated_at), fm.review_state ?? "draft");
|
|
57
59
|
}
|
|
58
60
|
export function deleteFromIndex(id) {
|
|
59
61
|
db().prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
package/dist/tools/ops.js
CHANGED
|
@@ -10,7 +10,7 @@ import fs from "node:fs";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { PERSONAL_SCOPE } from "../scope.js";
|
|
12
12
|
import { search, list, hitStateLabel } from "../retrieve/search.js";
|
|
13
|
-
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, MEMORY_TYPE_TAXONOMY, } from "../store/markdown.js";
|
|
13
|
+
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, MEMORY_TYPE_TAXONOMY, normalizeAliases, } from "../store/markdown.js";
|
|
14
14
|
import { upsertFromFile, deleteFromIndex } from "../store/sync.js";
|
|
15
15
|
import { findDuplicates, supersede } from "../store/lifecycle.js";
|
|
16
16
|
import { db } from "../store/db.js";
|
|
@@ -35,8 +35,8 @@ export async function withOutboxNote(scopeKey, p, reviewHint) {
|
|
|
35
35
|
}
|
|
36
36
|
/** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
|
|
37
37
|
export const TOOL_DESCRIPTIONS = {
|
|
38
|
-
memory_add: "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.",
|
|
39
|
-
memory_search: "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
|
|
38
|
+
memory_add: "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.",
|
|
39
|
+
memory_search: "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.",
|
|
40
40
|
memory_list: "List memories in a scope, newest first. Useful for browsing what is remembered, or verifying that a save landed.",
|
|
41
41
|
memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
|
|
42
42
|
memory_forget: "Delete a memory by id. Use when the user asks to forget something.",
|
|
@@ -52,6 +52,10 @@ export const scopeArg = z
|
|
|
52
52
|
.enum(["project", "personal", "user"])
|
|
53
53
|
.optional()
|
|
54
54
|
.describe("Memory scope. `project` = tied to this repo. `personal` = global across all your projects. `user` is a deprecated alias of `personal`. Default: project.");
|
|
55
|
+
export const aliasesArg = z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("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.");
|
|
55
59
|
export const memoryAddArgs = {
|
|
56
60
|
content: z.string().min(1).describe("The fact to remember. One idea per memory."),
|
|
57
61
|
type: z
|
|
@@ -60,6 +64,7 @@ export const memoryAddArgs = {
|
|
|
60
64
|
.describe("Category of memory. Default: fact."),
|
|
61
65
|
scope: scopeArg,
|
|
62
66
|
tags: z.array(z.string()).optional().describe("Optional tags for filtering."),
|
|
67
|
+
aliases: aliasesArg,
|
|
63
68
|
source: z
|
|
64
69
|
.string()
|
|
65
70
|
.optional()
|
|
@@ -90,6 +95,7 @@ export const memorySupersedeArgs = {
|
|
|
90
95
|
.optional()
|
|
91
96
|
.describe("Category of the new memory. Defaults to the old memory's type."),
|
|
92
97
|
tags: z.array(z.string()).optional().describe("Optional tags for the new memory."),
|
|
98
|
+
aliases: aliasesArg,
|
|
93
99
|
};
|
|
94
100
|
export const memoryForgetArgs = {
|
|
95
101
|
id: z.string().min(1),
|
|
@@ -149,6 +155,7 @@ function buildFrontmatter(s, body) {
|
|
|
149
155
|
importance: "normal",
|
|
150
156
|
status: "active",
|
|
151
157
|
tags: body.tags,
|
|
158
|
+
...(body.aliases && body.aliases.length > 0 ? { aliases: body.aliases } : {}),
|
|
152
159
|
source: body.source,
|
|
153
160
|
created_at: rfc,
|
|
154
161
|
updated_at: rfc,
|
|
@@ -180,6 +187,13 @@ export async function addMemory(getScope, cfg, args) {
|
|
|
180
187
|
const fm = buildFrontmatter(s, {
|
|
181
188
|
type: args.type ?? "fact",
|
|
182
189
|
tags: args.tags ?? [],
|
|
190
|
+
// D61: aliases are stored only when the install enables them (init
|
|
191
|
+
// asks once, default on); normalize caps at 4 and drops junk silently.
|
|
192
|
+
// Aliases pass through the same redaction as content (write-path
|
|
193
|
+
// invariant) — an alias carrying a token gets masked, not stored raw.
|
|
194
|
+
aliases: cfg.captureAliases
|
|
195
|
+
? normalizeAliases((args.aliases ?? []).map((a) => redact(a, cfg.redactPatterns).content))
|
|
196
|
+
: [],
|
|
183
197
|
source: args.source ?? "tool",
|
|
184
198
|
});
|
|
185
199
|
const { filePath } = writeMemoryFile(fm, redacted);
|
|
@@ -230,6 +244,11 @@ export async function supersedeMemory(cfg, args) {
|
|
|
230
244
|
body: redacted,
|
|
231
245
|
type: args.type,
|
|
232
246
|
tags: args.tags,
|
|
247
|
+
// D61: explicit aliases replace the carried-over ones, only when
|
|
248
|
+
// the install enables capture aliases. Same redaction as content.
|
|
249
|
+
aliases: cfg.captureAliases && args.aliases !== undefined
|
|
250
|
+
? normalizeAliases(args.aliases.map((a) => redact(a, cfg.redactPatterns).content))
|
|
251
|
+
: undefined,
|
|
233
252
|
source: "tool",
|
|
234
253
|
});
|
|
235
254
|
upsertFromFile(oldMf);
|