open-memex 0.7.0-alpha.1 → 0.7.0-alpha.13

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.
Files changed (66) hide show
  1. package/AGENTS.md +23 -5
  2. package/CHANGELOG.md +88 -0
  3. package/README.md +46 -15
  4. package/README.zh-CN.md +27 -14
  5. package/dist/.build-stamp +1 -0
  6. package/dist/capture/keywords.js +41 -8
  7. package/dist/cli.js +259 -40
  8. package/dist/config.js +102 -18
  9. package/dist/distill-agents.js +9 -1
  10. package/dist/doctor.js +37 -47
  11. package/dist/export.js +12 -2
  12. package/dist/index.js +4 -2
  13. package/dist/init.js +23 -6
  14. package/dist/inventory-html.js +150 -0
  15. package/dist/inventory.js +176 -0
  16. package/dist/mcp.js +21 -5
  17. package/dist/paths.js +34 -2
  18. package/dist/retrieve/display.js +161 -0
  19. package/dist/retrieve/inject.js +1 -1
  20. package/dist/retrieve/search.js +44 -5
  21. package/dist/review.js +9 -3
  22. package/dist/scope.js +17 -0
  23. package/dist/store/db.js +11 -9
  24. package/dist/store/lifecycle.js +20 -4
  25. package/dist/store/markdown.js +53 -7
  26. package/dist/store/migrate.js +14 -3
  27. package/dist/store/sync.js +5 -3
  28. package/dist/store/v2migrate.js +2 -2
  29. package/dist/submit.js +13 -5
  30. package/dist/tools/memory.js +28 -0
  31. package/dist/tools/ops.js +127 -16
  32. package/docs/CURATOR.md +7 -3
  33. package/docs/V2-DESIGN.md +299 -32
  34. package/docs/memory-visibility-design.md +130 -0
  35. package/package.json +2 -2
  36. package/scripts/build-stamp.mjs +63 -0
  37. package/scripts/smoke-mcp.ts +47 -1
  38. package/scripts/smoke-pure.ts +227 -3
  39. package/scripts/test-full.ts +435 -5
  40. package/skills/open-memex/SKILL.md +53 -1
  41. package/src/capture/keywords.ts +56 -6
  42. package/src/cli.ts +282 -39
  43. package/src/config.ts +104 -17
  44. package/src/distill-agents.ts +19 -0
  45. package/src/doctor.ts +41 -42
  46. package/src/export.ts +14 -2
  47. package/src/index.ts +4 -2
  48. package/src/init.ts +25 -6
  49. package/src/inventory-html.ts +191 -0
  50. package/src/inventory.ts +242 -0
  51. package/src/mcp.ts +21 -5
  52. package/src/paths.ts +35 -1
  53. package/src/retrieve/display.ts +174 -0
  54. package/src/retrieve/inject.ts +1 -1
  55. package/src/retrieve/search.ts +63 -6
  56. package/src/review.ts +9 -2
  57. package/src/scope.ts +18 -0
  58. package/src/store/db.ts +11 -9
  59. package/src/store/lifecycle.ts +24 -3
  60. package/src/store/markdown.ts +60 -7
  61. package/src/store/migrate.ts +15 -2
  62. package/src/store/sync.ts +6 -3
  63. package/src/store/v2migrate.ts +2 -1
  64. package/src/submit.ts +12 -4
  65. package/src/tools/memory.ts +28 -0
  66. package/src/tools/ops.ts +147 -17
package/AGENTS.md CHANGED
@@ -33,6 +33,7 @@ npm run typecheck # tsc --noEmit — the only
33
33
  npm run cli -- where | list | search "q" | add ... | forget <id> | reindex
34
34
  npm run cli -- <command> --help # per-command help (AI assistants discover flags this way)
35
35
  npm run cli -- sync-status # last sync time/kind + outbox drafts + repo review states + uncommitted files
36
+ npm run cli -- inventory [--format text|json|html] [--scope both] # what is remembered, in the open (D68); personal-bearing output refused inside a git worktree without --allow-personal
36
37
  npm run cli -- audit [--scope project|personal|both] # memory health: near-dup pairs, stale actives, broken chains, personal-in-repo (read-only)
37
38
  npm run cli -- pull # fetch + fast-forward only (explicit; diverged = clean failure, never force-merge)
38
39
  npm run cli -- push # push current branch to remote (explicit only; open-memex never auto-pushes)
