open-memex 0.7.0-alpha.6 → 0.7.0-alpha.9
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 +6 -3
- package/CHANGELOG.md +27 -0
- package/README.md +21 -6
- package/README.zh-CN.md +11 -3
- package/dist/.build-stamp +1 -0
- package/dist/capture/keywords.js +41 -8
- package/dist/cli.js +138 -28
- package/dist/config.js +18 -5
- package/dist/distill-agents.js +4 -1
- package/dist/inventory.js +138 -0
- package/dist/mcp.js +9 -0
- package/dist/retrieve/display.js +153 -0
- package/dist/retrieve/search.js +27 -3
- package/dist/submit.js +7 -4
- package/dist/tools/memory.js +8 -3
- package/dist/tools/ops.js +59 -17
- package/docs/V2-DESIGN.md +91 -4
- package/docs/memory-visibility-design.md +130 -0
- package/package.json +2 -2
- package/scripts/build-stamp.mjs +63 -0
- package/scripts/smoke-mcp.ts +31 -1
- package/scripts/smoke-pure.ts +100 -1
- package/scripts/test-full.ts +82 -12
- package/skills/open-memex/SKILL.md +21 -0
- package/src/capture/keywords.ts +56 -6
- package/src/cli.ts +146 -28
- package/src/config.ts +18 -5
- package/src/distill-agents.ts +7 -0
- package/src/inventory.ts +197 -0
- package/src/mcp.ts +9 -0
- package/src/retrieve/display.ts +166 -0
- package/src/retrieve/search.ts +43 -4
- package/src/submit.ts +8 -4
- package/src/tools/memory.ts +8 -3
- package/src/tools/ops.ts +64 -21
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] [--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)
|
|
@@ -157,15 +158,17 @@ AGENTS.md hygiene footer, the MCP handshake, and the tool descriptions.
|
|
|
157
158
|
- 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.
|
|
158
159
|
- Frontmatter is `schema_version: 2`. The SQLite index schema is versioned separately and rebuilds automatically on version change — never hand-edit `index.db`.
|
|
159
160
|
|
|
160
|
-
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.
|
|
161
|
+
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).
|
|
162
|
+
|
|
163
|
+
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.
|
|
161
164
|
|
|
162
165
|
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.
|
|
163
166
|
|
|
164
167
|
## Design constraints — read the frozen design first
|
|
165
168
|
|
|
166
|
-
`docs/V2-DESIGN.md` is the frozen protocol v0.2 (decisions
|
|
169
|
+
`docs/V2-DESIGN.md` is the frozen protocol v0.2 (decisions settled through D68; open questions tracked at the end of the doc). Per its §12:
|
|
167
170
|
AGENTS.md answers "how should AI work here"; the design doc answers "why is it
|
|
168
|
-
built this way" (principles, iron rules,
|
|
171
|
+
built this way" (principles, iron rules, the append-only decision log, currently D1–D68). Before changing
|
|
169
172
|
architecture, scope semantics, lifecycle, or the protocol surface (frontmatter
|
|
170
173
|
schema, MCP tools, CLI contract), read the relevant design section — the decision
|
|
171
174
|
log records what was already considered and rejected.
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,33 @@ the `@alpha` tag are development snapshots and are not listed individually.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## Memory visibility: see it, hide it, delete it (D68)
|
|
13
|
+
|
|
14
|
+
Group 3-C — the surface a user reads to learn what the store actually remembers:
|
|
15
|
+
|
|
16
|
+
- **`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.
|
|
17
|
+
- **`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`.
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
## Keyword capture: what counts as a trigger (D67)
|
|
22
|
+
|
|
23
|
+
The D66 review left two questions open and introduced one regression. Ruled, not deferred:
|
|
24
|
+
|
|
25
|
+
- **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.
|
|
26
|
+
- **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`.
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
|
|
30
|
+
## Review follow-ups (D66)
|
|
31
|
+
|
|
32
|
+
Fixes from the follow-up review of the D63–D65 stack:
|
|
33
|
+
|
|
34
|
+
- 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`.
|
|
35
|
+
- `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.
|
|
36
|
+
- `open-memex status --help` documents that retraction is one-way.
|
|
37
|
+
- The full test suite is Windows-safe now (file:// probe imports, USERPROFILE-aware uninstall test) and refuses to run against a stale `dist/` build.
|
|
38
|
+
|
|
12
39
|
## Keyword capture gaps (D65)
|
|
13
40
|
|
|
14
41
|
The most natural phrasings were the ones that fell through:
|
package/README.md
CHANGED
|
@@ -364,12 +364,18 @@ snippet to paste instead.
|
|
|
364
364
|
Three ways memories get in:
|
|
365
365
|
|
|
366
366
|
- **Keyword triggers** (opencode native plugin only): say `remember …`,
|
|
367
|
-
`note that …`, `don't forget …`, `TIL …`, `save this …` — or in Chinese
|
|
368
|
-
`记住…` /
|
|
367
|
+
`note that …`, `don't forget: …`, `TIL …`, `save this …` — or in Chinese
|
|
368
|
+
`记住…` / `记一下…` / `记录一下…` / `别忘了:…` — and the sentence is captured
|
|
369
369
|
without any tool call. Captures land in the current **project** by default;
|
|
370
|
-
phrases that signal "this is about me" — `remember for me …`,
|
|
371
|
-
`替我记…`, `帮我记…`, `我觉得…`, `我喜欢…` — 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
|
|
372
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.
|
|
373
379
|
- **The agent saves it**: in any editor, ask your agent to remember something
|
|
374
380
|
(or it saves on its own when you state a fact worth keeping) — it calls
|
|
375
381
|
`memory_add`. The routing above is a heuristic; you can always say "save this
|
|
@@ -410,9 +416,9 @@ they only matter once you share memories through Git.
|
|
|
410
416
|
|---|---|
|
|
411
417
|
| ● `memory_add` | Save a fact, preference, decision, note |
|
|
412
418
|
| ● `memory_search` | Keyword search (BM25) across project + personal memories |
|
|
413
|
-
| ● `memory_list` | List memories in a scope (`project`, `personal`, or `both`)
|
|
419
|
+
| ● `memory_list` | List memories in a scope (`project`, `personal`, or `both`) as a numbered inventory; `include=all` is the audit view |
|
|
414
420
|
| ● `memory_supersede` | Replace a memory with a newer version (keeps a supersede chain) |
|
|
415
|
-
| ● `memory_forget` | Delete a memory by id |
|
|
421
|
+
| ● `memory_forget` | Delete a memory by id (`soft=true` hides it instead — retracted, one-way) |
|
|
416
422
|
| `memory_status` | Show the sync queue: outbox drafts, repo review states, uncommitted files |
|
|
417
423
|
| `memory_submit` | Move named drafts into the repo memory dir (local branch + commit) |
|
|
418
424
|
| `memory_propose` | Copy personal memories into the project scope as review candidates |
|
|
@@ -636,9 +642,18 @@ open-memex add "This repo uses better-sqlite3" --type fact
|
|
|
636
642
|
open-memex search "auth flow"
|
|
637
643
|
open-memex search "auth flow" --explain # show FTS expression, scores, lifecycle-hidden counts
|
|
638
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
|
|
639
647
|
open-memex supersede <id> "Updated content"
|
|
640
648
|
open-memex status <id> deprecated
|
|
641
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
|
+
# Personal + current project, outbox drafts in their own section, hidden
|
|
655
|
+
# history counted. Refuses to write a personal-bearing report inside a
|
|
656
|
+
# git working tree unless you pass --allow-personal.
|
|
642
657
|
```
|
|
643
658
|
|
|
644
659
|
Team review workflow (two homes, one per stage):
|
package/README.zh-CN.md
CHANGED
|
@@ -252,7 +252,7 @@ open-memex uninstall --yes
|
|
|
252
252
|
|
|
253
253
|
三种存入方式:
|
|
254
254
|
|
|
255
|
-
- **关键词触发**(仅 opencode 原生插件):说 `remember …`、`note that …`、`don't forget …`、`TIL …`、`save this …`——中文 `记住…` /
|
|
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
257
|
- **检查点提议**:一个任务收尾时,助手会提议 1–3 条从这轮工作里提炼的候选记忆——决定及其理由、约定、踩过的坑、走过又放弃的路——只有你点头的那几条才存。不会有任何东西在你不知情时被写下来。
|
|
258
258
|
- **CLI**:`open-memex add "…"`,可选 `--scope` / `--tag` / `--type` / `--aliases`。
|
|
@@ -275,9 +275,9 @@ MCP 服务器暴露十一个工具;opencode 原生插件暴露其中五个核
|
|
|
275
275
|
|---|---|
|
|
276
276
|
| ● `memory_add` | 保存一条事实、偏好、决策、笔记 |
|
|
277
277
|
| ● `memory_search` | 关键词搜索(BM25),横跨项目 + 个人记忆 |
|
|
278
|
-
| ● `memory_list` |
|
|
278
|
+
| ● `memory_list` | 以编号清单列出某个作用域(`project`、`personal` 或 `both`)的记忆;`include=all` 是审计视图 |
|
|
279
279
|
| ● `memory_supersede` | 用新版本替换一条记忆(保留 supersede 链) |
|
|
280
|
-
| ● `memory_forget` | 按 id
|
|
280
|
+
| ● `memory_forget` | 按 id 删除一条记忆(`soft=true` 改为隐藏——retracted,单向) |
|
|
281
281
|
| `memory_status` | 显示同步队列:outbox 草稿、仓库评审状态、未提交文件 |
|
|
282
282
|
| `memory_submit` | 把指定的草稿移进仓库记忆目录(本地分支 + 提交) |
|
|
283
283
|
| `memory_propose` | 把个人记忆复制到项目作用域,作为评审候选 |
|
|
@@ -442,9 +442,17 @@ open-memex add "This repo uses better-sqlite3" --type fact
|
|
|
442
442
|
open-memex search "auth flow"
|
|
443
443
|
open-memex search "auth flow" --explain # 显示 FTS 表达式、分数、被生命周期藏掉的计数
|
|
444
444
|
open-memex list --scope project
|
|
445
|
+
open-memex list --scope both # 两个作用域各自最新的,各带一个标题
|
|
446
|
+
open-memex list --include all # 审计视图:被替换的版本、已撤回、已归档
|
|
445
447
|
open-memex supersede <id> "Updated content"
|
|
446
448
|
open-memex status <id> deprecated
|
|
447
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
|
+
# 个人 + 当前项目,outbox 草稿单独成节,被藏起的历史只计数。含个人内容的
|
|
455
|
+
# 报告拒绝写进 git 工作树,除非显式 --allow-personal。
|
|
448
456
|
```
|
|
449
457
|
|
|
450
458
|
团队评审工作流(两个家,一个阶段一个):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3a590ae176c625e174a075a2d9068fe443d23c374f70fc7df76809ff10a63ad4
|
package/dist/capture/keywords.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,22 @@ import { redact } from "./redact.js";
|
|
|
17
17
|
import { resolveMcpCommand } from "./init.js";
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
/** True when `target` sits inside a git working tree (D68 guard). */
|
|
23
|
+
function isInsideWorkTree(target) {
|
|
24
|
+
try {
|
|
25
|
+
const dir = path.dirname(path.resolve(target));
|
|
26
|
+
const out = execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
27
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
});
|
|
30
|
+
return out.trim() === "true";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
/** Per-command help, printed by `open-memex <command> --help`.
|
|
22
37
|
AI assistants discover the CLI through --help, so every command needs one. */
|
|
23
38
|
const COMMAND_HELP = {
|
|
@@ -29,12 +44,14 @@ Example:
|
|
|
29
44
|
open-memex where`,
|
|
30
45
|
list: `List memories in a scope, newest first.
|
|
31
46
|
|
|
32
|
-
Usage: open-memex list [--scope project|personal] [--type T] [--limit N]
|
|
47
|
+
Usage: open-memex list [--scope project|personal|both] [--type T] [--limit N] [--include active|all]
|
|
33
48
|
|
|
34
49
|
Flags:
|
|
35
|
-
--scope
|
|
36
|
-
--type
|
|
37
|
-
--limit
|
|
50
|
+
--scope project (default), personal, or both
|
|
51
|
+
--type filter by memory type
|
|
52
|
+
--limit max results
|
|
53
|
+
--include active (default: what is currently remembered) or all (audit
|
|
54
|
+
view: superseded versions, retracted and archived included)
|
|
38
55
|
|
|
39
56
|
Examples:
|
|
40
57
|
open-memex list
|
|
@@ -80,14 +97,20 @@ Example:
|
|
|
80
97
|
|
|
81
98
|
Usage: open-memex status <id> active|deprecated|retracted|archived
|
|
82
99
|
|
|
100
|
+
Retraction is one-way: a retracted memory cannot be set back to active
|
|
101
|
+
(it was withdrawn on purpose). Save the content as a new memory instead.
|
|
102
|
+
|
|
83
103
|
Example:
|
|
84
104
|
open-memex status 01ABC deprecated`,
|
|
85
|
-
forget: `Delete a memory by id.
|
|
105
|
+
forget: `Delete a memory by id. With --soft, hide it instead: the memory
|
|
106
|
+
is retracted (out of lists and search, file kept). Retraction is one-way —
|
|
107
|
+
save the content as a new memory to bring it back.
|
|
86
108
|
|
|
87
|
-
Usage: open-memex forget <id>
|
|
109
|
+
Usage: open-memex forget <id> [--soft]
|
|
88
110
|
|
|
89
111
|
Example:
|
|
90
|
-
open-memex forget 01ABC
|
|
112
|
+
open-memex forget 01ABC
|
|
113
|
+
open-memex forget 01ABC --soft`,
|
|
91
114
|
propose: `Copy personal memories into the project outbox as review drafts.
|
|
92
115
|
The personal originals stay put. Nothing enters git at this step.
|
|
93
116
|
|
|
@@ -134,6 +157,19 @@ Usage: open-memex sync-status
|
|
|
134
157
|
|
|
135
158
|
Example:
|
|
136
159
|
open-memex sync-status`,
|
|
160
|
+
inventory: `Show everything remembered — personal + this project — in the
|
|
161
|
+
open: what each memory says, where it came from, and whether it has reached
|
|
162
|
+
the repo yet. Read-only; deleting stays a confirmed conversation with your
|
|
163
|
+
agent. Formats: text (default, diffable) and json (for agents).
|
|
164
|
+
|
|
165
|
+
Usage: open-memex inventory [--format text|json] [--scope project|personal|both] [--out <path>] [--allow-personal]
|
|
166
|
+
|
|
167
|
+
Default scope: both. Without --out the report goes to stdout. A report that
|
|
168
|
+
includes personal memories is refused inside a git working tree unless
|
|
169
|
+
--allow-personal is passed — personal content must not ride a commit.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
open-memex inventory --format json --out ~/inventory.json`,
|
|
137
173
|
pull: `Pull shared project memories from the git remote: fetch + fast-forward
|
|
138
174
|
only. Never auto-merges — a diverged branch fails with a clear message and is
|
|
139
175
|
left for you to resolve by hand. On success the local index re-syncs.
|
|
@@ -331,16 +367,17 @@ function usage(exitCode = 1) {
|
|
|
331
367
|
|
|
332
368
|
Usage:
|
|
333
369
|
open-memex where
|
|
334
|
-
open-memex list [--scope project|personal] [--type T] [--limit N]
|
|
370
|
+
open-memex list [--scope project|personal|both] [--type T] [--limit N] [--include active|all]
|
|
335
371
|
open-memex search "query" [--scope project|personal|both] [--type T] [--limit N] [--explain]
|
|
336
372
|
open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2]
|
|
337
373
|
open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
338
374
|
open-memex status <id> active|deprecated|retracted|archived
|
|
339
|
-
open-memex forget <id>
|
|
375
|
+
open-memex forget <id> [--soft]
|
|
340
376
|
open-memex propose <id...> --to project [--local-approve]
|
|
341
377
|
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
342
378
|
open-memex resolve [id-or-path]
|
|
343
379
|
open-memex sync-status
|
|
380
|
+
open-memex inventory [--format text|json] [--scope project|personal|both] [--out <path>] [--allow-personal]
|
|
344
381
|
open-memex pull
|
|
345
382
|
open-memex push
|
|
346
383
|
open-memex export [--scope project|personal|both] [--type T] [--tag t] [--all] [-o <file>]
|
|
@@ -720,12 +757,11 @@ async function main() {
|
|
|
720
757
|
process.exit(1);
|
|
721
758
|
}
|
|
722
759
|
const cfg = loadConfig();
|
|
723
|
-
const {
|
|
760
|
+
const { scanKeywords, MIN_CAPTURE_CHARS, MAX_CAPTURE_CHARS } = await import("./capture/keywords.js");
|
|
724
761
|
const text = rest.slice(1).join(" ");
|
|
725
|
-
const hits =
|
|
762
|
+
const { hits, dropped } = scanKeywords(text, cfg);
|
|
726
763
|
if (hits.length === 0) {
|
|
727
|
-
console.log("no keyword triggers
|
|
728
|
-
return;
|
|
764
|
+
console.log("no keyword triggers - nothing would be captured.");
|
|
729
765
|
}
|
|
730
766
|
for (const h of hits) {
|
|
731
767
|
const r = redact(h.content, cfg.redactPatterns);
|
|
@@ -734,6 +770,13 @@ async function main() {
|
|
|
734
770
|
console.log(` secret hit: ${r.hadSecret ? `yes (${r.matchedPattern}) — will be masked` : "no"}`);
|
|
735
771
|
console.log(` body: ${r.content.slice(0, 160)}${r.content.length > 160 ? "…" : ""}`);
|
|
736
772
|
}
|
|
773
|
+
// D67: a trigger that matched but produced a fragment is reported, not
|
|
774
|
+
// silently dropped - otherwise "记住:这个" just looks broken.
|
|
775
|
+
for (const d of dropped) {
|
|
776
|
+
console.log(d.reason === "too-short"
|
|
777
|
+
? `! matched but NOT captured: body "${d.content}" is ${d.content.length} chars (floor: ${MIN_CAPTURE_CHARS}). A trigger alone isn't a memory - write the fact: "记住:<fact>".`
|
|
778
|
+
: `! matched but NOT captured: body is ${d.content.length} chars (ceiling: ${MAX_CAPTURE_CHARS}) - save it with memory_add instead.`);
|
|
779
|
+
}
|
|
737
780
|
return;
|
|
738
781
|
}
|
|
739
782
|
// `mcp` starts the stdio MCP server (11 tools; the opencode plugin exposes 5).
|
|
@@ -767,20 +810,27 @@ async function main() {
|
|
|
767
810
|
}
|
|
768
811
|
if (cmd === "list") {
|
|
769
812
|
const flags = parseFlags(rest);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
813
|
+
// D66: --scope both, on par with the memory_list tool — prints each
|
|
814
|
+
// scope's newest under its own header.
|
|
815
|
+
const scopes = flags.scope === "both" ? [project, PERSONAL_SCOPE] : [resolveCliScope(flags, project)];
|
|
816
|
+
for (const s of scopes) {
|
|
817
|
+
syncScope(s.key, "cli");
|
|
818
|
+
const hits = list(s.key, {
|
|
819
|
+
type: flags.type,
|
|
820
|
+
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
821
|
+
include: flags.include === "all" ? "all" : "active",
|
|
822
|
+
});
|
|
823
|
+
if (scopes.length > 1)
|
|
824
|
+
console.log(`## ${s.kind} (${s.key})`);
|
|
825
|
+
if (hits.length === 0) {
|
|
826
|
+
console.log(`(no memories in ${s.key})`);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
for (const h of hits) {
|
|
830
|
+
const dep = h.status !== "active" ? ` [${h.status}]` : "";
|
|
831
|
+
const rev = hitStateLabel(h);
|
|
832
|
+
console.log(`[${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
833
|
+
}
|
|
784
834
|
}
|
|
785
835
|
return;
|
|
786
836
|
}
|
|
@@ -949,6 +999,20 @@ async function main() {
|
|
|
949
999
|
const id = rest[0];
|
|
950
1000
|
if (!id)
|
|
951
1001
|
usage();
|
|
1002
|
+
// D68: --soft hides (retracts) instead of deleting — same action as
|
|
1003
|
+
// the memory_forget tool's soft flag. One-way (D64).
|
|
1004
|
+
if (rest.includes("--soft")) {
|
|
1005
|
+
try {
|
|
1006
|
+
const mf = setStatus(id, "retracted");
|
|
1007
|
+
upsertFromFile(mf);
|
|
1008
|
+
console.log(`hidden ${id} (retracted — out of lists and search; save it again as a new memory to bring it back)`);
|
|
1009
|
+
}
|
|
1010
|
+
catch (e) {
|
|
1011
|
+
console.error(`hide failed: ${e.message}`);
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
952
1016
|
const row = db().prepare(`SELECT file_path FROM memories WHERE id = ?`).get(id);
|
|
953
1017
|
if (!row) {
|
|
954
1018
|
console.error("not found");
|
|
@@ -1086,11 +1150,57 @@ async function main() {
|
|
|
1086
1150
|
}
|
|
1087
1151
|
return;
|
|
1088
1152
|
}
|
|
1153
|
+
if (cmd === "inventory") {
|
|
1154
|
+
// D68: refresh both scopes first so the report matches the markdown
|
|
1155
|
+
// on disk. Default scope: personal + this project.
|
|
1156
|
+
syncScope(project.key, "cli");
|
|
1157
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
1158
|
+
const flags = parseFlags(rest);
|
|
1159
|
+
const format = flags.format ?? "text";
|
|
1160
|
+
if (format !== "text" && format !== "json") {
|
|
1161
|
+
console.error(`inventory: unknown --format '${format}' (text|json; html arrives separately)`);
|
|
1162
|
+
process.exit(2);
|
|
1163
|
+
}
|
|
1164
|
+
const scopeKeys = flags.scope === "personal"
|
|
1165
|
+
? [PERSONAL_SCOPE.key]
|
|
1166
|
+
: flags.scope === "project"
|
|
1167
|
+
? [project.key]
|
|
1168
|
+
: [PERSONAL_SCOPE.key, project.key];
|
|
1169
|
+
const outPath = flags["out"] ?? flags["output"];
|
|
1170
|
+
// D68/3-C v2: the report contains personal content. Writing it into
|
|
1171
|
+
// a git working tree would ride the next commit off this machine —
|
|
1172
|
+
// refuse unless the user explicitly accepts that.
|
|
1173
|
+
if (outPath &&
|
|
1174
|
+
scopeKeys.includes(PERSONAL_SCOPE.key) &&
|
|
1175
|
+
flags["allow-personal"] !== "true" &&
|
|
1176
|
+
isInsideWorkTree(outPath)) {
|
|
1177
|
+
console.error(`inventory: ${outPath} is inside a git working tree and this report includes personal memories. ` +
|
|
1178
|
+
`Refusing to write it there. Pass --allow-personal if you really want that, or write outside the repo.`);
|
|
1179
|
+
process.exit(2);
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
const { loadInventory, renderInventoryText, renderInventoryJson } = await import("./inventory.js");
|
|
1183
|
+
const data = loadInventory(scopeKeys);
|
|
1184
|
+
const rendered = format === "json" ? renderInventoryJson(data) : renderInventoryText(data);
|
|
1185
|
+
if (outPath) {
|
|
1186
|
+
fs.writeFileSync(outPath, rendered, "utf8");
|
|
1187
|
+
console.log(`inventory: ${data.total} memories → ${outPath}`);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
process.stdout.write(rendered);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
catch (e) {
|
|
1194
|
+
console.error(`inventory failed: ${e.message}`);
|
|
1195
|
+
process.exit(2);
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1089
1199
|
if (cmd === "sync-status") {
|
|
1090
1200
|
syncScope(project.key, "cli");
|
|
1091
1201
|
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
1092
1202
|
try {
|
|
1093
|
-
console.log(formatSyncStatus(getSyncStatus()));
|
|
1203
|
+
console.log(formatSyncStatus(getSyncStatus(), { cap: Number.POSITIVE_INFINITY }));
|
|
1094
1204
|
}
|
|
1095
1205
|
catch (e) {
|
|
1096
1206
|
console.error(`sync-status failed: ${e.message}`);
|
package/dist/config.js
CHANGED
|
@@ -9,25 +9,38 @@ export const DEFAULT_CONFIG = {
|
|
|
9
9
|
captureAliases: true,
|
|
10
10
|
keywordPatterns: [
|
|
11
11
|
"^\\s*remember(?!\\s+for\\s+me)(?:\\s+that|\\s+to)?[:,]?\\s+(.+)$",
|
|
12
|
-
"^\\s*(?:please\\s+)?
|
|
12
|
+
"^\\s*(?:please\\s+)?note(?:\\s+that)?[:,]?\\s+(.+)$",
|
|
13
|
+
// D67: "don't forget the wifi password" is narration addressed to the
|
|
14
|
+
// agent, not a statement to the store. An explicit marker - "that" or a
|
|
15
|
+
// colon - makes it an instruction ("don't forget that ..." / "don't
|
|
16
|
+
// forget: ..."). Mirrors 别忘了 below; see D67.
|
|
17
|
+
"^\\s*(?:please\\s+)?don'?t\\s+forget(?:\\s+that\\s+|[:,]\\s+)(.+)$",
|
|
13
18
|
"^\\s*TIL[:,]?\\s+(.+)$",
|
|
14
19
|
"^\\s*save\\s+(?:this|to\\s+memory)[:,]?\\s+(.+)$",
|
|
15
20
|
// Chinese equivalents
|
|
16
21
|
"^\\s*(?:请)?记住(?!(个人))[::,,]?\\s*(.+)$",
|
|
17
22
|
"^\\s*(?:请)?(?:记一下|记录一下)[::,,]?\\s*(.+)$",
|
|
18
|
-
"
|
|
23
|
+
// D67: separator required. "别忘了带伞" is narration ("don't forget your
|
|
24
|
+
// umbrella"); "别忘了:周五不发布" is an instruction to the store. Same
|
|
25
|
+
// rule as the English "don't forget" above.
|
|
26
|
+
"^\\s*别忘了[::,,]\\s*(.+)$",
|
|
19
27
|
// First-person plural: team/project context, NOT personal
|
|
20
28
|
"^\\s*我们认为[::,,]?\\s*(.+)$",
|
|
21
29
|
"^\\s*我们决定[::,,]?\\s*(.+)$",
|
|
22
|
-
|
|
30
|
+
// D67: the boundary applies to the BARE verb only. 记 + (住|录) + 一下 is a
|
|
31
|
+
// complete verb phrase, so the noun may follow directly ("帮我记住这个配置",
|
|
32
|
+
// "记一下这个约定"); only a bare 记 needs a separator, otherwise 帮我记得…
|
|
33
|
+
// would capture mid-word garbage. See D67.
|
|
34
|
+
"^\\s*(?:请)?帮我们记(?:(?:住|录)(?!得|着)(?:一下)?|一下|(?=[::,,\\s]|$))[::,,]?\\s*(.+)$",
|
|
23
35
|
],
|
|
24
36
|
keywordPersonalPatterns: [
|
|
25
37
|
"^\\s*remember\\s+for\\s+me(?:\\s+that)?[:,]?\\s+(.+)$",
|
|
26
38
|
"^\\s*(?:请)?记住(个人)[::,,]?\\s*(.+)$",
|
|
27
39
|
// First-person singular: personal scope ("我" → 个人, "我们" → 项目)
|
|
28
40
|
"^\\s*(?:请)?记住我(?!们)[::,,]?\\s*(.+)$",
|
|
29
|
-
"^\\s
|
|
30
|
-
"^\\s
|
|
41
|
+
"^\\s*(?:请)?替我记(?:(?:住|录)(?!得|着)(?:一下)?|一下|(?=[::,,\\s]|$))[::,,]?\\s*(.+)$",
|
|
42
|
+
"^\\s*(?:请)?帮我记(?:(?:住|录)(?!得|着)(?:一下)?|一下|(?=[::,,\\s]|$))[::,,]?\\s*(.+)$",
|
|
43
|
+
"^\\s*help\\s+me\\s+remember(?:\\s+that)?[:,]?\\s+(.+)$",
|
|
31
44
|
"^\\s*我觉得[::,,]?\\s*(.+)$",
|
|
32
45
|
"^\\s*我喜欢[::,,]?\\s*(.+)$",
|
|
33
46
|
],
|
package/dist/distill-agents.js
CHANGED
|
@@ -62,7 +62,10 @@ export function distillAgentsMarkdown(opts) {
|
|
|
62
62
|
// inferred captures source: "inference"; this footer omitted it, so
|
|
63
63
|
// opencode agents saved distilled conclusions as if the user had
|
|
64
64
|
// stated them ("tool"). Keep the rubric one voice across surfaces.
|
|
65
|
-
`- Mark captures you infer (rather than the user stating) with`, ` \`source: "inference"\` — provenance matters more than polish
|
|
65
|
+
`- Mark captures you infer (rather than the user stating) with`, ` \`source: "inference"\` — provenance matters more than polish.`,
|
|
66
|
+
// D68: same voice for the visibility surface — opencode agents reading
|
|
67
|
+
// this footer never see the MCP handshake either.
|
|
68
|
+
`- When the user asks what you remember, list it conversationally`, ` (\`memory_list\` with scope both, or \`open-memex inventory\`) — plain`, ` words, not raw ids. Before deleting an entry they point at, read it`, ` back in full and confirm; deletion is permanent. Offer hiding`, ` (\`memory_forget\` soft / \`forget --soft\`) when they hesitate.`);
|
|
66
69
|
lines.push(``);
|
|
67
70
|
return lines.join("\n");
|
|
68
71
|
}
|