keepm 3.2.0__tar.gz → 3.4.0__tar.gz
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.
- {keepm-3.2.0/src/keepm.egg-info → keepm-3.4.0}/PKG-INFO +61 -7
- {keepm-3.2.0 → keepm-3.4.0}/README.md +60 -6
- {keepm-3.2.0 → keepm-3.4.0}/README_EN.md +60 -6
- {keepm-3.2.0 → keepm-3.4.0}/pyproject.toml +1 -1
- keepm-3.4.0/src/keepm/__init__.py +1 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/audit.py +21 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/config.py +43 -2
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/database.py +176 -8
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/doctor.py +115 -4
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/elicitation.py +3 -3
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/hooks.py +16 -1
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/identifiers.py +18 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/instructions.py +69 -38
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/integrations.py +78 -13
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/markdown.py +18 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/models.py +23 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/proposal.py +4 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/server.py +189 -6
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/service.py +189 -6
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/setup.py +185 -7
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/validation.py +231 -4
- {keepm-3.2.0 → keepm-3.4.0/src/keepm.egg-info}/PKG-INFO +61 -7
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm.egg-info/SOURCES.txt +4 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_audit.py +29 -0
- keepm-3.4.0/tests/test_catalog.py +246 -0
- keepm-3.4.0/tests/test_contracts.py +237 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_database.py +165 -1
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_elicitation.py +3 -4
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_end_to_end.py +2 -1
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_instructions.py +33 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_maintenance.py +1 -1
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_markdown.py +83 -1
- keepm-3.4.0/tests/test_pi_agent.py +275 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_proposals.py +41 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_server.py +158 -3
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_service.py +204 -4
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_setup.py +66 -0
- keepm-3.4.0/tests/test_source_pointers.py +133 -0
- keepm-3.2.0/src/keepm/__init__.py +0 -1
- {keepm-3.2.0 → keepm-3.4.0}/LICENSE +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/MANIFEST.in +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/assets/keepm-wordmark.svg +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/setup.cfg +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/cli.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/filesystems.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/gc.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/handoff.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/health.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/locking.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/maintenance.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/projects.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/relationships.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/repair.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/storage.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/sync.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/tokens.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm/watcher.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm.egg-info/dependency_links.txt +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm.egg-info/entry_points.txt +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm.egg-info/requires.txt +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/src/keepm.egg-info/top_level.txt +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_cli.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_config.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_dated_filenames.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_filesystems.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_gc.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_handoff.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_health.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_hooks.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_identifiers.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_integrations.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_locking.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_projects.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_relationships.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_repair.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_storage.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_sync.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_tokens.py +0 -0
- {keepm-3.2.0 → keepm-3.4.0}/tests/test_watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keepm
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Admission-controlled local Markdown memory MCP for AI coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/LeoKon3/KeepM
|
|
6
6
|
Project-URL: Repository, https://github.com/LeoKon3/KeepM
|
|
@@ -24,7 +24,7 @@ Dynamic: license-file
|
|
|
24
24
|
|
|
25
25
|
> 带准入控制、本地优先、无模型依赖的 AI 编码 Agent 长期记忆引擎。
|
|
26
26
|
|
|
27
|
-
[](https://pypi.org/project/keepm/)
|
|
28
28
|
[](https://www.python.org/)
|
|
29
29
|
[](https://modelcontextprotocol.io/)
|
|
30
30
|
[](https://github.com/LeoKon3/KeepM/blob/main/LICENSE)
|
|
@@ -98,16 +98,36 @@ uvx keepm setup
|
|
|
98
98
|
`uvx` 会在隔离环境中运行 KeepM,无需永久安装。交互向导会完成最少且完整的接入:
|
|
99
99
|
|
|
100
100
|
- 指定共用的 Markdown 存储根目录与默认记忆语言(`zh` / `en`);
|
|
101
|
-
- 选择 Codex 或
|
|
101
|
+
- 选择 Codex、Claude Code 或 pi;
|
|
102
102
|
- 指定要接入的项目目录,默认使用当前工作目录;
|
|
103
103
|
- 仅在该项目中注册 `keepm` stdio MCP Server、安装受管理的 Agent 记忆策略,并默认配置 `SessionStart` / `Stop` Hook。
|
|
104
104
|
|
|
105
|
+
三个宓主的接入位置:
|
|
106
|
+
|
|
107
|
+
| 宓主 | MCP 注册 | Agent 入口 | 生命周期 Hook |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| Codex | `.codex/config.toml` | `AGENTS.md` | `.codex/hooks.json` |
|
|
110
|
+
| Claude Code | `.mcp.json` | `CLAUDE.md` | `.claude/settings.json` |
|
|
111
|
+
| pi | `.mcp.json` | `AGENTS.md` | 无宓主 Hook 文件,报 `not-applicable` |
|
|
112
|
+
|
|
113
|
+
pi 与 Claude Code 共用同一份项目 `.mcp.json`,且两边都把 `command` 条目视为 stdio,因此一次注册对两个宓主同时生效,不会互相覆盖。写入采用 JSON 合并:其他 MCP Server 和未知顶层字段全部保留;文件格式非法、是符号链接或不是普通文件时直接报错,绝不整体重写。pi 没有 Claude 那种宓主 Hook 配置文件,setup 会在文本和 JSON 中把 `lifecycle_hooks` 明确报为 `not-applicable`,而不伪造成功;相同的会话与 checkpoint 指引由受管理的 `AGENTS.md` 策略承担。
|
|
114
|
+
|
|
105
115
|
自动化配置:
|
|
106
116
|
|
|
107
117
|
```bash
|
|
108
118
|
uvx keepm setup --yes --root /path/to/notes --agent codex --project-dir /path/to/project
|
|
109
119
|
```
|
|
110
120
|
|
|
121
|
+
`--agent` 可选 `codex`、`claude` 或 `pi`。
|
|
122
|
+
|
|
123
|
+
CI、安装脚本或 Agent 还可以请求机器可读的验收摘要:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uvx keepm setup --yes --format json --root /path/to/notes --agent codex --project-dir /path/to/project
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
文本和 JSON 输出都会分别汇总 Vault 可访问性、MCP 注册、受管理策略、Hook 与 Audit 配置。SQLite 健康和宿主 Elicitation 能力只有在首次 MCP 会话建立后才能验证,因此 setup 会明确标为 deferred,而不会伪造成功。
|
|
130
|
+
|
|
111
131
|
可选的运维参数包括 `--deep-reconcile-seconds`(默认 1800,`0` 禁用周期深扫)、`--verification-review-days`(默认 180)和 `--sqlite-journal-mode wal|delete`(默认 `wal`;网络文件系统可显式选择 `delete`,但这不会提供跨机器锁)。
|
|
112
132
|
|
|
113
133
|
重复执行 setup 是幂等的,只会更新目标项目中 KeepM 管理的配置块。`~/.config/keepm/config.toml` 只保存 KeepM 自身的本机 Vault 配置,不会在所有项目中全局启用 Agent。KeepM 不要求安装或运行 Obsidian;在 WSL 中直接使用 `/mnt/c/...` 形式的 Windows 挂载路径即可。
|
|
@@ -133,6 +153,38 @@ uvx keepm gc --confirm --token <gc-token>
|
|
|
133
153
|
|
|
134
154
|
确认后的 GC 是不可恢复删除;trash 内容、checksum、候选集合或 token 时效发生变化时会拒绝执行并要求重新预览。
|
|
135
155
|
|
|
156
|
+
## JIT 两阶段读取与可复现卡片契约
|
|
157
|
+
|
|
158
|
+
上下文压缩丢细节的真正原因往往不是“没存”,而是“存得太粗”与“召回靠语义联想”。KeepM 把两个问题分开解决。
|
|
159
|
+
|
|
160
|
+
**写入侧:确定性契约。** `decision`、`constraint`、`fix` 必须包含固定英文段落 `## Decision and constraints` 与 `## Reproducible parameters`(无论正文是中文还是英文),`fix` 还必须在 frontmatter 携带至少一个仓库相对 `sources` 指针:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
sources:
|
|
164
|
+
- deploy/docker-compose.yml#service:db
|
|
165
|
+
- src/config/database.py#symbol:get_engine
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
指针路径是机器真源,`#` 后的锚点只是给人看的提示,KeepM 绝不做 AST 解析,避免重构造成误报。契约只在写入时生效(create / propose / inbox_update / promote,以及改变正文或 kind 的 update);契约之前保存的卡片仍然可解析、可检索,只刷新 `verified_at` 或 `tags` 也不会被拦。写入层另外会拦住带密码的连接串与显式密码赋值,但会放行 `***`、`${PGPASSWORD}`、`<password>` 这类脉敏写法。
|
|
169
|
+
|
|
170
|
+
**读取侧:两阶段 JIT。**
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
阶段 1:骶架探测
|
|
174
|
+
memory_context(query) ─► 相关记忆、handoff、Inbox 计数
|
|
175
|
+
memory_catalog() ─► 有界槽位句柄(name / kind / scope / 30 字用途 / is_stale)
|
|
176
|
+
|
|
177
|
+
阶段 2:目标点读
|
|
178
|
+
memory_catalog(source_path="deploy/docker-compose.yml")
|
|
179
|
+
─► 路径反查到 postgres-wsl2-io-config
|
|
180
|
+
memory_read(identifier="postgres-wsl2-io-config")
|
|
181
|
+
─► 完整参数、真源指针、版本凭证
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
目录只返回句柄,不返回事实:排序固定为 `pinned → kind 优先级 → verified_at → name_key`,默认 `limit=20`、用途行截断到 30 字符,并带 `notice` 明确禁止从槽位名推测参数。同名的 global 槽位被 project 槽位遮蔽后不会重复出现,`is_stale` 直接复用 doctor 的验证判据(只对 active 的 `constraint` / `fix` / `workflow` 生效)。
|
|
185
|
+
|
|
186
|
+
路径反查把“该读哪张卡”从语义联想变成确定性路由:准备修改 `deploy/docker-compose.yml` 时,相关 `fix` 卡片会直接被命中,而不依赖 Agent 猜到名字。目录支持文件与目录前缀,并对 `./`、反斜杠和大小写做可移植归一化。
|
|
187
|
+
|
|
136
188
|
## Agent 工作流与低噪对话式审核
|
|
137
189
|
|
|
138
190
|
```text
|
|
@@ -150,7 +202,7 @@ uvx keepm gc --confirm --token <gc-token>
|
|
|
150
202
|
|
|
151
203
|
写操作会立即刷新对应索引,不需要在任务开始或结束时常规调用 `memory_sync`。Hook 只负责提醒当前 Agent 做有边界的恢复或检查点整理;KeepM 不读取原始对话,也不会调用模型。
|
|
152
204
|
|
|
153
|
-
KeepM 使用受控对话完成 Inbox
|
|
205
|
+
KeepM 使用受控对话完成 Inbox 审核,不强制依赖 GUI 或 Obsidian 插件。Agent 只有在用户主动要求、当前对话刚产生新 Proposal,或 Inbox 出现过期/数量阈值信号时,才会在主任务回答末尾提醒;每次最多呈报 3 条。宿主支持 MCP Elicitation 时,实际提升前还会显示一次与 Proposal checksum 和目标方案绑定的原生最终确认。
|
|
154
206
|
|
|
155
207
|
> **Agent 对话式审核示例**
|
|
156
208
|
>
|
|
@@ -172,7 +224,7 @@ KeepM 使用受控对话完成 Inbox 审核,不依赖 GUI 或 Obsidian 插件
|
|
|
172
224
|
| 分组 | 工具 | 用途 |
|
|
173
225
|
| --- | --- | --- |
|
|
174
226
|
| 上下文 | `memory_status`、`memory_context` | 运行状态、有长度边界的任务上下文和仅计数的 Inbox 信号 |
|
|
175
|
-
| 检索 | `memory_search`、`memory_read`、`memory_links` | active 记忆加权检索、权威读取、WikiLink 与反向链接 |
|
|
227
|
+
| 检索 | `memory_catalog`、`memory_search`、`memory_read`、`memory_links` | 有界槽位目录与文件路径反查、active 记忆加权检索、权威读取、WikiLink 与反向链接 |
|
|
176
228
|
| 正式记忆 | `memory_create`、`memory_update`、`memory_delete`、`memory_resolve_conflict` | 创建已确认记忆;保护更新、删除及两条 active 记忆的预览式冲突解决 |
|
|
177
229
|
| Inbox | `memory_propose`、`memory_inbox_list`、`memory_inbox_read`、`memory_inbox_update`、`memory_inbox_promote`、`memory_inbox_reject`、`memory_inbox_prune` | 隔离、查看、修正、批准、拒绝、合并或安全清理候选 |
|
|
178
230
|
| Handoff | `memory_handoff_create`、`memory_handoff_read`、`memory_handoff_update`、`memory_handoff_complete` | 保存、恢复、更新和归档未完成任务状态 |
|
|
@@ -186,8 +238,10 @@ KeepM 使用受控对话完成 Inbox 审核,不依赖 GUI 或 Obsidian 插件
|
|
|
186
238
|
- **落盘边界:** 单文件写入会先 `fsync` 临时文件,`os.replace` 后再 `fsync` 父目录;Markdown 仍是唯一真源,Doctor 只报告损坏,不猜测性重写。
|
|
187
239
|
- **单活 Writer:** 一个 Vault 同一时间只能由一台机器写入。`memory_status` 会尽力识别 NFS/SMB 等网络文件系统并给出提示;SQLite journal mode 可显式配置为 `wal` 或 `delete`,但两者都不能替代跨主机协调。
|
|
188
240
|
- **核验提示:** `memory_doctor` 只对 active 的 `constraint`、`fix`、`workflow` 给出 `verification_missing` / `verification_overdue` info;`decision` 和 `preference` 默认免检,系统绝不因此自动归档或改写记忆。
|
|
241
|
+
- **指针校验:** 只有本机配置记录了 `[projects.<id>].root`,且该目录仍然解析回相同 project id 时,doctor 才会检查 `sources` 指针是否存在;否则返回 `source_pointer_check_skipped`。缺失只会产生 info 级 `source_pointer_missing`,绝不把报告标为 failed——因为同一个 Vault 会被多台机器共用。项目根目录只保存在本机 `~/.config/keepm/config.toml`,绝不写入 Vault。
|
|
189
242
|
- **结构化报告:** `memory_doctor(report_format=json|markdown)` 和 `keepm doctor` 复用同一份无正文健康报告;CLI 可安全写入本地 JSON/Markdown,不引入 HTML 渲染。
|
|
190
|
-
- **Host 确认适配:** `memory_status.host_confirmation` 根据 MCP 客户端能力报告 `elicitation` 或 `proposal-protocol
|
|
243
|
+
- **Host 确认适配:** `memory_status.host_confirmation` 根据 MCP 客户端能力报告 `elicitation` 或 `proposal-protocol`。不支持 Elicitation 时沿用已经明确完成的 Proposal P1/P2 授权;宿主声明支持后,只有原生确认明确接受才会继续,拒绝、取消或调用错误都会保持 Proposal pending,不写入正式记忆。
|
|
244
|
+
- **无正文审计溯源:** Proposal 提升审计记录来源 Proposal、MCP 客户端、来源引用、目标记忆、KeepM 策略版本与确认模式,但不会记录 Proposal 正文、描述或 Prompt。
|
|
191
245
|
|
|
192
246
|
## 设计哲学与非目标
|
|
193
247
|
|
|
@@ -197,7 +251,7 @@ KeepM 使用受控对话完成 Inbox 审核,不依赖 GUI 或 Obsidian 插件
|
|
|
197
251
|
- **人类可编辑真源:** 手工修改的 Markdown 会增量对账;无效文件会被诊断和隔离,不会被猜测性改写。
|
|
198
252
|
- **不内置 LLM、Embedding 或向量数据库:** 语义提炼由当前 Agent 完成,KeepM 只执行确定性规则。
|
|
199
253
|
- **不使用伪置信度、静默衰减或自动提升:** 用明确生命周期状态和 `verified_at` 管理记忆,审核阈值绝不构成写入授权。
|
|
200
|
-
-
|
|
254
|
+
- **不强制 Obsidian 插件或审核 UI:** 受控 Agent 对话是通用审核入口;支持 MCP Elicitation 的宿主可额外提供原生最终确认。
|
|
201
255
|
- **不归档原始对话:** 临时进度进入有边界的 handoff,长期记忆只保留提炼后的可复用结论。
|
|
202
256
|
|
|
203
257
|
## License
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> 带准入控制、本地优先、无模型依赖的 AI 编码 Agent 长期记忆引擎。
|
|
6
6
|
|
|
7
|
-
[](https://pypi.org/project/keepm/)
|
|
8
8
|
[](https://www.python.org/)
|
|
9
9
|
[](https://modelcontextprotocol.io/)
|
|
10
10
|
[](https://github.com/LeoKon3/KeepM/blob/main/LICENSE)
|
|
@@ -78,16 +78,36 @@ uvx keepm setup
|
|
|
78
78
|
`uvx` 会在隔离环境中运行 KeepM,无需永久安装。交互向导会完成最少且完整的接入:
|
|
79
79
|
|
|
80
80
|
- 指定共用的 Markdown 存储根目录与默认记忆语言(`zh` / `en`);
|
|
81
|
-
- 选择 Codex 或
|
|
81
|
+
- 选择 Codex、Claude Code 或 pi;
|
|
82
82
|
- 指定要接入的项目目录,默认使用当前工作目录;
|
|
83
83
|
- 仅在该项目中注册 `keepm` stdio MCP Server、安装受管理的 Agent 记忆策略,并默认配置 `SessionStart` / `Stop` Hook。
|
|
84
84
|
|
|
85
|
+
三个宓主的接入位置:
|
|
86
|
+
|
|
87
|
+
| 宓主 | MCP 注册 | Agent 入口 | 生命周期 Hook |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| Codex | `.codex/config.toml` | `AGENTS.md` | `.codex/hooks.json` |
|
|
90
|
+
| Claude Code | `.mcp.json` | `CLAUDE.md` | `.claude/settings.json` |
|
|
91
|
+
| pi | `.mcp.json` | `AGENTS.md` | 无宓主 Hook 文件,报 `not-applicable` |
|
|
92
|
+
|
|
93
|
+
pi 与 Claude Code 共用同一份项目 `.mcp.json`,且两边都把 `command` 条目视为 stdio,因此一次注册对两个宓主同时生效,不会互相覆盖。写入采用 JSON 合并:其他 MCP Server 和未知顶层字段全部保留;文件格式非法、是符号链接或不是普通文件时直接报错,绝不整体重写。pi 没有 Claude 那种宓主 Hook 配置文件,setup 会在文本和 JSON 中把 `lifecycle_hooks` 明确报为 `not-applicable`,而不伪造成功;相同的会话与 checkpoint 指引由受管理的 `AGENTS.md` 策略承担。
|
|
94
|
+
|
|
85
95
|
自动化配置:
|
|
86
96
|
|
|
87
97
|
```bash
|
|
88
98
|
uvx keepm setup --yes --root /path/to/notes --agent codex --project-dir /path/to/project
|
|
89
99
|
```
|
|
90
100
|
|
|
101
|
+
`--agent` 可选 `codex`、`claude` 或 `pi`。
|
|
102
|
+
|
|
103
|
+
CI、安装脚本或 Agent 还可以请求机器可读的验收摘要:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uvx keepm setup --yes --format json --root /path/to/notes --agent codex --project-dir /path/to/project
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
文本和 JSON 输出都会分别汇总 Vault 可访问性、MCP 注册、受管理策略、Hook 与 Audit 配置。SQLite 健康和宿主 Elicitation 能力只有在首次 MCP 会话建立后才能验证,因此 setup 会明确标为 deferred,而不会伪造成功。
|
|
110
|
+
|
|
91
111
|
可选的运维参数包括 `--deep-reconcile-seconds`(默认 1800,`0` 禁用周期深扫)、`--verification-review-days`(默认 180)和 `--sqlite-journal-mode wal|delete`(默认 `wal`;网络文件系统可显式选择 `delete`,但这不会提供跨机器锁)。
|
|
92
112
|
|
|
93
113
|
重复执行 setup 是幂等的,只会更新目标项目中 KeepM 管理的配置块。`~/.config/keepm/config.toml` 只保存 KeepM 自身的本机 Vault 配置,不会在所有项目中全局启用 Agent。KeepM 不要求安装或运行 Obsidian;在 WSL 中直接使用 `/mnt/c/...` 形式的 Windows 挂载路径即可。
|
|
@@ -113,6 +133,38 @@ uvx keepm gc --confirm --token <gc-token>
|
|
|
113
133
|
|
|
114
134
|
确认后的 GC 是不可恢复删除;trash 内容、checksum、候选集合或 token 时效发生变化时会拒绝执行并要求重新预览。
|
|
115
135
|
|
|
136
|
+
## JIT 两阶段读取与可复现卡片契约
|
|
137
|
+
|
|
138
|
+
上下文压缩丢细节的真正原因往往不是“没存”,而是“存得太粗”与“召回靠语义联想”。KeepM 把两个问题分开解决。
|
|
139
|
+
|
|
140
|
+
**写入侧:确定性契约。** `decision`、`constraint`、`fix` 必须包含固定英文段落 `## Decision and constraints` 与 `## Reproducible parameters`(无论正文是中文还是英文),`fix` 还必须在 frontmatter 携带至少一个仓库相对 `sources` 指针:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
sources:
|
|
144
|
+
- deploy/docker-compose.yml#service:db
|
|
145
|
+
- src/config/database.py#symbol:get_engine
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
指针路径是机器真源,`#` 后的锚点只是给人看的提示,KeepM 绝不做 AST 解析,避免重构造成误报。契约只在写入时生效(create / propose / inbox_update / promote,以及改变正文或 kind 的 update);契约之前保存的卡片仍然可解析、可检索,只刷新 `verified_at` 或 `tags` 也不会被拦。写入层另外会拦住带密码的连接串与显式密码赋值,但会放行 `***`、`${PGPASSWORD}`、`<password>` 这类脉敏写法。
|
|
149
|
+
|
|
150
|
+
**读取侧:两阶段 JIT。**
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
阶段 1:骶架探测
|
|
154
|
+
memory_context(query) ─► 相关记忆、handoff、Inbox 计数
|
|
155
|
+
memory_catalog() ─► 有界槽位句柄(name / kind / scope / 30 字用途 / is_stale)
|
|
156
|
+
|
|
157
|
+
阶段 2:目标点读
|
|
158
|
+
memory_catalog(source_path="deploy/docker-compose.yml")
|
|
159
|
+
─► 路径反查到 postgres-wsl2-io-config
|
|
160
|
+
memory_read(identifier="postgres-wsl2-io-config")
|
|
161
|
+
─► 完整参数、真源指针、版本凭证
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
目录只返回句柄,不返回事实:排序固定为 `pinned → kind 优先级 → verified_at → name_key`,默认 `limit=20`、用途行截断到 30 字符,并带 `notice` 明确禁止从槽位名推测参数。同名的 global 槽位被 project 槽位遮蔽后不会重复出现,`is_stale` 直接复用 doctor 的验证判据(只对 active 的 `constraint` / `fix` / `workflow` 生效)。
|
|
165
|
+
|
|
166
|
+
路径反查把“该读哪张卡”从语义联想变成确定性路由:准备修改 `deploy/docker-compose.yml` 时,相关 `fix` 卡片会直接被命中,而不依赖 Agent 猜到名字。目录支持文件与目录前缀,并对 `./`、反斜杠和大小写做可移植归一化。
|
|
167
|
+
|
|
116
168
|
## Agent 工作流与低噪对话式审核
|
|
117
169
|
|
|
118
170
|
```text
|
|
@@ -130,7 +182,7 @@ uvx keepm gc --confirm --token <gc-token>
|
|
|
130
182
|
|
|
131
183
|
写操作会立即刷新对应索引,不需要在任务开始或结束时常规调用 `memory_sync`。Hook 只负责提醒当前 Agent 做有边界的恢复或检查点整理;KeepM 不读取原始对话,也不会调用模型。
|
|
132
184
|
|
|
133
|
-
KeepM 使用受控对话完成 Inbox
|
|
185
|
+
KeepM 使用受控对话完成 Inbox 审核,不强制依赖 GUI 或 Obsidian 插件。Agent 只有在用户主动要求、当前对话刚产生新 Proposal,或 Inbox 出现过期/数量阈值信号时,才会在主任务回答末尾提醒;每次最多呈报 3 条。宿主支持 MCP Elicitation 时,实际提升前还会显示一次与 Proposal checksum 和目标方案绑定的原生最终确认。
|
|
134
186
|
|
|
135
187
|
> **Agent 对话式审核示例**
|
|
136
188
|
>
|
|
@@ -152,7 +204,7 @@ KeepM 使用受控对话完成 Inbox 审核,不依赖 GUI 或 Obsidian 插件
|
|
|
152
204
|
| 分组 | 工具 | 用途 |
|
|
153
205
|
| --- | --- | --- |
|
|
154
206
|
| 上下文 | `memory_status`、`memory_context` | 运行状态、有长度边界的任务上下文和仅计数的 Inbox 信号 |
|
|
155
|
-
| 检索 | `memory_search`、`memory_read`、`memory_links` | active 记忆加权检索、权威读取、WikiLink 与反向链接 |
|
|
207
|
+
| 检索 | `memory_catalog`、`memory_search`、`memory_read`、`memory_links` | 有界槽位目录与文件路径反查、active 记忆加权检索、权威读取、WikiLink 与反向链接 |
|
|
156
208
|
| 正式记忆 | `memory_create`、`memory_update`、`memory_delete`、`memory_resolve_conflict` | 创建已确认记忆;保护更新、删除及两条 active 记忆的预览式冲突解决 |
|
|
157
209
|
| Inbox | `memory_propose`、`memory_inbox_list`、`memory_inbox_read`、`memory_inbox_update`、`memory_inbox_promote`、`memory_inbox_reject`、`memory_inbox_prune` | 隔离、查看、修正、批准、拒绝、合并或安全清理候选 |
|
|
158
210
|
| Handoff | `memory_handoff_create`、`memory_handoff_read`、`memory_handoff_update`、`memory_handoff_complete` | 保存、恢复、更新和归档未完成任务状态 |
|
|
@@ -166,8 +218,10 @@ KeepM 使用受控对话完成 Inbox 审核,不依赖 GUI 或 Obsidian 插件
|
|
|
166
218
|
- **落盘边界:** 单文件写入会先 `fsync` 临时文件,`os.replace` 后再 `fsync` 父目录;Markdown 仍是唯一真源,Doctor 只报告损坏,不猜测性重写。
|
|
167
219
|
- **单活 Writer:** 一个 Vault 同一时间只能由一台机器写入。`memory_status` 会尽力识别 NFS/SMB 等网络文件系统并给出提示;SQLite journal mode 可显式配置为 `wal` 或 `delete`,但两者都不能替代跨主机协调。
|
|
168
220
|
- **核验提示:** `memory_doctor` 只对 active 的 `constraint`、`fix`、`workflow` 给出 `verification_missing` / `verification_overdue` info;`decision` 和 `preference` 默认免检,系统绝不因此自动归档或改写记忆。
|
|
221
|
+
- **指针校验:** 只有本机配置记录了 `[projects.<id>].root`,且该目录仍然解析回相同 project id 时,doctor 才会检查 `sources` 指针是否存在;否则返回 `source_pointer_check_skipped`。缺失只会产生 info 级 `source_pointer_missing`,绝不把报告标为 failed——因为同一个 Vault 会被多台机器共用。项目根目录只保存在本机 `~/.config/keepm/config.toml`,绝不写入 Vault。
|
|
169
222
|
- **结构化报告:** `memory_doctor(report_format=json|markdown)` 和 `keepm doctor` 复用同一份无正文健康报告;CLI 可安全写入本地 JSON/Markdown,不引入 HTML 渲染。
|
|
170
|
-
- **Host 确认适配:** `memory_status.host_confirmation` 根据 MCP 客户端能力报告 `elicitation` 或 `proposal-protocol
|
|
223
|
+
- **Host 确认适配:** `memory_status.host_confirmation` 根据 MCP 客户端能力报告 `elicitation` 或 `proposal-protocol`。不支持 Elicitation 时沿用已经明确完成的 Proposal P1/P2 授权;宿主声明支持后,只有原生确认明确接受才会继续,拒绝、取消或调用错误都会保持 Proposal pending,不写入正式记忆。
|
|
224
|
+
- **无正文审计溯源:** Proposal 提升审计记录来源 Proposal、MCP 客户端、来源引用、目标记忆、KeepM 策略版本与确认模式,但不会记录 Proposal 正文、描述或 Prompt。
|
|
171
225
|
|
|
172
226
|
## 设计哲学与非目标
|
|
173
227
|
|
|
@@ -177,7 +231,7 @@ KeepM 使用受控对话完成 Inbox 审核,不依赖 GUI 或 Obsidian 插件
|
|
|
177
231
|
- **人类可编辑真源:** 手工修改的 Markdown 会增量对账;无效文件会被诊断和隔离,不会被猜测性改写。
|
|
178
232
|
- **不内置 LLM、Embedding 或向量数据库:** 语义提炼由当前 Agent 完成,KeepM 只执行确定性规则。
|
|
179
233
|
- **不使用伪置信度、静默衰减或自动提升:** 用明确生命周期状态和 `verified_at` 管理记忆,审核阈值绝不构成写入授权。
|
|
180
|
-
-
|
|
234
|
+
- **不强制 Obsidian 插件或审核 UI:** 受控 Agent 对话是通用审核入口;支持 MCP Elicitation 的宿主可额外提供原生最终确认。
|
|
181
235
|
- **不归档原始对话:** 临时进度进入有边界的 handoff,长期记忆只保留提炼后的可复用结论。
|
|
182
236
|
|
|
183
237
|
## License
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> An admission-controlled, local-first, model-free durable memory engine for AI coding agents.
|
|
6
6
|
|
|
7
|
-
[](https://pypi.org/project/keepm/)
|
|
8
8
|
[](https://www.python.org/)
|
|
9
9
|
[](https://modelcontextprotocol.io/)
|
|
10
10
|
[](LICENSE)
|
|
@@ -78,16 +78,36 @@ uvx keepm setup
|
|
|
78
78
|
`uvx` runs KeepM in an isolated environment, so no permanent installation is required. The wizard completes the smallest full integration:
|
|
79
79
|
|
|
80
80
|
- choose the shared Markdown root and default memory language (`zh` / `en`);
|
|
81
|
-
- choose Codex
|
|
81
|
+
- choose Codex, Claude Code, or pi;
|
|
82
82
|
- select the project directory to integrate, defaulting to the current working directory;
|
|
83
83
|
- register the `keepm` stdio MCP Server, managed Agent memory policy, and default `SessionStart` / `Stop` Hooks only in that project.
|
|
84
84
|
|
|
85
|
+
Where each host is integrated:
|
|
86
|
+
|
|
87
|
+
| Host | MCP registration | Agent entry | Lifecycle Hooks |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| Codex | `.codex/config.toml` | `AGENTS.md` | `.codex/hooks.json` |
|
|
90
|
+
| Claude Code | `.mcp.json` | `CLAUDE.md` | `.claude/settings.json` |
|
|
91
|
+
| pi | `.mcp.json` | `AGENTS.md` | no host Hook file; reported `not-applicable` |
|
|
92
|
+
|
|
93
|
+
pi and Claude Code share the same project `.mcp.json`, and both treat a `command` entry as stdio, so one registration satisfies both hosts instead of overwriting each other. The write is a JSON merge: every other MCP server and unknown top-level key is preserved, and an invalid, symlinked, or non-regular file raises an error instead of being replaced wholesale. pi has no Claude-style host Hook file, so setup reports `lifecycle_hooks` as `not-applicable` in both text and JSON rather than faking success; the managed `AGENTS.md` policy carries the same session and checkpoint guidance.
|
|
94
|
+
|
|
85
95
|
For automation:
|
|
86
96
|
|
|
87
97
|
```bash
|
|
88
98
|
uvx keepm setup --yes --root /path/to/notes --agent codex --project-dir /path/to/project
|
|
89
99
|
```
|
|
90
100
|
|
|
101
|
+
`--agent` accepts `codex`, `claude`, or `pi`.
|
|
102
|
+
|
|
103
|
+
CI, installer scripts, and Agents can request a machine-readable acceptance summary:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uvx keepm setup --yes --format json --root /path/to/notes --agent codex --project-dir /path/to/project
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Both text and JSON output summarize Vault accessibility, MCP registration, managed policy, Hooks, and Audit configuration independently. SQLite health and host Elicitation capability can only be verified after the first MCP session starts, so setup reports them as deferred instead of claiming success.
|
|
110
|
+
|
|
91
111
|
Optional operations settings include `--deep-reconcile-seconds` (default 1800; `0` disables periodic deep scans), `--verification-review-days` (default 180), and `--sqlite-journal-mode wal|delete` (default `wal`; network filesystems may explicitly select `delete`, but it does not provide cross-machine locking).
|
|
92
112
|
|
|
93
113
|
Setup is idempotent and updates only KeepM-managed configuration blocks in the target project. `~/.config/keepm/config.toml` stores KeepM's local Vault configuration only; it does not enable the Agent globally in every project. Obsidian does not need to be installed or running. From WSL, use a mounted Windows path such as `/mnt/c/...` directly.
|
|
@@ -113,6 +133,38 @@ uvx keepm gc --confirm --token <gc-token>
|
|
|
113
133
|
|
|
114
134
|
Confirmed GC is irreversible. Any trash-content, checksum, candidate-set, or token-expiry change rejects execution and requires a new preview.
|
|
115
135
|
|
|
136
|
+
## Just-in-time reads and the reproducible card contract
|
|
137
|
+
|
|
138
|
+
Detail usually disappears from a long session for two reasons: cards were written too coarsely, and recall depended on the model associating a topic with a slot name. KeepM addresses them separately.
|
|
139
|
+
|
|
140
|
+
**Write side: a deterministic contract.** `decision`, `constraint`, and `fix` must contain the fixed English sections `## Decision and constraints` and `## Reproducible parameters` regardless of body language, and a `fix` must also carry at least one repository-relative `sources` pointer in frontmatter:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
sources:
|
|
144
|
+
- deploy/docker-compose.yml#service:db
|
|
145
|
+
- src/config/database.py#symbol:get_engine
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The pointer path is the machine-readable truth, while the anchor after `#` is a human hint that KeepM never parses into an AST, so refactors cannot create false diagnostics. The contract applies only to writes (create, propose, inbox_update, promote, and an update that changes the body or kind). Cards stored before the contract keep parsing and stay retrievable, and refreshing only `verified_at` or `tags` is never blocked. The write layer additionally rejects credential-bearing connection strings and explicit password assignments while allowing masked forms such as `***`, `${PGPASSWORD}`, or `<password>`.
|
|
149
|
+
|
|
150
|
+
**Read side: two-phase JIT.**
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
phase 1: skeleton
|
|
154
|
+
memory_context(query) ─► relevant memories, handoff, Inbox counts
|
|
155
|
+
memory_catalog() ─► bounded slot handles (name / kind / scope / 30-char purpose / is_stale)
|
|
156
|
+
|
|
157
|
+
phase 2: targeted read
|
|
158
|
+
memory_catalog(source_path="deploy/docker-compose.yml")
|
|
159
|
+
─► routes the path to postgres-wsl2-io-config
|
|
160
|
+
memory_read(identifier="postgres-wsl2-io-config")
|
|
161
|
+
─► exact parameters, pointers, version credentials
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The catalog returns handles, never facts: ordering is fixed at `pinned → kind priority → verified_at → name_key`, the default `limit` is 20, the purpose line truncates at 30 characters, and a `notice` states that parameters must never be inferred from a slot name. A global slot shadowed by a same-named project slot never appears twice, and `is_stale` reuses the doctor verification predicate, so only active `constraint`, `fix`, and `workflow` slots can be marked.
|
|
165
|
+
|
|
166
|
+
The reverse lookup turns “which card should I read” from semantic association into deterministic routing: editing `deploy/docker-compose.yml` surfaces the `fix` card that documents it without guessing its name. Queries accept a file or a directory prefix and normalize `./`, backslashes, and case portably.
|
|
167
|
+
|
|
116
168
|
## Agent workflow and low-noise review
|
|
117
169
|
|
|
118
170
|
```text
|
|
@@ -130,7 +182,7 @@ new retained information
|
|
|
130
182
|
|
|
131
183
|
Writes immediately refresh the affected index rows, so normal task starts and finishes do not call `memory_sync`. Hooks only remind the current Agent to perform a bounded recovery or checkpoint pass; KeepM never reads raw transcripts and never invokes a model.
|
|
132
184
|
|
|
133
|
-
KeepM reviews Inbox candidates through controlled conversation, with no GUI or Obsidian plugin dependency. The Agent may mention review only when the user asks, the current conversation has just created a Proposal, or Inbox age/count thresholds signal a backlog. It appears after the main answer and presents at most three candidates.
|
|
185
|
+
KeepM reviews Inbox candidates through controlled conversation, with no mandatory GUI or Obsidian plugin dependency. The Agent may mention review only when the user asks, the current conversation has just created a Proposal, or Inbox age/count thresholds signal a backlog. It appears after the main answer and presents at most three candidates. When the host supports MCP Elicitation, promotion also shows a native final confirmation bound to the Proposal checksum and exact destination plan.
|
|
134
186
|
|
|
135
187
|
> **Conversational review example**
|
|
136
188
|
>
|
|
@@ -152,7 +204,7 @@ After the conflict is confirmed and the user explicitly authorizes the change, t
|
|
|
152
204
|
| Group | Tools | Purpose |
|
|
153
205
|
| --- | --- | --- |
|
|
154
206
|
| Context | `memory_status`, `memory_context` | Runtime state and bounded task context with count-only Inbox signals |
|
|
155
|
-
| Retrieval | `memory_search`, `memory_read`, `memory_links` |
|
|
207
|
+
| Retrieval | `memory_catalog`, `memory_search`, `memory_read`, `memory_links` | Bounded slot catalog and repository-path reverse lookup, weighted active-memory search, authoritative reads, WikiLinks, and backlinks |
|
|
156
208
|
| Formal memory | `memory_create`, `memory_update`, `memory_delete`, `memory_resolve_conflict` | Create confirmed memory; protect updates, deletes, and previewed conflict resolution of two active notes |
|
|
157
209
|
| Inbox | `memory_propose`, `memory_inbox_list`, `memory_inbox_read`, `memory_inbox_update`, `memory_inbox_promote`, `memory_inbox_reject`, `memory_inbox_prune` | Isolate, inspect, edit, approve, reject, merge, or safely prune candidates |
|
|
158
210
|
| Handoff | `memory_handoff_create`, `memory_handoff_read`, `memory_handoff_update`, `memory_handoff_complete` | Save, resume, update, and archive unfinished task state |
|
|
@@ -166,8 +218,10 @@ Mutating an existing object requires a complete current read plus one-time versi
|
|
|
166
218
|
- **Durability boundary:** a single-file write fsyncs the temporary file, performs `os.replace`, and then fsyncs the parent directory. Markdown remains the only source of truth; Doctor reports damage without guessing a rewrite.
|
|
167
219
|
- **Single-active Writer:** only one machine may write a Vault at a time. `memory_status` makes a best-effort check for network filesystems such as NFS/SMB. SQLite journal mode can be configured as `wal` or `delete`, but neither substitutes for cross-host coordination.
|
|
168
220
|
- **Verification reminders:** `memory_doctor` emits `verification_missing` / `verification_overdue` info only for active `constraint`, `fix`, and `workflow` memories. `decision` and `preference` are exempt by default, and KeepM never archives or rewrites a memory because of age.
|
|
221
|
+
- **Pointer verification:** doctor checks `sources` pointers only when the local config records `[projects.<id>].root` and that checkout still resolves to the same project id; otherwise it reports `source_pointer_check_skipped`. A missing file produces the info-level `source_pointer_missing` and never marks a report failed, because one Vault is shared across machines. Project roots live only in the local `~/.config/keepm/config.toml` and are never written into the Vault.
|
|
169
222
|
- **Structured reports:** `memory_doctor(report_format=json|markdown)` and `keepm doctor` share one body-free report model. The CLI can write local JSON/Markdown without adding HTML rendering.
|
|
170
|
-
- **Host confirmation adapter:** `memory_status.host_confirmation` reports `elicitation` or `proposal-protocol` from MCP client capabilities. Unsupported
|
|
223
|
+
- **Host confirmation adapter:** `memory_status.host_confirmation` reports `elicitation` or `proposal-protocol` from MCP client capabilities. Unsupported hosts keep using the already-completed explicit Proposal P1/P2 authorization. Once a host advertises Elicitation, only explicit native acceptance continues; decline, cancel, or an invocation error leaves the Proposal pending and writes no formal memory.
|
|
224
|
+
- **Body-free audit provenance:** Proposal promotion records the source Proposal, MCP client, source reference, target memory, KeepM policy version, and confirmation mode, but never the Proposal body, description, or Prompt.
|
|
171
225
|
|
|
172
226
|
## Design principles and non-goals
|
|
173
227
|
|
|
@@ -177,7 +231,7 @@ Mutating an existing object requires a complete current read plus one-time versi
|
|
|
177
231
|
- **Human-editable truth:** manual Markdown edits are reconciled incrementally; invalid files are diagnosed and isolated instead of being guessed into shape.
|
|
178
232
|
- **No embedded LLM, embeddings, or vector database:** the current Agent performs semantic distillation; KeepM executes deterministic rules only.
|
|
179
233
|
- **No fake confidence, silent decay, or automatic promotion:** explicit lifecycle states and `verified_at` govern memory; review thresholds never authorize a write.
|
|
180
|
-
- **No
|
|
234
|
+
- **No mandatory Obsidian plugin or review UI:** controlled Agent conversation is the universal review surface; hosts with MCP Elicitation may add native final confirmation.
|
|
181
235
|
- **No raw transcript archive:** temporary progress belongs in a bounded handoff; durable memory contains distilled, reusable conclusions only.
|
|
182
236
|
|
|
183
237
|
## License
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.4.0"
|
|
@@ -25,6 +25,13 @@ SAFE_FIELDS = frozenset(
|
|
|
25
25
|
"before_checksum",
|
|
26
26
|
"after_checksum",
|
|
27
27
|
"code",
|
|
28
|
+
"origin",
|
|
29
|
+
"source_id",
|
|
30
|
+
"source_client",
|
|
31
|
+
"source_ref",
|
|
32
|
+
"target_id",
|
|
33
|
+
"policy_version",
|
|
34
|
+
"confirmation_mode",
|
|
28
35
|
}
|
|
29
36
|
)
|
|
30
37
|
|
|
@@ -48,6 +55,13 @@ class AuditLogger:
|
|
|
48
55
|
before_checksum: str | None,
|
|
49
56
|
after_checksum: str | None,
|
|
50
57
|
code: str | None,
|
|
58
|
+
origin: str | None = None,
|
|
59
|
+
source_id: str | None = None,
|
|
60
|
+
source_client: str | None = None,
|
|
61
|
+
source_ref: str | None = None,
|
|
62
|
+
target_id: str | None = None,
|
|
63
|
+
policy_version: str | None = None,
|
|
64
|
+
confirmation_mode: str | None = None,
|
|
51
65
|
) -> None:
|
|
52
66
|
if not self.enabled:
|
|
53
67
|
return
|
|
@@ -62,6 +76,13 @@ class AuditLogger:
|
|
|
62
76
|
"before_checksum": before_checksum,
|
|
63
77
|
"after_checksum": after_checksum,
|
|
64
78
|
"code": code,
|
|
79
|
+
"origin": origin,
|
|
80
|
+
"source_id": source_id,
|
|
81
|
+
"source_client": source_client,
|
|
82
|
+
"source_ref": source_ref,
|
|
83
|
+
"target_id": target_id,
|
|
84
|
+
"policy_version": policy_version,
|
|
85
|
+
"confirmation_mode": confirmation_mode,
|
|
65
86
|
}
|
|
66
87
|
try:
|
|
67
88
|
payload = (json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n").encode(
|
|
@@ -4,9 +4,9 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import hashlib
|
|
6
6
|
import tomllib
|
|
7
|
-
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Literal, cast
|
|
9
|
+
from typing import Literal, Mapping, cast
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
MemoryLanguage = Literal["zh", "en"]
|
|
@@ -30,6 +30,26 @@ def vault_id(config: "KeepMConfig") -> str:
|
|
|
30
30
|
return hashlib.sha256(identity.encode("utf-8")).hexdigest()[:24]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _parse_project_roots(value: object) -> dict[str, Path]:
|
|
34
|
+
"""Parse the optional machine-local ``[projects.<id>]`` mapping."""
|
|
35
|
+
|
|
36
|
+
if value is None:
|
|
37
|
+
return {}
|
|
38
|
+
if not isinstance(value, Mapping):
|
|
39
|
+
raise ValueError("projects must be a table of project ids")
|
|
40
|
+
roots: dict[str, Path] = {}
|
|
41
|
+
for project_id, entry in value.items():
|
|
42
|
+
if not isinstance(entry, Mapping):
|
|
43
|
+
raise ValueError(f"projects.{project_id} must be a table")
|
|
44
|
+
root = entry.get("root")
|
|
45
|
+
if root is None:
|
|
46
|
+
continue
|
|
47
|
+
if not isinstance(root, str) or not root.strip():
|
|
48
|
+
raise ValueError(f"projects.{project_id}.root must be a path string")
|
|
49
|
+
roots[str(project_id)] = Path(root)
|
|
50
|
+
return roots
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
@dataclass(frozen=True, slots=True)
|
|
34
54
|
class KeepMConfig:
|
|
35
55
|
vault_path: Path
|
|
@@ -42,6 +62,7 @@ class KeepMConfig:
|
|
|
42
62
|
deep_reconcile_interval_seconds: int = 1_800
|
|
43
63
|
verification_review_after_days: int = 180
|
|
44
64
|
sqlite_journal_mode: SQLiteJournalMode = "wal"
|
|
65
|
+
project_roots: Mapping[str, Path] = field(default_factory=dict)
|
|
45
66
|
|
|
46
67
|
def __post_init__(self) -> None:
|
|
47
68
|
memory_path = Path(self.memory_dir)
|
|
@@ -69,6 +90,17 @@ class KeepMConfig:
|
|
|
69
90
|
raise ValueError("verification_review_after_days must be positive")
|
|
70
91
|
if self.sqlite_journal_mode not in ("wal", "delete"):
|
|
71
92
|
raise ValueError("sqlite_journal_mode must be wal or delete")
|
|
93
|
+
if not isinstance(self.project_roots, Mapping):
|
|
94
|
+
raise ValueError("project_roots must be a mapping of project id to path")
|
|
95
|
+
normalized_roots: dict[str, Path] = {}
|
|
96
|
+
for project_id, root in self.project_roots.items():
|
|
97
|
+
if not isinstance(project_id, str) or not project_id.strip():
|
|
98
|
+
raise ValueError("project_roots keys must be non-empty project ids")
|
|
99
|
+
candidate = Path(root).expanduser()
|
|
100
|
+
if not candidate.is_absolute():
|
|
101
|
+
raise ValueError("project_roots values must be absolute local paths")
|
|
102
|
+
normalized_roots[project_id.strip()] = candidate.resolve()
|
|
103
|
+
object.__setattr__(self, "project_roots", normalized_roots)
|
|
72
104
|
object.__setattr__(self, "vault_path", self.vault_path.expanduser().resolve())
|
|
73
105
|
|
|
74
106
|
@property
|
|
@@ -165,6 +197,7 @@ class ConfigStore:
|
|
|
165
197
|
sqlite_journal_mode=cast(
|
|
166
198
|
SQLiteJournalMode, str(data.get("sqlite_journal_mode", "wal"))
|
|
167
199
|
),
|
|
200
|
+
project_roots=_parse_project_roots(data.get("projects")),
|
|
168
201
|
)
|
|
169
202
|
|
|
170
203
|
def load_optional(self) -> KeepMConfig | None:
|
|
@@ -198,6 +231,14 @@ class ConfigStore:
|
|
|
198
231
|
f"verification_review_after_days = {config.verification_review_after_days}\n"
|
|
199
232
|
f"sqlite_journal_mode = {json.dumps(config.sqlite_journal_mode)}\n"
|
|
200
233
|
)
|
|
234
|
+
# Project roots stay machine-local: they map a project id to this host's
|
|
235
|
+
# checkout so `keepm doctor` can verify source pointers without ever
|
|
236
|
+
# writing host paths into the shared Vault.
|
|
237
|
+
for project_id in sorted(config.project_roots):
|
|
238
|
+
content += (
|
|
239
|
+
f"\n[projects.{json.dumps(project_id)}]\n"
|
|
240
|
+
f"root = {json.dumps(str(config.project_roots[project_id]))}\n"
|
|
241
|
+
)
|
|
201
242
|
temporary = self.path.with_suffix(".tmp")
|
|
202
243
|
temporary.write_text(content, encoding="utf-8", newline="\n")
|
|
203
244
|
os.replace(temporary, self.path)
|