@@ -102,7 +103,7 @@ The published `open-memex` bin points at `dist/cli.js` (compiled at publish time
102
103
  From a source checkout, `npm run cli` / `npm run mcp` still run `src/` directly
103
104
  with type-stripping — no build step needed for development.
104
105
 
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 `MY_O_MEMORY_HOME`.
106
+ 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
107
 
107
108
  To load the plugin in opencode locally, `~/.config/opencode/opencode.jsonc` must have:
108
109
  ```
@@ -115,7 +116,7 @@ Restart opencode after any change — the plugin is not hot-reloaded.
115
116
  Storage root:
116
117
  - Windows: `%APPDATA%\open-memex\`
117
118
  - Linux/macOS: `$XDG_DATA_HOME/open-memex/` (fallback `~/.local/share/open-memex/`)
118
- - Override with `MY_O_MEMORY_HOME` (use this for tests to avoid clobbering real data).
119
+ - 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
120
 
120
121
  Layout: `memories/<scope_key>/<id>.md` (YAML frontmatter + body) + `index.db` (SQLite FTS5).
121
122
 
@@ -125,6 +126,7 @@ Layout: `memories/<scope_key>/<id>.md` (YAML frontmatter + body) + `index.db` (S
125
126
 
126
127
  - `personal` scope key is literal `"personal"` (v1 called this `user`; renamed in v2, design §19).
127
128
  - `project` scope key: `project__<sanitized-name>__<12-hex-sha256>`, seeded from normalized git origin URL, else lowercased cwd. See `src/scope.ts`. Same repo across machines → same key (intentional; enables future git-commit of memories).
129
+ - The root a host adapter seeds on matters: OpenCode v1 reports the **filesystem root** as `worktree` for non-git folders, so the v1 adapter goes through `pickScopeRoot` (src/scope.ts) — `worktree` only when it is a real directory below the filesystem root, else the opened `directory` (D71). Seeding on a filesystem root merges every non-git folder on a drive into one `project__workspace__…` bucket.
128
130
  - `org`, `team`, `public` are reserved — the schema rejects writes. See `docs/SCOPES.md`.
129
131
  - The scope key **changes** when a repo gains/loses a git origin. `src/index.ts` logs a one-shot warning on load if it finds files under the legacy cwd-only key (`resolveCwdScope`). Use `cli scopes` to enumerate all scope dirs and `cli migrate --from <old>` to reconcile — see `src/store/migrate.ts`. No auto-migration; two unrelated repos at the same cwd would silently merge.
130
132
  - Read-only callers must use `memoriesDirPath` (in `src/paths.ts`), never `memoriesDirFor`. The latter `mkdir -p`s the directory as a side effect and will pollute storage with empty scope dirs.
@@ -136,23 +138,39 @@ Every write path (tool, keyword hook, CLI `add`) must:
136
138
  2. If `hadSecret` → the matched secret is **masked in place** (first 4 characters kept, the rest replaced with `x`) and the write proceeds; never save the unmasked original. `<private>…</private>` spans are stripped to `[REDACTED]` instead (design D14).
137
139
  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
140
  4. `writeMemoryFile` first, then `readMemoryFile` + `upsertFromFile` to keep FTS in sync.
141
+ 5. Deletion reports honestly (D63): only ENOENT counts as "deleted"; any other unlink failure keeps the index row and returns failure — the file is the source of truth and would otherwise resurrect the memory on next sync.
142
+
143
+ Capture-time aliases (D61): `memory_add`/`supersede` may carry up to 4
144
+ `aliases` (alternate phrasings) into frontmatter; they index in their own FTS
145
+ column. Only when `cfg.captureAliases` is on (init asks once, default on) —
146
+ when off, passed aliases are dropped. Always pass them through
147
+ `normalizeAliases`; the keyword-capture path (no agent in the loop) never
148
+ invents aliases. Checkpoint-proposal guidance (propose, never auto-draft) is
149
+ copy, not mechanism: keep the rubric wording in sync across the skill, the
150
+ AGENTS.md hygiene footer, the MCP handshake, and the tool descriptions.
139
151
 
140
152
  ## Lifecycle invariants (design §3.3)
141
153
 
142
154
  - Statuses: `active → superseded | deprecated | retracted | archived`. Retrieval excludes `retracted`/`archived`, ranks `active` above `deprecated`.
143
155
  - Never hand-write `status: superseded` or half a chain. Use the `supersede` code path (`src/store/lifecycle.ts`): the old record keeps its file, flips to `superseded`, and both sides get `supersedes`/`superseded_by`. Only `active` memories can be superseded.
156
+ - Superseding a memory that had entered review (proposed/approved/published/rejected) starts the replacement at `review_state: proposed` so `promote` can advance it (D64). Never reset it to `draft` — a draft in the repo dir is stranded (submit only moves outbox files).
157
+ - `setStatus` refuses `retracted → active` (D64): retracted content must not silently return to recall. Save a new memory instead.
158
+ - **Visibility surfaces share one contract (D70).** `memory_list`, `cli list` and `inventory` render the same `formatInventoryLine`; numbering is a running counter across every section of one listing (never per scope), and a cut list discloses how many entries it dropped. Any *correction* that touches an in-repo memory (`memory_forget`, `soft=true`, `forget --soft`) must say the change is local until committed and pushed — use `isInRepoMemoryFile` (`src/paths.ts`). Report output paths go through `inventoryWriteRefusal` (`src/cli.ts`), which fails closed on an unresolvable directory.
159
+ - **Every memory-file write goes through `atomicWriteTextSync`** (markdown.ts; tmp + rename). The create path, supersede flips, review transitions, submit landings, migrate/v2migrate rewrites — no direct `fs.writeFileSync` on a memory file. A torn read mid-rewrite parses as a deletion and drops the memory from the index.
144
160
  - Chain integrity is self-healing: on read, a missing counterpart is auto-completed with a warning; a dangling pointer warns but is never fabricated. Don't "fix" chains by editing frontmatter directly — let the read path do it.
145
161
  - Frontmatter is `schema_version: 2`. The SQLite index schema is versioned separately and rebuilds automatically on version change — never hand-edit `index.db`.
146
162
 
147
- Keyword capture fires from `chat.message` on the user's message parts (`UserMessage`). Patterns live in `src/capture/keywords.ts` / config `keywordPatterns`; regex group 1 is the memory body. Scope routing: personal patterns (`cfg.keywordPersonalPatterns`) force the personal scope — the rule is 我 → personal (记住我/替我记/我觉得/我喜欢/remember for me), 我们 → current scope (我们认为/我们决定/帮我们记住); personal patterns run first and claim their line so a generic trigger can't double-fire.
163
+ Keyword capture fires from `chat.message` on the user's message parts (`UserMessage`). Patterns live in `src/capture/keywords.ts` / config `keywordPatterns`; regex group 1 is the memory body. Scope routing: personal patterns (`cfg.keywordPersonalPatterns`) force the personal scope — the rule is 我 → personal (记住我/替我记/帮我记/我觉得/我喜欢/remember for me/help me remember), 我们 → current scope (我们认为/我们决定/帮我们记住); personal patterns run first and claim their line so a generic trigger can't double-fire (a rejected personal match keeps the claim — see D67).
164
+
165
+ D67 rules for any new pattern: a trigger must be a **statement to the store**, not narration — 记得… / `remind me to…` are deliberately out, and the ambiguous forms require an explicit marker (`别忘了:…`, `don't forget: …`, `don't forget that …`). The 帮/替/我们 family takes the full verb phrase (`记` + optional `住|录`, never followed by a verb continuation) + optional `一下`; the separator boundary applies to the **bare** verb only, so `帮我记一下这个配置` captures while `帮我记得带伞` cannot produce mid-word garbage. Bodies under `MIN_CAPTURE_CHARS` (3) are rejected as fragments — never lower the floor silently; `scanKeywords` returns the drop so `capture --dry-run` and the debug log can report it.
148
166
 
149
167
  Context injection happens exactly once per session in `experimental.chat.system.transform`, guarded by an in-memory `Set<sessionID>` in `src/index.ts`. It is not persisted — restarting opencode re-injects on the next first turn.
150
168
 
151
169
  ## Design constraints — read the frozen design first
152
170
 
153
- `docs/V2-DESIGN.md` is the frozen protocol v0.2 (zero open questions). Per its §12:
171
+ `docs/V2-DESIGN.md` is the frozen protocol v0.2 (decisions settled through D71; open questions tracked at the end of the doc). Per its §12:
154
172
  AGENTS.md answers "how should AI work here"; the design doc answers "why is it
155
- built this way" (principles, iron rules, D1–D49 decision log). Before changing
173
+ built this way" (principles, iron rules, the append-only decision log, currently D1–D71). Before changing
156
174
  architecture, scope semantics, lifecycle, or the protocol surface (frontmatter
157
175
  schema, MCP tools, CLI contract), read the relevant design section — the decision
158
176
  log records what was already considered and rejected.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,94 @@ the `@alpha` tag are development snapshots and are not listed individually.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## Non-git folders shared one scope bucket (D71)
13
+
14
+ - **Fixed: every non-git folder on a drive shared a single project scope.** OpenCode v1 reports the filesystem root (`C:\`, `/`) as the project `worktree` when the opened folder is not a git repo, and the plugin seeded its scope on that root — so unrelated folders like `C:\temp` and `C:\scratch` all wrote to one `project__workspace__…` bucket. The scope now seeds on the folder actually opened (`pickScopeRoot` in `src/scope.ts`); real repo worktrees are unchanged. Memories already saved under the shared bucket are not moved automatically: run `open-memex scopes` to spot it, then `open-memex migrate --from <old-key>`.
15
+
16
+ ## Visibility follow-ups: one number, one guard, one truth (D70)
17
+
18
+ Review of the D68/D69 inventory surfaces — each was honest on its own, and they disagreed with each other:
19
+
20
+ - `open-memex list` no longer truncates silently: it renders the same structured line the agent sees and says how many entries it left out (20 of 25 used to look like all 25).
21
+ - Numbers are unique within one listing. `memory_list(scope: both)` and the text report restarted the counter per scope, so a single listing held two `#1`s and "delete #3" was a guess.
22
+ - Hiding an in-repo memory now says what the delete path already said: the retraction is a local working-tree edit until you commit and push it — until then the team still sees the memory.
23
+ - The report's worktree guard fails closed (it used to skip the check when the output directory did not exist yet), explains a missing directory instead of dying with a raw `ENOENT`, and is shared by all three formats instead of existing twice.
24
+ - `inventory --format json` is now `open-memex-inventory/2`: the absolute `file` path is gone — that artifact is meant for an agent, and it carried your home directory with it.
25
+ - An empty scope is reported as `(0)` rather than disappearing; only the HTML audit view loads the hidden rows themselves; the page enforces its "no network requests" promise with a `default-src 'none'` CSP.
26
+
27
+ ## Inventory HTML report (D68 follow-up)
28
+
29
+ - **`open-memex inventory --format html`** renders the same data layer as the text/JSON formats into a single local page: current memories grouped by scope, outbox drafts in their own section, and the replaced/hidden history folded at the bottom with supersede-chain pointers. All content is escaped; the filter box is local show/hide only. Default output is `<data dir>/inventory.html`; the worktree refusal (`--allow-personal`) applies here too. Sections fold when the store grows past 50 current entries.
30
+
31
+ ## Memory visibility: see it, hide it, delete it (D68)
32
+
33
+ Group 3-C — the surface a user reads to learn what the store actually remembers:
34
+
35
+ - **`memory_list` is now a numbered inventory.** Lines are structured (`[type] id=… created=… source=…`), keep the raw provenance, and every listing states its true total — a truncated list says how many entries it is not showing instead of quietly looking complete. New input `include: "active" | "all"` (default `active`): **behavior change** — superseded versions, retracted and archived memories no longer appear unless you pass `include=all` (the audit view). The CLI gains the matching `list --include` flag.
36
+ - **`open-memex inventory`** renders the same data as readable text (default) or JSON (`--format json`, format `open-memex-inventory/1`) — personal plus the current project, outbox drafts in their own section, hidden history counted rather than silently dropped. A report containing personal memories refuses to be written inside a git working tree without `--allow-personal`.
37
+ - **Hide instead of delete:** `memory_forget` gains `soft=true` (CLI: `forget --soft`) — the memory is retracted: out of lists and search, file kept. Still one-way (D64): bringing the fact back means saving it again.
38
+ - **Guidance kept in one voice:** the tool descriptions, the MCP session-start instructions, SKILL.md and the `distill-agents` AGENTS.md snippet all teach the same flow — answer "what do you remember?" conversationally from the inventory, re-list before acting on a number, read an entry back before deleting it.
39
+
40
+ ## Keyword capture: what counts as a trigger (D67)
41
+
42
+ The D66 review left two questions open and introduced one regression. Ruled, not deferred:
43
+
44
+ - **Fixed a regression:** D66's separator boundary was one notch too strict — it also demanded a separator after a *complete* verb phrase, so `帮我记一下这个配置`, `帮我记住这个配置`, `替我记一下我住在杭州` and `帮我们记一下这个约定` stopped matching. The boundary now guards the bare verb only, so `帮我记得…` still can't produce mid-word garbage.
45
+ - **The ≥3-character body floor stays, but is no longer silent.** `记住:这个` is a fragment, not a memory, so it isn't captured — but `open-memex capture --dry-run` now says why, and `logLevel: debug` logs it. A rejected *personal* match also keeps its line, so `记住我:OK` can no longer fall through to the generic 记住 and be saved as `我:OK`.
46
+ - **Narration is not a trigger.** `记得…`, `remind me to…` and friends stay out on purpose (a false trigger writes unreviewed memory; a missed one costs a sentence). The ambiguous forms already in the list now need an explicit marker: `别忘了:…`, `don't forget: …` and `don't forget that …` capture; bare `别忘了带伞` and `don't forget the wifi password` do not. The `note` family is unchanged.
47
+ - **Test tooling:** the stale-`dist/` guard is now a content fingerprint (`scripts/build-stamp.mjs`) instead of mtimes, which a branch switch could defeat; the agent-facing `memory_status` cap (20 per section + a "full list" pointer) is pinned again after D66's test replaced that coverage.
48
+
49
+ ## Review follow-ups (D66)
50
+
51
+ Fixes from the follow-up review of the D63–D65 stack:
52
+
53
+ - Keyword patterns: `请帮我记住…` works (the D65 pattern had missed its `请` prefix), `帮我记录一下:…` / `替我记录一下:…` capture cleanly instead of saving mid-word garbage, `帮我记得…` no longer fires, and `help me remember: …` routes personal like `remember for me`.
54
+ - `open-memex sync-status` (the CLI) shows the full list again — the 20-per-section cap now applies only to agent tool results, whose "… and N more" line points at the CLI. `open-memex list --scope both` matches the `memory_list` tool.
55
+ - `open-memex status --help` documents that retraction is one-way.
56
+ - The full test suite is Windows-safe now (file:// probe imports, USERPROFILE-aware uninstall test) and refuses to run against a stale `dist/` build.
57
+
58
+ ## Keyword capture gaps (D65)
59
+
60
+ The most natural phrasings were the ones that fell through:
61
+
62
+ - `帮我记住…` / `帮我记一下…` now capture (personal scope, same 我-rule as 替我记; `帮我们记住…` still routes project).
63
+ - A leading `请` no longer defeats capture: `请记住我…`, `请记住(个人)…`, and `请记住:…` all work.
64
+ - English `remember to …` no longer leaves a stray "to" at the front of the captured body.
65
+
66
+ Patterns are code defaults, so these apply automatically on upgrade — unless you hand-customized `keywordPatterns` / `keywordPersonalPatterns` in your config, in which case your lists win and you can port the additions by hand.
67
+
68
+ ## P1 fixes: stale answers and stranded files (D64)
69
+
70
+ Second batch from the whole-project review — the index, the files, and the guidance can no longer quietly disagree:
71
+
72
+ - The opencode plugin now re-syncs the index before every tool call (previously only at startup): memories added via the CLI or another client are visible right away instead of after a restart.
73
+ - Superseding a published memory re-enters review as `proposed` instead of a stranded `draft` no command could advance; `promote` can move it forward again.
74
+ - All memory-file rewrites are atomic (tmp + rename), not just creates.
75
+ - A retracted memory can't be flipped back to `active` via status changes — save a new memory if the content is valid again.
76
+ - Proposed copies of personal memories are `visibility: internal` (was: `private`, which export silently excluded from team bundles).
77
+ - `uninstall` finds the real global opencode config (`.jsonc` included), and `doctor` checks all eleven MCP tools plus stranded v1 (`user`-scope) memories.
78
+ - `memory_status` output is capped per section; `memory_list` accepts `scope: both`; `forget` notes when the deleted file was in the repo; `where` prints the config path.
79
+
80
+ ## Correctness fixes from the whole-project review (D63)
81
+
82
+ Six silent-wrong-answer bugs found by an external three-lens review, each fixed with a regression test:
83
+
84
+ - `init --client visualstudio` no longer wipes other servers from an existing `.mcp.json` (the parsed config was dropped instead of merged).
85
+ - `memory_forget` / `forget` tell the truth: when the memory file can't be deleted (locked, in use), they report failure and keep the index entry, instead of claiming success while the file — the source of truth — brings the memory back on next sync.
86
+ - Config parsing is string-aware: a redact pattern like `src/**/secrets` is no longer eaten as a block comment and silently disabled. `doctor` and the config loader now share one parser.
87
+ - `loadConfig` enforces the `memoryDir` invariant on hand-edited configs; an escaping value (`../../shared`) falls back to the default with a warning instead of writing project memories outside the repo.
88
+ - `migrate` re-scopes as well as re-keys: migrated files get the destination's `scope`/`visibility`, so personal→project migrations are submittable and project→personal memories stay private in exports. Conflict resolution compares parsed timestamps, not RFC-3339 strings.
89
+ - `import` rejects bundle entries whose paths escape the bundle (`../` or symlinks) instead of reading arbitrary local files into the store; rejected paths are printed.
90
+
91
+ ## Environment variable names (D62)
92
+
93
+ - 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.
94
+
95
+ ## Proactive capture: checkpoint proposals and aliases (D61)
96
+
97
+ - 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.
98
+ - 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.
99
+
12
100
  ## One plugin, both opencode generations (D60)
13
101
 
14
102
  - 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
@@ -222,9 +222,10 @@ open-memex init --client vscode
222
222
  ```
223
223
 
224
224
  Writes the MCP server entry; reload the window afterwards and confirm the
225
- `open-memex` server is started in Copilot Chat's MCP panel. By default the
226
- server entry goes to VS Code's *user-level* config (works in every project);
227
- `init` can also write a project-level `.vscode/mcp.json` if you prefer.
225
+ `open-memex` server is started in Copilot Chat's MCP panel. By default this
226
+ writes a project-level `.vscode/mcp.json` (an interactive run asks which
227
+ level you want); add `--global` for VS Code's *user-level* config instead —
228
+ one setup that works in every project.
228
229
 
229
230
  **Cursor:**
230
231
 
@@ -232,8 +233,8 @@ server entry goes to VS Code's *user-level* config (works in every project);
232
233
  open-memex init --client cursor
233
234
  ```
234
235
 
235
- Same shape as VS Code: user-level MCP config by default, project-level
236
- `.cursor/mcp.json` on request, plus Copilot-style instructions.
236
+ Same shape as VS Code: project-level `.cursor/mcp.json` by default,
237
+ user-level MCP config with `--global`, plus Copilot-style instructions.
237
238
 
238
239
  **opencode** (native plugin — recommended):
239
240
 
@@ -363,17 +364,35 @@ snippet to paste instead.
363
364
  Three ways memories get in:
364
365
 
365
366
  - **Keyword triggers** (opencode native plugin only): say `remember …`,
366
- `note that …`, `don't forget …`, `TIL …`, `save this …` — or in Chinese
367
- `记住…` / `记一下` / `记录一下` / `别忘了…` — and the sentence is captured
367
+ `note that …`, `don't forget: …`, `TIL …`, `save this …` — or in Chinese
368
+ `记住…` / `记一下…` / `记录一下…` / `别忘了:…` — and the sentence is captured
368
369
  without any tool call. Captures land in the current **project** by default;
369
- phrases that signal "this is about me" — `remember for me …`, `记住我…`,
370
- `替我记…`, `我觉得…`, `我喜欢…` — go to **personal** instead, and
370
+ phrases that signal "this is about me" — `remember for me …`, `help me remember: …`,
371
+ `记住我…`, `替我记…`, `帮我记…`, `我觉得…`, `我喜欢…` — go to **personal** instead, and
371
372
  team-context phrases (`我们决定…`, `帮我们记住…`) stay in project.
373
+ Two rules keep the noise down (D67): a trigger must be a *statement to the
374
+ store*, so the narration forms `记得…` / `remind me to…` / `别忘了带伞` /
375
+ `don't forget the wifi password` never fire (add a separator — `别忘了:…`,
376
+ `don't forget: …` — or `that` to make it an instruction); and a captured body
377
+ under 3 characters is rejected as a fragment rather than saved — `capture
378
+ --dry-run` says so instead of dropping it silently.
372
379
  - **The agent saves it**: in any editor, ask your agent to remember something
373
380
  (or it saves on its own when you state a fact worth keeping) — it calls
374
381
  `memory_add`. The routing above is a heuristic; you can always say "save this
375
382
  to my personal memory" or use the CLI with `--scope` to be explicit.
376
- - **The CLI**: `open-memex add "…"` with optional `--scope` / `--tags` / `--type`.
383
+ - **Checkpoint proposals**: when a task wraps up, the agent proposes 1–3 short
384
+ candidate memories distilled from the session — decisions and their reasons,
385
+ conventions, gotchas, approaches tried and abandoned — and saves only the
386
+ ones you approve. Nothing is written silently: no draft is created behind
387
+ your back.
388
+ - **The CLI**: `open-memex add "…"` with optional `--scope` / `--tag` / `--type`
389
+ / `--aliases`.
390
+
391
+ **Aliases.** Each saved memory can carry up to 4 alternate phrasings
392
+ (synonyms, another language's equivalent) that are indexed with it, so a
393
+ question worded differently still finds the memory — "vacation days" finds the
394
+ holiday policy. `init` asks once whether to enable this (default on); turn it
395
+ off any time with `open-memex config set captureAliases false`.
377
396
 
378
397
  **Redaction.** Wrap anything sensitive in `<private>…</private>` and it is
379
398
  stripped before saving. Recognized secrets (API keys, tokens, high-entropy
@@ -397,9 +416,9 @@ they only matter once you share memories through Git.
397
416
  |---|---|
398
417
  | ● `memory_add` | Save a fact, preference, decision, note |
399
418
  | ● `memory_search` | Keyword search (BM25) across project + personal memories |
400
- | ● `memory_list` | List memories in a scope, newest first |
419
+ | ● `memory_list` | List memories in a scope (`project`, `personal`, or `both`) as a numbered inventory; `include=all` is the audit view |
401
420
  | ● `memory_supersede` | Replace a memory with a newer version (keeps a supersede chain) |
402
- | ● `memory_forget` | Delete a memory by id |
421
+ | ● `memory_forget` | Delete a memory by id (`soft=true` hides it instead — retracted, one-way) |
403
422
  | `memory_status` | Show the sync queue: outbox drafts, repo review states, uncommitted files |
404
423
  | `memory_submit` | Move named drafts into the repo memory dir (local branch + commit) |
405
424
  | `memory_propose` | Copy personal memories into the project scope as review candidates |
@@ -566,8 +585,9 @@ branches and PRs like any other file.
566
585
 
567
586
  Settings live in `~/.config/opencode/open-memex.jsonc` — the `opencode` in the
568
587
  path is historical; this one file is shared by every client. Override the config
569
- path with `MY_O_MEMORY_CONFIG` and the storage root with `MY_O_MEMORY_HOME`
570
- (legacy environment names from before the rename, still honored).
588
+ path with `OPEN_MEMEX_CONFIG` and the storage root with `OPEN_MEMEX_HOME`
589
+ (the pre-rename `MY_O_MEMORY_CONFIG` / `MY_O_MEMORY_HOME` names are still
590
+ honored as fallbacks).
571
591
 
572
592
  Defaults:
573
593
 
@@ -592,7 +612,7 @@ open-memex config set sync.autoPull true # best-effort pull at MCP session sta
592
612
  ```
593
613
 
594
614
  Settable keys: `maxProjectMemories`, `maxProfileItems`, `injectOnFirstTurn`,
595
- `keywordCaptureEnabled`, `logLevel`, `memoryDir`, and `sync.autoPull` (a dotted
615
+ `keywordCaptureEnabled`, `captureAliases`, `logLevel`, `memoryDir`, and `sync.autoPull` (a dotted
596
616
  key that writes into the nested `sync` object). Full design:
597
617
  [docs/V2-DESIGN.md](./docs/V2-DESIGN.md).
598
618
 
@@ -622,9 +642,20 @@ open-memex add "This repo uses better-sqlite3" --type fact
622
642
  open-memex search "auth flow"
623
643
  open-memex search "auth flow" --explain # show FTS expression, scores, lifecycle-hidden counts
624
644
  open-memex list --scope project
645
+ open-memex list --scope both # each scope's newest under its own header
646
+ open-memex list --include all # audit view: superseded versions, retracted, archived
625
647
  open-memex supersede <id> "Updated content"
626
648
  open-memex status <id> deprecated
627
649
  open-memex forget <id>
650
+ open-memex forget <id> --soft # hide instead of delete (retracted; one-way)
651
+
652
+ open-memex inventory # everything remembered, as readable text
653
+ open-memex inventory --format json # the same data, for agents
654
+ open-memex inventory --format html # the same data as a local page (writes <data dir>/inventory.html)
655
+ # Personal + current project, outbox drafts in their own section, hidden
656
+ # history counted. Refuses to write a personal-bearing report inside a
657
+ # git working tree unless you pass --allow-personal.
658
+ # `list` numbers every entry in one listing and says how many it left out.
628
659
  ```
629
660
 
630
661
  Team review workflow (two homes, one per stage):
package/README.zh-CN.md CHANGED
@@ -162,7 +162,7 @@ npx -y open-memex init --yes
162
162
  open-memex init --client vscode
163
163
  ```
164
164
 
165
- 写入 MCP 服务器配置并可干净重载;重载窗口,确认 Copilot Chat 的 MCP 面板里 `open-memex` 服务器已启动。默认把配置写到 VS Code 的*用户级*配置(每个项目都生效);也可以写到项目级 `.vscode/mcp.json`。
165
+ 写入 MCP 服务器配置;重载窗口,确认 Copilot Chat 的 MCP 面板里 `open-memex` 服务器已启动。默认写到项目级 `.vscode/mcp.json`(交互式运行会问你选哪一级);加 `--global` 改写 VS Code 的*用户级*配置——一次设置,每个项目都生效。
166
166
 
167
167
  **Cursor**:
168
168
 
@@ -170,7 +170,7 @@ open-memex init --client vscode
170
170
  open-memex init --client cursor
171
171
  ```
172
172
 
173
- 形态和 VS Code 一样:默认用户级 MCP 配置,可选项目级 `.cursor/mcp.json`,外加 Copilot 风格的指令文件。
173
+ 形态和 VS Code 一样:默认项目级 `.cursor/mcp.json`,`--global` 改为用户级 MCP 配置,外加 Copilot 风格的指令文件。
174
174
 
175
175
  **opencode**(原生插件——推荐):
176
176
 
@@ -252,9 +252,12 @@ open-memex uninstall --yes
252
252
 
253
253
  三种存入方式:
254
254
 
255
- - **关键词触发**(仅 opencode 原生插件):说 `remember …`、`note that …`、`don't forget …`、`TIL …`、`save this …`——中文 `记住…` / `记一下` / `记录一下` / `别忘了…`——这句话就被存下来,不需要任何工具调用。捕获默认进当前 **project**;`remember for me …`、`记住我…`、`替我记…`、`我觉得…`、`我喜欢…` 这类"这是关于我"的信号会改为进 **personal**;`我们决定…`、`帮我们记住…` 等团队语境仍留在项目里。
255
+ - **关键词触发**(仅 opencode 原生插件):说 `remember …`、`note that …`、`don't forget: …`、`TIL …`、`save this …`——中文 `记住…` / `记一下…` / `记录一下…` / `别忘了:…`——这句话就被存下来,不需要任何工具调用。捕获默认进当前 **project**;`remember for me …`、`help me remember: …`、`记住我…`、`替我记…`、`帮我记…`、`我觉得…`、`我喜欢…` 这类"这是关于我"的信号会改为进 **personal**;`我们决定…`、`帮我们记住…` 等团队语境仍留在项目里。两条降噪规则(D67):触发词必须是**对存储说的话**,所以叙述型的 `记得…` / `remind me to…` / `别忘了带伞` / `don't forget the wifi password` 一律不触发(加一个分隔符——`别忘了:…`、`don't forget: …`——或用 `that`,才算指令);捕获内容不足 3 个字符按碎片丢弃,不会存下来——`capture --dry-run` 会明确说明,不再悄悄吞掉。
256
256
  - **助手主动保存**:在任何编辑器里,你让助手记住某件事(或者你陈述了一个值得记的事实、它自己判断该保存),它会调用 `memory_add`。上面的路由是启发式规则;你可以明说"存到我的个人记忆",或用 CLI 加 `--scope` 明确指定。
257
- - **CLI**:`open-memex add "…"`,可选 `--scope` / `--tags` / `--type`。
257
+ - **检查点提议**:一个任务收尾时,助手会提议 1–3 条从这轮工作里提炼的候选记忆——决定及其理由、约定、踩过的坑、走过又放弃的路——只有你点头的那几条才存。不会有任何东西在你不知情时被写下来。
258
+ - **CLI**:`open-memex add "…"`,可选 `--scope` / `--tag` / `--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
 
@@ -272,9 +275,9 @@ MCP 服务器暴露十一个工具;opencode 原生插件暴露其中五个核
272
275
  |---|---|
273
276
  | ● `memory_add` | 保存一条事实、偏好、决策、笔记 |
274
277
  | ● `memory_search` | 关键词搜索(BM25),横跨项目 + 个人记忆 |
275
- | ● `memory_list` | 列出某个作用域的记忆,最新在前 |
278
+ | ● `memory_list` | 以编号清单列出某个作用域(`project`、`personal` 或 `both`)的记忆;`include=all` 是审计视图 |
276
279
  | ● `memory_supersede` | 用新版本替换一条记忆(保留 supersede 链) |
277
- | ● `memory_forget` | 按 id 删除一条记忆 |
280
+ | ● `memory_forget` | 按 id 删除一条记忆(`soft=true` 改为隐藏——retracted,单向) |
278
281
  | `memory_status` | 显示同步队列:outbox 草稿、仓库评审状态、未提交文件 |
279
282
  | `memory_submit` | 把指定的草稿移进仓库记忆目录(本地分支 + 提交) |
280
283
  | `memory_propose` | 把个人记忆复制到项目作用域,作为评审候选 |
@@ -320,7 +323,7 @@ MCP 服务器暴露十一个工具;opencode 原生插件暴露其中五个核
320
323
  4. **PR 评审**——记忆是纯 Markdown;评审者走正常分支/PR 流程批准、要求修改或拒绝。
321
324
  5. **回想**——已发布的记忆在会话开始时注入,平时也能随时搜,人和助手都一样。
322
325
 
323
- 看护共享记忆的人遵循 [维护者约定](./docs/CURATOR.md):什么该批准、什么该打回、哪些卫生规则让共享记忆不腐烂。
326
+ 看护共享记忆的人遵循 [维护者约定](./docs/CURATOR.md)(英文):什么该批准、什么该打回、哪些卫生规则让共享记忆不腐烂。
324
327
 
325
328
  ## 回想:记忆是怎么回来的
326
329
 
@@ -388,7 +391,7 @@ npm install -g open-memex@latest # 或 @alpha
388
391
 
389
392
  ## 配置
390
393
 
391
- 设置放在 `~/.config/opencode/open-memex.jsonc`——路径里的 `opencode` 是历史名;这一个文件由所有客户端共享。用 `MY_O_MEMORY_CONFIG` 覆盖配置路径,用 `MY_O_MEMORY_HOME` 覆盖存储根目录(改名之前留下的旧环境变量名,仍然有效)。
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
 
@@ -411,7 +414,7 @@ open-memex config set maxProjectMemories 12
411
414
  open-memex config set sync.autoPull true # MCP 会话开始时尽力而为地 pull
412
415
  ```
413
416
 
414
- 可设置的键:`maxProjectMemories`、`maxProfileItems`、`injectOnFirstTurn`、`keywordCaptureEnabled`、`logLevel`、`memoryDir`,以及 `sync.autoPull`(带点的键,写入嵌套的 `sync` 对象)。完整设计:[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)。
417
+ 可设置的键:`maxProjectMemories`、`maxProfileItems`、`injectOnFirstTurn`、`keywordCaptureEnabled`、`captureAliases`、`logLevel`、`memoryDir`,以及 `sync.autoPull`(带点的键,写入嵌套的 `sync` 对象)。完整设计:[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)(英文)。
415
418
 
416
419
  ## CLI 参考
417
420
 
@@ -439,9 +442,19 @@ open-memex add "This repo uses better-sqlite3" --type fact
439
442
  open-memex search "auth flow"
440
443
  open-memex search "auth flow" --explain # 显示 FTS 表达式、分数、被生命周期藏掉的计数
441
444
  open-memex list --scope project
445
+ open-memex list --scope both # 两个作用域各自最新的,各带一个标题
446
+ open-memex list --include all # 审计视图:被替换的版本、已撤回、已归档
442
447
  open-memex supersede <id> "Updated content"
443
448
  open-memex status <id> deprecated
444
449
  open-memex forget <id>
450
+ open-memex forget <id> --soft # 隐藏而不是删除(retracted,单向)
451
+
452
+ open-memex inventory # 记住的一切,纯文本清单
453
+ open-memex inventory --format json # 同一份数据,JSON 格式给 agent 用
454
+ open-memex inventory --format html # 同一份数据的本地网页(写到 <数据目录>/inventory.html)
455
+ # 个人 + 当前项目,outbox 草稿单独成节,被藏起的历史只计数。含个人内容的
456
+ # 报告拒绝写进 git 工作树,除非显式 --allow-personal。
457
+ # `list` 在同一份清单里连续编号,并说明自己少显示了哪些条目。
445
458
  ```
446
459
 
447
460
  团队评审工作流(两个家,一个阶段一个):
@@ -459,7 +472,7 @@ open-memex submit <id...> [--branch <name>] [--base <branch>]
459
472
  # 把你点名的草稿移进 .ai/open-memex/,标为 "proposed":
460
473
  # 复制、翻转 review_state、在当前分支本地 git 提交。
461
474
  # 永不自己建分支——建分支是你(或拿到完整链路明确许可的助手)的事。
462
- # 全有全無;冲突(同 id 不同内容)干净地中止。
475
+ # 全有全无;冲突(同 id 不同内容)干净地中止。
463
476
  # 打印 push + gh pr 命令;持有你 Yes 的助手可以一并执行 push/PR。
464
477
  # --branch <name> 先建分支(助手全链路路径)。PR base 默认是当前分支
465
478
  # (记忆 PR 叠在你的工作分支上);--base 可改指到 main 或你的评审分支。
@@ -470,7 +483,7 @@ open-memex pr-status [--apply]
470
483
  # 要求修改 → 只给建议。默认只报告;--apply 在本地执行映射的迁移(不推送)。
471
484
 
472
485
  open-memex pull
473
- # 从 git 远程拉共享记忆:fetch + 純 fast-forward。
486
+ # 从 git 远程拉共享记忆:fetch + 纯 fast-forward。
474
487
  # 分支分叉则明确报错失败——open-memex 永不强制合并;手工解决后再 pull。
475
488
  # 成功后本地索引重新同步。pull 默认显式触发;设置
476
489
  # `open-memex config set sync.autoPull true` 可在 MCP 会话开始时尽力拉一次
@@ -499,7 +512,7 @@ open-memex distill-agents [--scope project|personal] [--type t1,t2] [--limit N]
499
512
 
500
513
  open-memex propose <id...> --to project [--local-approve]
501
514
  # 一次提议一条或多条个人记忆(一个分支、一个 PR);
502
- # 每条复制并生成新 id。全有全無:任何一条 id 出错,整批中止,绝不半途。
515
+ # 每条复制并生成新 id。全有全无:任何一条 id 出错,整批中止,绝不半途。
503
516
  # 把一条个人记忆复制到项目作用域作为评审候选
504
517
  # (是复制不是移动——个人原件保留)。结果落进 outbox;
505
518
  # 准备进仓库时再跑 sync-status / submit。
@@ -544,7 +557,7 @@ npx -y open-memex mcp # 无需安装
544
557
  - **project(项目)**——限定在当前仓库,键由 git origin URL 的 hash 派生(所以同一仓库的多个 clone 共享一个作用域),没有远程时退回到当前工作目录路径。新记忆的默认作用域。
545
558
  - **personal(个人)**——横跨你的所有项目,仅限本机,永不向外同步。放个人偏好用。(v1 里叫 `user`;`migrate --to-v2` 会改名。)
546
559
 
547
- 完整的 scope 模型(键派生、迁移、可见性、保留名)见 [docs/SCOPES.md](./docs/SCOPES.md)。[概念指南](./docs/CONCEPTS.zh-CN.md)把心智模型完整走了一遍。
560
+ 完整的 scope 模型(键派生、迁移、可见性、保留名)见 [docs/SCOPES.md](./docs/SCOPES.md)(英文)。[概念指南](./docs/CONCEPTS.zh-CN.md)把心智模型完整走了一遍。
548
561
 
549
562
  ## 故障排查
550
563
 
@@ -599,7 +612,7 @@ Windows 上已加载的 DLL 是锁定的:如果 open-memex 的 MCP 服务器
599
612
 
600
613
  ## 项目状态
601
614
 
602
- open-memex 是稳定且日常在用的;当前稳定线以 `latest` 发到 npm 上,`alpha` 构建给测试者。发版历史在 [GitHub Releases](https://github.com/stoneskin/open-memex/releases);设计决策记录在只追加的日志 [docs/V2-DESIGN.md](./docs/V2-DESIGN.md) 里。
615
+ open-memex 是稳定且日常在用的;当前稳定线以 `latest` 发到 npm 上,`alpha` 构建给测试者。发版历史在 [GitHub Releases](https://github.com/stoneskin/open-memex/releases);设计决策记录在只追加的日志 [docs/V2-DESIGN.md](./docs/V2-DESIGN.md) 里(英文)。
603
616
 
604
617
  在望的方向(不承诺版本号):更多编辑器的原生助手插件(作为在同一组 MCP 工具之上的增强路径);本地 embedding 作为可选择开启的实验(没经询问,永不下载任何模型);只有在真实多仓库共享、ACL 或合规需求出现时才做 org 层。
605
618
 
@@ -0,0 +1 @@
1
+ f8a813363fbd1433ce149e78defa533d42be7ccbb7f37f1d53be64cd48b90c14
@@ -1,15 +1,27 @@
1
1
  /**
2
- * Scan user text for memory-capture keyword triggers.
2
+ * D67: capture-body length bounds. The lower bound rejects fragments — a
3
+ * trigger that yields "这个" or "OK" is a match on the trigger, not a memory,
4
+ * and a 2-char memory is noise in every future session's injection. The floor
5
+ * stays deliberately above 1: D67 considered lowering it and decided against
6
+ * it. What changed is that a rejection is no longer silent (see KeywordDrop).
7
+ */
8
+ export const MIN_CAPTURE_CHARS = 3;
9
+ export const MAX_CAPTURE_CHARS = 2000;
10
+ /**
11
+ * Scan user text for memory-capture keyword triggers, reporting both what
12
+ * would be captured and what matched but was rejected (D67) — a dropped match
13
+ * that says nothing reads as a broken feature.
3
14
  * Each pattern must have a single capture group whose value becomes the memory body.
4
15
  * Personal patterns (cfg.keywordPersonalPatterns) force the personal scope.
5
16
  *
6
17
  * Personal patterns run first and claim their line: an explicit scope signal
7
18
  * (e.g. "记住我对花粉过敏") must not also fire the generic 记住 into the project scope.
8
19
  */
9
- export function detectKeywords(userText, cfg) {
10
- if (!cfg.keywordCaptureEnabled)
11
- return [];
20
+ export function scanKeywords(userText, cfg) {
12
21
  const hits = [];
22
+ const dropped = [];
23
+ if (!cfg.keywordCaptureEnabled)
24
+ return { hits, dropped };
13
25
  const lines = userText.split(/\r?\n/);
14
26
  const claimed = new Set();
15
27
  const scan = (patterns, personal, skipClaimed) => {
@@ -28,16 +40,37 @@ export function detectKeywords(userText, cfg) {
28
40
  if (!m || !m[1])
29
41
  continue;
30
42
  const content = m[1].trim().replace(/[.!?]$/, "").trim();
31
- if (content.length >= 3 && content.length <= 2000) {
43
+ if (personal)
44
+ claimed.add(i);
45
+ if (content.length >= MIN_CAPTURE_CHARS && content.length <= MAX_CAPTURE_CHARS) {
32
46
  hits.push({ content, pattern: src, personal });
33
- if (personal)
34
- claimed.add(i);
35
47
  break; // one hit per pattern per message
36
48
  }
49
+ // Rejected: record it once per pattern. The line stays claimed for
50
+ // personal patterns even here - "记住我:OK" must not fall through to
51
+ // the generic 记住 and be saved as "我:OK" (D67).
52
+ if (!dropped.some((d) => d.pattern === src && d.content === content)) {
53
+ dropped.push({
54
+ content,
55
+ pattern: src,
56
+ personal,
57
+ reason: content.length < MIN_CAPTURE_CHARS ? "too-short" : "too-long",
58
+ });
59
+ }
60
+ break;
37
61
  }
38
62
  }
39
63
  };
40
64
  scan(cfg.keywordPersonalPatterns, true, false);
41
65
  scan(cfg.keywordPatterns, false, true);
42
- return hits;
66
+ if (cfg.logLevel === "debug" && dropped.length > 0) {
67
+ for (const d of dropped) {
68
+ console.error(`[open-memex] keyword match not captured (${d.reason === "too-short" ? `body ${d.content.length} chars < ${MIN_CAPTURE_CHARS}` : `body ${d.content.length} chars > ${MAX_CAPTURE_CHARS}`}): "${d.content}"`);
69
+ }
70
+ }
71
+ return { hits, dropped };
72
+ }
73
+ /** Capture-only view of {@link scanKeywords} (drops are logged, not returned). */
74
+ export function detectKeywords(userText, cfg) {
75
+ return scanKeywords(userText, cfg).hits;
43
76
  }