@meitouai/mem-note-cli 0.0.3 → 0.1.0
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/bin/conventions.md +44 -289
- package/bin/groom-rules.md +43 -0
- package/bin/mem-note.mjs +63 -45
- package/package.json +3 -2
package/bin/conventions.md
CHANGED
|
@@ -1,346 +1,101 @@
|
|
|
1
1
|
# mem-note 数据约定
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 本文件由插件管理,`mem-note init` 和插件升级时会覆盖写入,**请勿手动修改**。
|
|
4
|
+
> 这里定义的数据格式是硬约束,CLI 和 AI agent 都必须遵守。
|
|
4
5
|
>
|
|
5
|
-
>
|
|
6
|
+
> 如何 groom inbox 数据(triage 策略、合并判断、洞察提取等)见 [GROOM_RULES.md](GROOM_RULES.md),该文件归用户所有,可自由迭代。
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
1. **文件即数据库**。所有数据是 markdown 文件,无隐藏索引 / 数据库 / 二进制状态。
|
|
10
|
-
2. **CLI 最小化**。CLI 只覆盖 5 个场景:(a) hook 时 AI 不在场、(b) 人需要快捷入口、(c) 多步易错操作的封装、(d) 跨会话的一致性检查、(e) 初始化。其他所有读写由 AI agent 用原生工具(`Read` / `Write` / `Edit` / `Glob` / `Grep` / `Bash`)完成。
|
|
11
|
-
3. **AI agent 负责智能**。语义判断、矛盾识别、重复检测、groom 决策全部由 skill 里的 AI agent 完成,CLI 不包含任何智能逻辑。
|
|
12
|
-
4. **约定优于配置**。本文档定义的格式是硬约束;漂移由 `mem-note doctor` 发现。
|
|
13
|
-
|
|
14
|
-
## 2. 目录布局
|
|
8
|
+
## 目录布局
|
|
15
9
|
|
|
16
10
|
```
|
|
17
11
|
~/.aibox-mem-note/ # 数据根(可由 $MEM_NOTE_DATA_DIR 覆盖)
|
|
18
|
-
├── CONVENTIONS.md #
|
|
12
|
+
├── CONVENTIONS.md # 本文件(插件管理,自动同步)
|
|
13
|
+
├── GROOM_RULES.md # groom 策略(用户所有,可自由修改)
|
|
19
14
|
├── notes/ # 永久 note
|
|
20
15
|
│ ├── NOTES.md # 根索引
|
|
21
16
|
│ ├── <topic>/
|
|
22
17
|
│ │ ├── NOTES.md # 子目录索引(必维护)
|
|
23
|
-
│ │ └── <slug>.md
|
|
24
|
-
│ ├── skills/ # 用户自定义 skill
|
|
25
|
-
│
|
|
26
|
-
│ │ ├── SKILL.md # skill 本体(frontmatter: name, description)
|
|
27
|
-
│ │ └── evals/ # 可选:该 skill 的 eval workspace
|
|
28
|
-
│ └── _archived/ # 归档的 note
|
|
18
|
+
│ │ └── <slug>.md
|
|
19
|
+
│ ├── skills/ # 用户自定义 skill(非 note,groom 跳过)
|
|
20
|
+
│ └── _archived/ # 归档 note(groom 排除)
|
|
29
21
|
│ └── <YYYY-MM-DD>_<slug>.md
|
|
30
|
-
├── inbox/ # 待 groom
|
|
22
|
+
├── inbox/ # 待 groom 的原始输入
|
|
31
23
|
│ ├── <YYYY-MM-DD-HHMMSS>_<slug>.md
|
|
32
|
-
│ └── .processed/ # 已处理的 entry
|
|
33
|
-
├── insights/ # groom
|
|
24
|
+
│ └── .processed/ # 已处理的 entry
|
|
25
|
+
├── insights/ # groom 产出的洞察
|
|
34
26
|
│ └── <id>.md
|
|
35
|
-
├── .context/
|
|
36
|
-
│ └── index.md #
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
└── .
|
|
40
|
-
└── <snapshot-id>/
|
|
27
|
+
├── .context/
|
|
28
|
+
│ └── index.md # groom 编译的会话注入上下文(< 2KB)
|
|
29
|
+
└── .reports/ # 审计日志(append-only)
|
|
30
|
+
├── <YYYY-MM-DD>.md
|
|
31
|
+
└── graph.html # 知识图谱可视化(groom 生成)
|
|
41
32
|
```
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `notes/_archived/` **不**参与任何 Glob scanning;AI agent 应显式排除
|
|
46
|
-
- `notes/**/NOTES.md` 不是 note,AI agent 应显式排除
|
|
47
|
-
- `notes/skills/` 是**保留子目录**,里面存的是用户自定义 skill 而不是 note:
|
|
48
|
-
- groom 不要 triage 它(不当作 inbox 输出目标,不更新 NOTES.md 条目)
|
|
49
|
-
- 任何 `Glob notes/**/*.md` 应显式排除 `notes/skills/**` 以免混入 SKILL.md
|
|
50
|
-
- 发现 user skill 时用 `Glob notes/skills/*/SKILL.md`,读 frontmatter 的 `name` + `description` 即可
|
|
51
|
-
- `inbox/.processed/` 是软删除区,不参与 pending 统计
|
|
34
|
+
**排除规则**:`_archived/`、`skills/`、`NOTES.md` 不是 note,Glob 时排除。
|
|
52
35
|
|
|
53
|
-
##
|
|
36
|
+
## Frontmatter Schema
|
|
54
37
|
|
|
55
|
-
|
|
56
|
-
`notes/skills/**/SKILL.md`(遵循 Claude Code skill 自有格式)外,
|
|
57
|
-
所有 `.md` 文件必须有 YAML frontmatter。
|
|
58
|
-
|
|
59
|
-
### 3.1 Note (`notes/**/*.md`,除 NOTES.md)
|
|
38
|
+
### Note (`notes/<topic>/<slug>.md`)
|
|
60
39
|
|
|
61
40
|
```yaml
|
|
62
41
|
---
|
|
63
|
-
type: preference
|
|
64
|
-
title: Dark mode
|
|
65
|
-
description: >
|
|
66
|
-
Prefers dark mode across all editors
|
|
67
|
-
tags: [ui, editor]
|
|
42
|
+
type: preference # 必填:preference | fact | lesson | reminder | note
|
|
43
|
+
title: Dark mode # 必填:人类可读标题
|
|
44
|
+
description: > # 必填:一句话摘要
|
|
45
|
+
Prefers dark mode across all editors.
|
|
46
|
+
tags: [ui, editor] # 必填(可空数组):小写 kebab-case
|
|
68
47
|
updated: 2026-04-10 # 必填:ISO 日期
|
|
69
|
-
related: # 可选:相关 note 路径
|
|
70
|
-
|
|
71
|
-
source: # 可选:原始来源 inbox entry 路径
|
|
72
|
-
- inbox/2026-04-09-142030_dark-mode.md
|
|
48
|
+
related: [] # 可选:相关 note 路径
|
|
49
|
+
source: [] # 可选:来源 inbox entry 路径
|
|
73
50
|
---
|
|
74
51
|
```
|
|
75
52
|
|
|
76
|
-
###
|
|
53
|
+
### Inbox entry (`inbox/<YYYY-MM-DD-HHMMSS>_<slug>.md`)
|
|
77
54
|
|
|
78
55
|
```yaml
|
|
79
56
|
---
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
source:
|
|
84
|
-
terminal # 可选但推荐:去重 key
|
|
85
|
-
# 取值:terminal | hook:stop | hook:sync | auto-memory:<path>
|
|
57
|
+
created: 2026-04-10-142030 # 必填
|
|
58
|
+
type: note # 可选:初步判断
|
|
59
|
+
tags: [] # 可选
|
|
60
|
+
source: terminal # 可选:去重 key(terminal | hook:stop | hook:sync | auto-memory:<path>)
|
|
86
61
|
---
|
|
87
62
|
```
|
|
88
63
|
|
|
89
|
-
|
|
90
|
-
- `source`:`mem-note save --source X` 会拒绝 source 已存在的导入(去重)。
|
|
91
|
-
- 顶层 `inbox/*.md` 保持原始输入语义;当条目被移动到 `inbox/.processed/` 后,
|
|
92
|
-
处理后的副本**可以**追加一个 `processed` 对象记录处理结果,例如:
|
|
93
|
-
|
|
94
|
-
```yaml
|
|
95
|
-
processed:
|
|
96
|
-
status: user-ignored
|
|
97
|
-
at: 2026-04-11-090807
|
|
98
|
-
reason: Ignored by user from the mem-note inbox UI.
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 3.3 Insight (`insights/*.md`)
|
|
64
|
+
### Insight (`insights/<id>.md`)
|
|
102
65
|
|
|
103
66
|
```yaml
|
|
104
67
|
---
|
|
105
|
-
id: insight-v1K2p7
|
|
106
|
-
kind: clarification
|
|
107
|
-
status: open
|
|
68
|
+
id: insight-v1K2p7 # 必填:短 nanoid
|
|
69
|
+
kind: clarification # 必填:clarification | pattern | skill-candidate | contradiction
|
|
70
|
+
status: open # 必填:open | resolved | dismissed
|
|
108
71
|
title: Testing framework contradiction
|
|
109
72
|
created: 2026-04-10
|
|
110
|
-
related:
|
|
111
|
-
- notes/user/testing-jest.md
|
|
112
|
-
- notes/user/testing-vitest.md
|
|
113
|
-
---
|
|
114
|
-
<自由 markdown 正文:evidence、reasoning、建议行动>
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### 3.4 Archived note (`notes/_archived/**/*.md`)
|
|
118
|
-
|
|
119
|
-
与原 note frontmatter 相同,追加一个 `archived` 字段:
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
---
|
|
123
|
-
type: fact
|
|
124
|
-
title: Old deployment tool
|
|
125
|
-
---
|
|
126
|
-
archived: 2026-04-10 # 归档日期
|
|
73
|
+
related: []
|
|
127
74
|
---
|
|
128
75
|
```
|
|
129
76
|
|
|
130
|
-
|
|
77
|
+
### Archived note (`notes/_archived/<YYYY-MM-DD>_<slug>.md`)
|
|
131
78
|
|
|
132
|
-
|
|
133
|
-
| ------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
|
134
|
-
| Note | `notes/<topic>/<slug>.md` | `notes/user/dark-mode.md` |
|
|
135
|
-
| Inbox entry | `inbox/<YYYY-MM-DD-HHMMSS>_<slug>.md` (collisions within the same second get a `-<n>` suffix before `.md`) | `inbox/2026-04-10-142030_dark-mode.md` |
|
|
136
|
-
| Archived note | `notes/_archived/<YYYY-MM-DD>_<original-slug>.md` | `notes/_archived/2026-04-10_dark-mode.md` |
|
|
137
|
-
| Insight | `insights/<id>.md` | `insights/insight-v1K2p7.md` |
|
|
79
|
+
同 note frontmatter,追加 `archived: 2026-04-10`。
|
|
138
80
|
|
|
139
|
-
|
|
81
|
+
## 文件命名
|
|
140
82
|
|
|
141
|
-
|
|
83
|
+
Slug:小写 kebab-case,只含 `[a-z0-9-]`,最长 60 字符。
|
|
142
84
|
|
|
143
|
-
|
|
85
|
+
## NOTES.md 索引
|
|
144
86
|
|
|
145
|
-
|
|
87
|
+
每个 `notes/` 子目录维护 `NOTES.md`:
|
|
146
88
|
|
|
147
89
|
```markdown
|
|
148
90
|
# notes/user
|
|
149
91
|
|
|
150
|
-
User-scope preferences, facts, and lessons.
|
|
151
|
-
|
|
152
92
|
## Notes
|
|
153
93
|
|
|
154
94
|
- [Dark mode preference](dark-mode.md) — Prefers dark mode, Monokai Pro theme.
|
|
155
|
-
- [Deploy lesson](deploy-lesson.md) — Always run DB migrations before deploying.
|
|
156
95
|
|
|
157
96
|
## Subdirectories
|
|
158
97
|
|
|
159
98
|
- [editor/](editor/NOTES.md) — Editor-specific preferences
|
|
160
99
|
```
|
|
161
100
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
**任何创建、重命名、归档 note 的代理都必须同步更新对应目录的 `NOTES.md`**:
|
|
165
|
-
|
|
166
|
-
- 创建新 note → `## Notes` 追加 `- [<title>](<file>.md) — <description>`
|
|
167
|
-
- 更新 note 的 title 或 description → 更新对应行
|
|
168
|
-
- 归档 note → 从 `## Notes` 删除该行
|
|
169
|
-
- 创建子目录 → `## Subdirectories` 追加条目
|
|
170
|
-
- 删除空子目录 → 从 `## Subdirectories` 删除条目
|
|
171
|
-
|
|
172
|
-
`<title>` 和 `<description>` 直接来自 note 的 frontmatter 同名字段。
|
|
173
|
-
|
|
174
|
-
### 5.3 为什么不自动维护
|
|
175
|
-
|
|
176
|
-
- NOTES.md 就地可读(人 / AI agent / git diff 都一目了然)
|
|
177
|
-
- 避免"CLI 自动维护 + AI agent 手动维护"的 race
|
|
178
|
-
- `mem-note doctor` 负责定期检查漂移,不靠运行时
|
|
179
|
-
|
|
180
|
-
## 6. `.context/index.md` 约定
|
|
181
|
-
|
|
182
|
-
`.context/index.md` 是 **groom skill 编译**出的"本会话 AI agent 应记住的核心信息"。
|
|
183
|
-
SessionStart 和 PreCompact hook 都会注入它。
|
|
184
|
-
|
|
185
|
-
### 6.1 结构
|
|
186
|
-
|
|
187
|
-
```markdown
|
|
188
|
-
# Context — 2026-04-10
|
|
189
|
-
|
|
190
|
-
## Preferences
|
|
191
|
-
|
|
192
|
-
- Dark mode, Monokai Pro, JetBrains Mono
|
|
193
|
-
- Functional style, named exports, files under 200 lines
|
|
194
|
-
|
|
195
|
-
## Active Reminders
|
|
196
|
-
|
|
197
|
-
- API design review deadline: April 15
|
|
198
|
-
- Tuesday 10am standup — prepare notes
|
|
199
|
-
|
|
200
|
-
## Key Facts
|
|
201
|
-
|
|
202
|
-
- Uses PostgreSQL 15 with PgBouncer
|
|
203
|
-
- Tests: Jest (unit) + Playwright (e2e), 80% coverage
|
|
204
|
-
- Planning REST → GraphQL migration (Apollo Server) in Q3
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### 6.2 约束
|
|
208
|
-
|
|
209
|
-
- `## Active Reminders` 是**必须**段落(可以为空)。groom skill 必须在每次
|
|
210
|
-
编译时扫描所有 `type: reminder` 的 note 重新生成这一段 —— 因为 PreCompact
|
|
211
|
-
hook 依赖它。
|
|
212
|
-
- 整体文件大小建议 **< 2KB**。SessionStart 和 PreCompact 每次都注入,太大
|
|
213
|
-
会吃掉宝贵的 session context。
|
|
214
|
-
- 由 groom skill 的最后一步 `Write` 生成,**不**由 CLI 维护。
|
|
215
|
-
|
|
216
|
-
## 7. Inbox / Archive 生命周期
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
mem-note save
|
|
220
|
-
│
|
|
221
|
-
▼
|
|
222
|
-
inbox/<ts>_<slug>.md ────── groom skill reads ──┐
|
|
223
|
-
│
|
|
224
|
-
┌────────────────────┤
|
|
225
|
-
│ │
|
|
226
|
-
merge discard
|
|
227
|
-
│ │
|
|
228
|
-
▼ ▼
|
|
229
|
-
notes/<topic>/<slug>.md inbox/.processed/<original>.md
|
|
230
|
-
(+ update NOTES.md) (软删除)
|
|
231
|
-
|
|
232
|
-
mem-note archive notes/<topic>/<slug>.md
|
|
233
|
-
│
|
|
234
|
-
▼
|
|
235
|
-
notes/_archived/<date>_<slug>.md (+ remove from topic's NOTES.md)
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
`.processed/` 保留所有历史 inbox entry,便于回溯;不再在 listing 中出现。
|
|
239
|
-
|
|
240
|
-
## 8. AI agent 的工具使用优先级
|
|
241
|
-
|
|
242
|
-
AI agent 在 skill 会话中处理数据时,**优先使用原生文件工具**。
|
|
243
|
-
CLI 只在下表标注的少数场景调用。
|
|
244
|
-
|
|
245
|
-
| 操作 | 首选 | 不要用 |
|
|
246
|
-
| ------------------------ | ------------------------------------------------------------------------ | ---------------------------------- |
|
|
247
|
-
| 列出所有 note | `Glob notes/**/*.md`(记得排除 `_archived/**`、`skills/**`、`NOTES.md`) | ❌ `mem-note list-notes`(不存在) |
|
|
248
|
-
| 发现用户 skill | `Glob notes/skills/*/SKILL.md` + 读 frontmatter | ❌ 任何 CLI |
|
|
249
|
-
| 读一个 note | `Read notes/path/foo.md` | ❌ CLI |
|
|
250
|
-
| 读某目录的索引视图 | `Read notes/<topic>/NOTES.md` | ❌ CLI |
|
|
251
|
-
| 写 / 更新 note | `Write` / `Edit` + 记得同步 `NOTES.md` | ❌ CLI |
|
|
252
|
-
| 搜索 note 内容 | `Grep "query" notes/**/*.md` | ❌ CLI |
|
|
253
|
-
| 处理 inbox entry | `Bash mv inbox/x.md inbox/.processed/` | ❌ CLI |
|
|
254
|
-
| 写 insight | `Write insights/<id>.md` | ❌ CLI |
|
|
255
|
-
| 编译 `.context/index.md` | `Write .context/index.md` | ❌ CLI |
|
|
256
|
-
| **归档 note** | ✅ `mem-note archive <path>`(封装多步操作) | `mv` + 手动更新 NOTES.md(易漏) |
|
|
257
|
-
| **检查一致性** | ✅ `mem-note doctor`(每次 groom 末尾) | 手动检查(不现实) |
|
|
258
|
-
| 追加 audit log | `Bash` 追加 `.reports/<date>.md` 或最后一次 `Write` | — |
|
|
259
|
-
|
|
260
|
-
## 9. CLI 命令清单(5 个)
|
|
261
|
-
|
|
262
|
-
| # | 命令 | 职责 | 典型调用方 |
|
|
263
|
-
| --- | ------------------------- | ------------------------------------------------------ | ---------------------------------- |
|
|
264
|
-
| 1 | `mem-note init` | 幂等创建目录 + 写入 `CONVENTIONS.md`(已存在则跳过) | 人(首次安装) |
|
|
265
|
-
| 2 | `mem-note save <content>` | 存一条 inbox entry。支持 `--type`、`--source`、stdin | 人 / hook / 外部脚本 |
|
|
266
|
-
| 3 | `mem-note archive <path>` | 封装 "加日期前缀 + mv 到 `_archived/` + 更新 NOTES.md" | 人 / AI agent |
|
|
267
|
-
| 4 | `mem-note doctor` | 检查 frontmatter schema、NOTES.md 漂移、归档命名一致性 | 人 / AI agent(每次 groom 结束时) |
|
|
268
|
-
| 5 | `mem-note quick-context` | 输出 `.context/index.md` 内容(封装路径解析) | hook (SessionStart / PreCompact) |
|
|
269
|
-
|
|
270
|
-
其他一切都是 AI agent / 人用原生工具直接做。
|
|
271
|
-
|
|
272
|
-
## 10. Groom Skill 默认工作流
|
|
273
|
-
|
|
274
|
-
作为参考,groom skill 按以下顺序处理:
|
|
275
|
-
|
|
276
|
-
1. `Read CONVENTIONS.md` —— 加载约定(若当前会话已加载可跳过)
|
|
277
|
-
2. `Read notes/NOTES.md` + 感兴趣子目录的 `NOTES.md` —— 拿目录视图
|
|
278
|
-
3. `Glob inbox/*.md` —— 列 pending inbox entries
|
|
279
|
-
4. `Read` 每个 inbox entry
|
|
280
|
-
5. 对每条 entry 决策 **create** / **merge** / **discard**:
|
|
281
|
-
- **create**:`Write notes/<topic>/<slug>.md` + `Edit notes/<topic>/NOTES.md`
|
|
282
|
-
- **merge**:`Edit notes/<topic>/existing.md` 合入新信息 + 更新 NOTES.md 的 description
|
|
283
|
-
- **discard**:无动作
|
|
284
|
-
6. 所有情况都 `Bash mv inbox/<entry> inbox/.processed/`
|
|
285
|
-
7. 如有冲突 / 过期 note,`mem-note archive <path>` 归档
|
|
286
|
-
8. 如发现矛盾 / 模式,`Write insights/<id>.md`
|
|
287
|
-
9. 重新编译 `.context/index.md`:`Write .context/index.md` 包含 Preferences / **Active Reminders** / Key Facts
|
|
288
|
-
10. `Bash mem-note doctor` 检查一致性;有警告则修正
|
|
289
|
-
11. `Bash` 追加一段 audit 到 `.reports/<YYYY-MM-DD>.md`
|
|
290
|
-
|
|
291
|
-
## 11. Git 集成(可选但推荐)
|
|
292
|
-
|
|
293
|
-
`mem-note init` 会检测 git 是否可用,可用则在数据根执行 `git init`、写入
|
|
294
|
-
`.gitignore`、并做一次初始 commit。之后:
|
|
295
|
-
|
|
296
|
-
- `mem-note save` / `mem-note archive` 每次成功写入后自 commit,message 形如
|
|
297
|
-
`save: <slug>` / `archive: <slug>`。
|
|
298
|
-
- 插件的 Stop hook (`session-commit.js`) 在每次 Claude Code 会话结束时兜底
|
|
299
|
-
commit 剩余未提交的改动,message 形如 `session: <ISO timestamp>`,
|
|
300
|
-
用于捕获 skill 通过 `Write`/`Edit`/`Bash mv` 直接做的写入。
|
|
301
|
-
|
|
302
|
-
git 不可用时 mem-note 完全回退:所有功能照常工作,只是没有版本历史。
|
|
303
|
-
|
|
304
|
-
**`.gitignore` 默认内容**:忽略 `.snapshots/` 和 OS 垃圾(`.DS_Store` 等)。
|
|
305
|
-
其余所有内容(包括 `notes/`、`inbox/`、`inbox/.processed/`、`insights/`、
|
|
306
|
-
`.context/index.md`、`.reports/`、`notes/skills/`)都进入 git。
|
|
307
|
-
|
|
308
|
-
**为什么 `.processed/` 也 track**:虽然和 git 历史有重合,但它保留了"已处理
|
|
309
|
-
inbox entry"的语义维度,方便后续 groom 或人类直接 `Read` 回溯;grep 友好。
|
|
310
|
-
|
|
311
|
-
## 12. FAQ
|
|
312
|
-
|
|
313
|
-
**Q: 为什么 inbox entry 的 frontmatter 可以为空?**
|
|
314
|
-
A: inbox 是原始输入池。人 / hook / 外部脚本 save 时不应该被分类 schema 绑架。分类是 groom 的职责。
|
|
315
|
-
|
|
316
|
-
**Q: 为什么 `archive` 有 CLI 但 `create-note` 没有?**
|
|
317
|
-
A: archive 涉及"加日期前缀 + mv + 更新 NOTES.md"多步操作,手动容易漏;create 只是一次 `Write` + 一次 `Edit NOTES.md`,原生工具更透明。
|
|
318
|
-
|
|
319
|
-
**Q: AI agent 怎么在会话里搜 note?**
|
|
320
|
-
A: 用 `Grep "query" notes/**/*.md` 或 `Grep --glob '!**/NOTES.md' --glob '!**/_archived/**' "query" notes/`。
|
|
321
|
-
|
|
322
|
-
**Q: 人想在 terminal 快速搜 note?**
|
|
323
|
-
A: 用 ripgrep:`rg -i "query" ~/.aibox-mem-note/notes/`。懒的话 shell rc 加:
|
|
324
|
-
|
|
325
|
-
```sh
|
|
326
|
-
alias mns='rg -i ~/.aibox-mem-note/notes/'
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
**Q: 插件升级时 `CONVENTIONS.md` 会被覆盖吗?**
|
|
330
|
-
A: 不会。`mem-note init` 检测到已存在的 `CONVENTIONS.md` 会跳过写入。如果 schema 升级需要迁移,由 `mem-note doctor --migrate` 处理(未来扩展)。
|
|
331
|
-
|
|
332
|
-
**Q: `doctor` 检查哪些东西?**
|
|
333
|
-
A:
|
|
334
|
-
|
|
335
|
-
1. 每个 note 的 frontmatter 是否有必填字段(`type`、`title`、`description`、`tags`、`updated`)
|
|
336
|
-
2. 每个 note 是否在对应目录的 `NOTES.md` 里有对应条目
|
|
337
|
-
3. 每个 `NOTES.md` 条目是否指向存在的文件
|
|
338
|
-
4. `notes/_archived/` 下的文件名是否符合 `<YYYY-MM-DD>_<slug>.md` 格式
|
|
339
|
-
5. Inbox entry 的 `created` 字段是否存在
|
|
340
|
-
6. `.context/index.md` 是否存在 `## Active Reminders` 段落
|
|
341
|
-
|
|
342
|
-
**Q: 多个 AI agent 会话并发写怎么办?**
|
|
343
|
-
A: 不支持真正的并发(没有锁)。groom 是单会话操作,假设没有并发 writer。
|
|
344
|
-
|
|
345
|
-
**Q: 归档后能恢复吗?**
|
|
346
|
-
A: 能。`Bash mv notes/_archived/<date>_<slug>.md notes/<topic>/<slug>.md` + `Edit NOTES.md`。没有专门 CLI 命令,因为是罕见操作。
|
|
101
|
+
**维护契约**:创建 / 重命名 / 归档 note 时必须同步更新对应 `NOTES.md`。`title` 和 `description` 来自 note frontmatter。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Groom Rules
|
|
2
|
+
|
|
3
|
+
> 本文件归用户所有,插件升级不会覆盖(仅首次 init 时写入种子版本)。
|
|
4
|
+
> 你可以自由修改以调教 AI agent 的 groom 行为。
|
|
5
|
+
|
|
6
|
+
## Triage 策略
|
|
7
|
+
|
|
8
|
+
对每条 inbox entry,决策 **create** / **merge** / **discard**:
|
|
9
|
+
|
|
10
|
+
- **create**:全新知识,与现有 notes 无重叠
|
|
11
|
+
- **merge**:补充已有 note 的信息
|
|
12
|
+
- **discard**:噪声、调试碎片、已被捕获的重复内容
|
|
13
|
+
|
|
14
|
+
拿不准时优先 **create** — 后续 groom 可以再合并去重。
|
|
15
|
+
|
|
16
|
+
## 并行 Triage
|
|
17
|
+
|
|
18
|
+
可为每条 inbox entry 派发一个 sub-agent(便宜模型即可),并行处理:
|
|
19
|
+
- 每个 sub-agent `Read` 一条 entry + notes 索引快照,输出结构化决策
|
|
20
|
+
- 主 agent 收集结果后顺序执行写入(避免 NOTES.md 冲突)
|
|
21
|
+
|
|
22
|
+
## 合并判断
|
|
23
|
+
|
|
24
|
+
- 语义重叠 > 50% 即应 merge 而非 create
|
|
25
|
+
- merge 时保留原 note 的结构,追加新信息,bump `updated`
|
|
26
|
+
|
|
27
|
+
## Discard 标准
|
|
28
|
+
|
|
29
|
+
- 纯 tool 输出、命令行日志、无实质内容的对话片段
|
|
30
|
+
- 与已有 note 完全重复(无新信息)
|
|
31
|
+
- 临时性调试上下文(除非包含可复用的 lesson)
|
|
32
|
+
|
|
33
|
+
## 洞察提取
|
|
34
|
+
|
|
35
|
+
发现以下情况时写入 `insights/`:
|
|
36
|
+
- 两条 note 之间存在矛盾
|
|
37
|
+
- ≥ 3 条 note 呈现同一模式
|
|
38
|
+
- 某个重复操作适合抽成 skill
|
|
39
|
+
|
|
40
|
+
## .context/index.md 编译
|
|
41
|
+
|
|
42
|
+
groom 结束时重新编译,< 2KB,必须包含 `## Active Reminders`(可为空)。
|
|
43
|
+
扫描所有 `type: reminder` 的 note 生成该段落。
|
package/bin/mem-note.mjs
CHANGED
|
@@ -7084,13 +7084,7 @@ var require_gray_matter = __commonJS({
|
|
|
7084
7084
|
import { homedir } from "node:os";
|
|
7085
7085
|
import { basename as basename3, dirname as dirname3, join as join22, resolve } from "node:path";
|
|
7086
7086
|
import { fileURLToPath } from "node:url";
|
|
7087
|
-
import {
|
|
7088
|
-
access as access2,
|
|
7089
|
-
copyFile,
|
|
7090
|
-
readFile as readFile4,
|
|
7091
|
-
readdir as readdir3,
|
|
7092
|
-
writeFile as writeFile22
|
|
7093
|
-
} from "node:fs/promises";
|
|
7087
|
+
import { access as access2, copyFile, readFile as readFile4, readdir as readdir3 } from "node:fs/promises";
|
|
7094
7088
|
|
|
7095
7089
|
// apps/mem-note-cli/node_modules/commander/esm.mjs
|
|
7096
7090
|
var import_index = __toESM(require_commander(), 1);
|
|
@@ -7172,6 +7166,10 @@ var DataDir = class {
|
|
|
7172
7166
|
conventionsPath() {
|
|
7173
7167
|
return join(this.root, "CONVENTIONS.md");
|
|
7174
7168
|
}
|
|
7169
|
+
/** `<root>/GROOM_RULES.md` — user-owned groom strategy, seeded by init, never overwritten. */
|
|
7170
|
+
groomRulesPath() {
|
|
7171
|
+
return join(this.root, "GROOM_RULES.md");
|
|
7172
|
+
}
|
|
7175
7173
|
/** The absolute root path. */
|
|
7176
7174
|
get rootDir() {
|
|
7177
7175
|
return this.root;
|
|
@@ -7731,19 +7729,33 @@ function printInstallGuidance() {
|
|
|
7731
7729
|
function resolveDataRoot() {
|
|
7732
7730
|
return process.env["MEM_NOTE_DATA_DIR"] ?? join22(homedir(), ".aibox-mem-note");
|
|
7733
7731
|
}
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7732
|
+
function resolveResourceDir() {
|
|
7733
|
+
try {
|
|
7734
|
+
return dirname3(fileURLToPath(import.meta.url));
|
|
7735
|
+
} catch {
|
|
7736
|
+
return __dirname;
|
|
7737
|
+
}
|
|
7738
7738
|
}
|
|
7739
|
-
function
|
|
7740
|
-
|
|
7739
|
+
async function syncResources(dd) {
|
|
7740
|
+
const resDir = resolveResourceDir();
|
|
7741
|
+
try {
|
|
7742
|
+
await copyFile(resolve(resDir, "conventions.md"), dd.conventionsPath());
|
|
7743
|
+
} catch {
|
|
7744
|
+
}
|
|
7741
7745
|
try {
|
|
7742
|
-
|
|
7746
|
+
await access2(dd.groomRulesPath());
|
|
7743
7747
|
} catch {
|
|
7744
|
-
|
|
7748
|
+
try {
|
|
7749
|
+
await copyFile(resolve(resDir, "groom-rules.md"), dd.groomRulesPath());
|
|
7750
|
+
} catch {
|
|
7751
|
+
}
|
|
7745
7752
|
}
|
|
7746
|
-
|
|
7753
|
+
}
|
|
7754
|
+
async function getDataDir() {
|
|
7755
|
+
const dd = new DataDir(resolveDataRoot());
|
|
7756
|
+
await dd.ensureInitialized();
|
|
7757
|
+
await syncResources(dd);
|
|
7758
|
+
return dd;
|
|
7747
7759
|
}
|
|
7748
7760
|
async function readStdin() {
|
|
7749
7761
|
if (process.stdin.isTTY)
|
|
@@ -7757,28 +7769,9 @@ async function readStdin() {
|
|
|
7757
7769
|
var program2 = new Command();
|
|
7758
7770
|
program2.name("mem-note").description("AI-enhanced personal knowledge base \u2014 minimal CLI surface").version("0.2.0");
|
|
7759
7771
|
program2.command("init").description(
|
|
7760
|
-
"Create
|
|
7772
|
+
"Create data directory, sync plugin resources, and initialize git (idempotent)"
|
|
7761
7773
|
).action(async () => {
|
|
7762
7774
|
const dd = await getDataDir();
|
|
7763
|
-
const conventionsDest = dd.conventionsPath();
|
|
7764
|
-
let conventionsExisted = false;
|
|
7765
|
-
try {
|
|
7766
|
-
await access2(conventionsDest);
|
|
7767
|
-
conventionsExisted = true;
|
|
7768
|
-
} catch {
|
|
7769
|
-
}
|
|
7770
|
-
if (!conventionsExisted) {
|
|
7771
|
-
const source = resolveConventionsResource();
|
|
7772
|
-
try {
|
|
7773
|
-
await copyFile(source, conventionsDest);
|
|
7774
|
-
} catch (err) {
|
|
7775
|
-
const fallback = "# mem-note CONVENTIONS\n\nFull conventions document missing from this installation. See apps/mem-note-cli/src/conventions.md in the source repo.\n";
|
|
7776
|
-
await writeFile22(conventionsDest, fallback, "utf-8");
|
|
7777
|
-
console.warn(
|
|
7778
|
-
`[mem-note init] Warning: could not read ${source}: ${String(err)}`
|
|
7779
|
-
);
|
|
7780
|
-
}
|
|
7781
|
-
}
|
|
7782
7775
|
let gitStatus = "skipped-no-git";
|
|
7783
7776
|
if (await isGitAvailable()) {
|
|
7784
7777
|
if (await isGitRepo(dd.rootDir)) {
|
|
@@ -7794,12 +7787,14 @@ program2.command("init").description(
|
|
|
7794
7787
|
JSON.stringify({
|
|
7795
7788
|
ok: true,
|
|
7796
7789
|
root: dd.rootDir,
|
|
7797
|
-
conventions: conventionsExisted ? "already-present" : conventionsDest,
|
|
7798
7790
|
git: gitStatus
|
|
7799
7791
|
})
|
|
7800
7792
|
);
|
|
7801
7793
|
});
|
|
7802
|
-
program2.command("save").description("Save a new inbox entry. Accepts content as argument or stdin.").argument("[content]", "Content to save (omit to read from stdin)").option(
|
|
7794
|
+
program2.command("save").description("Save a new inbox entry. Accepts content as argument or stdin.").argument("[content]", "Content to save (omit to read from stdin)").option(
|
|
7795
|
+
"--type <type>",
|
|
7796
|
+
"Preliminary type hint (optional; groom may rewrite)"
|
|
7797
|
+
).option(
|
|
7803
7798
|
"--tags <tags>",
|
|
7804
7799
|
"Comma-separated tags (optional)",
|
|
7805
7800
|
(val) => val.split(",").map((t) => t.trim()).filter(Boolean)
|
|
@@ -7918,9 +7913,7 @@ async function walkNotes(dir) {
|
|
|
7918
7913
|
}
|
|
7919
7914
|
return results;
|
|
7920
7915
|
}
|
|
7921
|
-
program2.command("ui-exec", { hidden: true }).description(
|
|
7922
|
-
"Internal-only UI bridge command"
|
|
7923
|
-
).action(async () => {
|
|
7916
|
+
program2.command("ui-exec", { hidden: true }).description("Internal-only UI bridge command").action(async () => {
|
|
7924
7917
|
const raw = await readStdin();
|
|
7925
7918
|
if (!raw) {
|
|
7926
7919
|
process.stderr.write("ui-exec: no input received on stdin\n");
|
|
@@ -7959,7 +7952,9 @@ program2.command("ui-exec", { hidden: true }).description(
|
|
|
7959
7952
|
}
|
|
7960
7953
|
case "list-inbox": {
|
|
7961
7954
|
const inboxDir = dd.inboxDir();
|
|
7962
|
-
const entries = await readdir3(inboxDir, {
|
|
7955
|
+
const entries = await readdir3(inboxDir, {
|
|
7956
|
+
withFileTypes: true
|
|
7957
|
+
}).catch(() => []);
|
|
7963
7958
|
const items = await Promise.all(
|
|
7964
7959
|
entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map(async (e) => {
|
|
7965
7960
|
const absPath = join22(inboxDir, e.name);
|
|
@@ -7990,10 +7985,31 @@ program2.command("ui-exec", { hidden: true }).description(
|
|
|
7990
7985
|
const date = typeof input["date"] === "string" ? input["date"] : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7991
7986
|
let content = "";
|
|
7992
7987
|
try {
|
|
7993
|
-
content = await readFile4(
|
|
7988
|
+
content = await readFile4(
|
|
7989
|
+
join22(dd.reportsDir(), `${date}.md`),
|
|
7990
|
+
"utf-8"
|
|
7991
|
+
);
|
|
7994
7992
|
} catch {
|
|
7995
7993
|
}
|
|
7996
|
-
process.stdout.write(
|
|
7994
|
+
process.stdout.write(
|
|
7995
|
+
JSON.stringify({ ok: true, data: { content, date } })
|
|
7996
|
+
);
|
|
7997
|
+
break;
|
|
7998
|
+
}
|
|
7999
|
+
case "get-graph-path": {
|
|
8000
|
+
const graphPath = join22(dd.reportsDir(), "graph.html");
|
|
8001
|
+
let exists = false;
|
|
8002
|
+
try {
|
|
8003
|
+
await access2(graphPath);
|
|
8004
|
+
exists = true;
|
|
8005
|
+
} catch {
|
|
8006
|
+
}
|
|
8007
|
+
process.stdout.write(
|
|
8008
|
+
JSON.stringify({
|
|
8009
|
+
ok: true,
|
|
8010
|
+
data: { path: exists ? graphPath : null }
|
|
8011
|
+
})
|
|
8012
|
+
);
|
|
7997
8013
|
break;
|
|
7998
8014
|
}
|
|
7999
8015
|
case "process-inbox": {
|
|
@@ -8002,7 +8018,9 @@ program2.command("ui-exec", { hidden: true }).description(
|
|
|
8002
8018
|
const reason = typeof input["reason"] === "string" ? input["reason"] : void 0;
|
|
8003
8019
|
const filename = basename3(rawFilename);
|
|
8004
8020
|
if (!filename || !filename.endsWith(".md") || filename !== rawFilename) {
|
|
8005
|
-
process.stderr.write(
|
|
8021
|
+
process.stderr.write(
|
|
8022
|
+
"ui-exec: process-inbox requires a plain .md filename\n"
|
|
8023
|
+
);
|
|
8006
8024
|
process.exit(1);
|
|
8007
8025
|
}
|
|
8008
8026
|
if (rawStatus !== "processed" && rawStatus !== "user-ignored") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meitouai/mem-note-cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Personal knowledge base CLI for use with the mem-note Claude Code plugin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/mem-note.mjs",
|
|
12
|
-
"bin/conventions.md"
|
|
12
|
+
"bin/conventions.md",
|
|
13
|
+
"bin/groom-rules.md"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=20"
|