keepm 0.2.0__tar.gz → 0.3.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-0.2.0/src/keepm.egg-info → keepm-0.3.0}/PKG-INFO +1 -1
- keepm-0.3.0/README.md +184 -0
- {keepm-0.2.0 → keepm-0.3.0}/pyproject.toml +1 -1
- keepm-0.3.0/src/keepm/__init__.py +1 -0
- keepm-0.3.0/src/keepm/audit.py +140 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/cli.py +5 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/config.py +15 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/database.py +241 -72
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/doctor.py +52 -17
- keepm-0.3.0/src/keepm/instructions.py +450 -0
- keepm-0.3.0/src/keepm/markdown.py +204 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/models.py +25 -3
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/server.py +131 -54
- keepm-0.3.0/src/keepm/service.py +611 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/setup.py +25 -3
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/storage.py +23 -6
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/sync.py +42 -19
- keepm-0.3.0/src/keepm/tokens.py +49 -0
- keepm-0.3.0/src/keepm/validation.py +261 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/watcher.py +14 -3
- {keepm-0.2.0 → keepm-0.3.0/src/keepm.egg-info}/PKG-INFO +1 -1
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm.egg-info/SOURCES.txt +5 -0
- keepm-0.3.0/tests/test_audit.py +78 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_cli.py +9 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_config.py +10 -2
- keepm-0.3.0/tests/test_database.py +192 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_end_to_end.py +18 -11
- keepm-0.3.0/tests/test_instructions.py +354 -0
- keepm-0.3.0/tests/test_markdown.py +238 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_server.py +103 -33
- keepm-0.3.0/tests/test_service.py +326 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_setup.py +86 -5
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_storage.py +15 -1
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_sync.py +45 -9
- keepm-0.3.0/tests/test_tokens.py +20 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_watcher.py +43 -4
- keepm-0.2.0/README.md +0 -161
- keepm-0.2.0/src/keepm/__init__.py +0 -1
- keepm-0.2.0/src/keepm/instructions.py +0 -226
- keepm-0.2.0/src/keepm/markdown.py +0 -152
- keepm-0.2.0/src/keepm/service.py +0 -299
- keepm-0.2.0/tests/test_database.py +0 -104
- keepm-0.2.0/tests/test_instructions.py +0 -201
- keepm-0.2.0/tests/test_markdown.py +0 -60
- keepm-0.2.0/tests/test_service.py +0 -145
- {keepm-0.2.0 → keepm-0.3.0}/LICENSE +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/setup.cfg +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/integrations.py +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm/projects.py +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm.egg-info/dependency_links.txt +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm.egg-info/entry_points.txt +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm.egg-info/requires.txt +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/src/keepm.egg-info/top_level.txt +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_integrations.py +0 -0
- {keepm-0.2.0 → keepm-0.3.0}/tests/test_projects.py +0 -0
keepm-0.3.0/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# KeepM
|
|
2
|
+
|
|
3
|
+
KeepM is a local Markdown memory MCP for coding agents. It stores durable memories in one Obsidian Vault, keeps Markdown as the source of truth, and uses a rebuildable SQLite FTS5 index for bounded context, search, WikiLinks, and backlinks.
|
|
4
|
+
|
|
5
|
+
[中文文档](README-zh.md)
|
|
6
|
+
|
|
7
|
+
KeepM is local-only: no embeddings, vector database, cloud service, HTTP server, required Obsidian plugin, or local model. Obsidian may be closed while KeepM runs.
|
|
8
|
+
|
|
9
|
+
## Install and setup
|
|
10
|
+
|
|
11
|
+
Requirements:
|
|
12
|
+
|
|
13
|
+
- Python 3.11+
|
|
14
|
+
- [uv](https://docs.astral.sh/uv/) for the recommended `uvx` flow
|
|
15
|
+
- One local Obsidian Vault reachable from the machine running KeepM
|
|
16
|
+
|
|
17
|
+
From WSL, a Windows Vault is normally addressed through its mount, for example:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
/mnt/c/Users/alice/Documents/ObsidianVault
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Install and configure one Vault:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uvx keepm setup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The colored setup wizard asks for the Vault, default memory language (`zh` or `en`), one Agent (Codex or Claude Code), and either user or project scope. Use arrow keys and Enter to select options. Non-interactive setup is available with:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
keepm setup --yes --agent codex --scope project
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add `--audit` or `--no-audit` to set the optional local audit preference without editing the config file.
|
|
36
|
+
|
|
37
|
+
The Agent registration scope determines where setup installs the managed instruction entry:
|
|
38
|
+
|
|
39
|
+
| Agent | User scope | Project scope |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| Codex | `~/.codex/AGENTS.md` | `<project>/AGENTS.md` |
|
|
42
|
+
| Claude Code | `~/.claude/CLAUDE.md` | `<project>/CLAUDE.md` |
|
|
43
|
+
|
|
44
|
+
Setup also installs the detailed policy referenced by that entry:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
User scope: ~/.config/keepm/policies/keepm-memory-policy.md
|
|
48
|
+
Project scope: <project>/.keepm/keepm-memory-policy.md
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
KeepM manages only content between `<!-- keepm:start -->` and `<!-- keepm:end -->`. Re-running setup replaces that block without changing surrounding user instructions. Broken, reversed, or duplicated markers are rejected rather than overwritten.
|
|
52
|
+
|
|
53
|
+
The single-Vault configuration is stored at `~/.config/keepm/config.toml`; it is not a Codex or Claude configuration file. A typical file is:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
vault_path = "/mnt/c/Users/alice/Documents/ObsidianVault"
|
|
57
|
+
memory_dir = "Memory"
|
|
58
|
+
stale_after_seconds = 30
|
|
59
|
+
language = "zh"
|
|
60
|
+
audit_enabled = false
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run `keepm` without a subcommand only as the MCP stdio server. Vault configuration is deliberately performed by `keepm setup`, not by a normal Agent MCP tool.
|
|
64
|
+
|
|
65
|
+
## Storage and V2 format
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
<Vault>/Memory/
|
|
69
|
+
├── global/
|
|
70
|
+
├── projects/
|
|
71
|
+
│ └── <project-id>/
|
|
72
|
+
└── .trash/
|
|
73
|
+
|
|
74
|
+
~/.local/share/keepm/
|
|
75
|
+
├── indexes/<vault-id>.sqlite3
|
|
76
|
+
└── audit/<vault-id>.jsonl
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `global/` contains cross-project preferences, workflows, feedback, and references.
|
|
80
|
+
- `projects/<project-id>/` contains repository-specific knowledge.
|
|
81
|
+
- `.trash/` holds soft-deleted memories and is never indexed.
|
|
82
|
+
- SQLite is derived data. It can be removed and rebuilt from Markdown without losing memory.
|
|
83
|
+
|
|
84
|
+
The current project ID comes from `.keepm.toml`, Git origin, or the workspace directory name. A project can make the mapping stable with:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
# <repository>/.keepm.toml
|
|
88
|
+
project = "keepm"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A V2 memory has explicit range and content semantics:
|
|
92
|
+
|
|
93
|
+
```markdown
|
|
94
|
+
---
|
|
95
|
+
id: 00000000-0000-0000-0000-000000000001
|
|
96
|
+
name: markdown-source-of-truth
|
|
97
|
+
description: Markdown is the canonical KeepM memory record
|
|
98
|
+
scope: project
|
|
99
|
+
project_id: keepm
|
|
100
|
+
kind: constraint
|
|
101
|
+
language: en
|
|
102
|
+
tags: [storage, obsidian]
|
|
103
|
+
pinned: true
|
|
104
|
+
created: 2026-07-18
|
|
105
|
+
updated: 2026-07-18
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
Why: Users must be able to inspect, edit, back up, and recover every memory in Obsidian.
|
|
109
|
+
|
|
110
|
+
How to apply: Store durable facts in Markdown first. Treat SQLite only as a rebuildable index.
|
|
111
|
+
|
|
112
|
+
## Relations
|
|
113
|
+
|
|
114
|
+
- depends_on [[local-first]]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`scope` is `global` or `project`; a project memory must include `project_id`, while a global memory must not. New documents also record `language: zh|en`. KeepM preserves non-KeepM frontmatter added manually in Obsidian when an Agent updates the note.
|
|
118
|
+
|
|
119
|
+
| kind | Typical use | Required non-empty labels |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `preference` | User preference | `How to apply:` |
|
|
122
|
+
| `workflow` | Repeatable workflow | `How to apply:` |
|
|
123
|
+
| `feedback` | Corrective feedback | `Why:`, `How to apply:` |
|
|
124
|
+
| `overview` | Project purpose and boundaries | None |
|
|
125
|
+
| `capability` | Stable completed capability | None |
|
|
126
|
+
| `decision` | Technical or product decision | `Why:`, `How to apply:` |
|
|
127
|
+
| `constraint` | Invariant or prohibition | `Why:`, `How to apply:` |
|
|
128
|
+
| `fix` | Reusable diagnosis and repair | `Why:`, `How to apply:` |
|
|
129
|
+
| `reference` | Stable terminology or external material | None |
|
|
130
|
+
|
|
131
|
+
V1 documents using `type: user|feedback|project|reference` are intentionally not compatible with V2 and are left untouched but excluded from the index. Rewrite them as V2 Markdown if they should remain active memories.
|
|
132
|
+
|
|
133
|
+
## MCP tools
|
|
134
|
+
|
|
135
|
+
| Tool | Purpose |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `memory_status` | Report configuration, active project, watcher, index, dirty state, and audit status |
|
|
138
|
+
| `memory_context` | Return bounded pinned and query-relevant global/project memories |
|
|
139
|
+
| `memory_search` | Search the SQLite FTS5 index |
|
|
140
|
+
| `memory_read` | Read one current Markdown memory, checksum, and an update token when output is complete |
|
|
141
|
+
| `memory_create` | Create a new memory; it never overwrites an existing logical name |
|
|
142
|
+
| `memory_update` | Replace mutable fields after a full read, checksum check, and update token |
|
|
143
|
+
| `memory_delete` | Soft-delete a full-read, checksum-verified memory into `.trash` |
|
|
144
|
+
| `memory_links` | Return forward WikiLinks and backlinks |
|
|
145
|
+
| `memory_doctor` | Diagnose Markdown/index problems and return a short-lived full-rebuild token |
|
|
146
|
+
| `memory_sync` | Run quick reconciliation or an explicitly authorized full SQLite rebuild |
|
|
147
|
+
|
|
148
|
+
Creating a memory needs no checksum. Updating or deleting one requires a complete `memory_read`, then its `expected_checksum` and one-time, ten-minute `update_token`. A truncated read intentionally has no update token. If the user edited the note in Obsidian after the read, the operation is rejected instead of overwritten.
|
|
149
|
+
|
|
150
|
+
The setup policy tells Agents when to use these tools. The Agent decides whether a verified result has durable value; the server enforces document format, version protection, body size, duplicate prevention, and sensitive-content protection.
|
|
151
|
+
|
|
152
|
+
## Synchronization, safety, and recovery
|
|
153
|
+
|
|
154
|
+
KeepM reconciles the Vault at MCP startup. It compares path, mtime, and size first, hashes only changed files, and updates only the affected index rows. While the MCP process runs, `watchfiles` provides lower-latency external-edit handling; startup and stale reconciliation remain the correctness fallback, including Windows-to-WSL event loss.
|
|
155
|
+
|
|
156
|
+
`memory_create`, `memory_update`, and `memory_delete` immediately reconcile their affected paths. Do not call `memory_sync` as routine task cleanup.
|
|
157
|
+
|
|
158
|
+
For external bulk edits or suspected inconsistency:
|
|
159
|
+
|
|
160
|
+
1. Call `memory_status` and `memory_doctor`.
|
|
161
|
+
2. Use `memory_sync(full=false)` for a quick reconcile if needed.
|
|
162
|
+
3. Use `memory_sync(full=true, confirm=true, repair_token=...)` only with the recent token from `memory_doctor` to rebuild SQLite.
|
|
163
|
+
|
|
164
|
+
The full rebuild never rewrites or deletes Markdown.
|
|
165
|
+
|
|
166
|
+
KeepM limits a memory body to 16 KiB and rejects high-confidence private keys, vendor tokens, and AWS credentials passed through MCP writes. It does not use fuzzy raw-log detection. A file edited directly in Obsidian that is invalid, oversized, or sensitive remains on disk but is excluded from the index and reported by `memory_doctor`.
|
|
167
|
+
|
|
168
|
+
## Optional audit log
|
|
169
|
+
|
|
170
|
+
Auditing is disabled by default. Set `audit_enabled = true` in KeepM configuration to enable a local, rotating JSONL log under `~/.local/share/keepm/audit/`.
|
|
171
|
+
|
|
172
|
+
The log is for the local owner, not normal Agent context. It records only operation metadata, IDs, scope/kind, checksums, result, and stable error codes—never body text, titles, descriptions, tags, raw exceptions, or secrets.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
keepm audit --limit 50
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
uv sync --extra dev
|
|
182
|
+
uv run pytest -q
|
|
183
|
+
uv run keepm setup
|
|
184
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from keepm.config import ConfigStore, default_data_root, vault_id
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MAX_AUDIT_BYTES = 10 * 1024 * 1024
|
|
15
|
+
MAX_AUDIT_HISTORY = 5
|
|
16
|
+
SAFE_FIELDS = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"time",
|
|
19
|
+
"operation",
|
|
20
|
+
"result",
|
|
21
|
+
"memory_id",
|
|
22
|
+
"scope",
|
|
23
|
+
"project_id",
|
|
24
|
+
"kind",
|
|
25
|
+
"before_checksum",
|
|
26
|
+
"after_checksum",
|
|
27
|
+
"code",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuditLogger:
|
|
33
|
+
"""Best-effort, metadata-only audit sink for the local Vault owner."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, path: Path, *, enabled: bool) -> None:
|
|
36
|
+
self.path = path.expanduser().resolve()
|
|
37
|
+
self.enabled = enabled
|
|
38
|
+
|
|
39
|
+
def record(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
operation: str,
|
|
43
|
+
result: str,
|
|
44
|
+
memory_id: str | None,
|
|
45
|
+
scope: str | None,
|
|
46
|
+
project_id: str | None,
|
|
47
|
+
kind: str | None,
|
|
48
|
+
before_checksum: str | None,
|
|
49
|
+
after_checksum: str | None,
|
|
50
|
+
code: str | None,
|
|
51
|
+
) -> None:
|
|
52
|
+
if not self.enabled:
|
|
53
|
+
return
|
|
54
|
+
event = {
|
|
55
|
+
"time": dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
56
|
+
"operation": operation,
|
|
57
|
+
"result": result,
|
|
58
|
+
"memory_id": memory_id,
|
|
59
|
+
"scope": scope,
|
|
60
|
+
"project_id": project_id,
|
|
61
|
+
"kind": kind,
|
|
62
|
+
"before_checksum": before_checksum,
|
|
63
|
+
"after_checksum": after_checksum,
|
|
64
|
+
"code": code,
|
|
65
|
+
}
|
|
66
|
+
try:
|
|
67
|
+
payload = (json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n").encode(
|
|
68
|
+
"utf-8"
|
|
69
|
+
)
|
|
70
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
if self.path.exists() and self.path.stat().st_size + len(payload) > MAX_AUDIT_BYTES:
|
|
72
|
+
self._rotate()
|
|
73
|
+
with self.path.open("ab") as handle:
|
|
74
|
+
handle.write(payload)
|
|
75
|
+
handle.flush()
|
|
76
|
+
except OSError:
|
|
77
|
+
# Auditing must never make an otherwise valid memory operation fail.
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
def _rotate(self) -> None:
|
|
81
|
+
try:
|
|
82
|
+
oldest = self.path.with_name(f"{self.path.name}.{MAX_AUDIT_HISTORY}")
|
|
83
|
+
oldest.unlink(missing_ok=True)
|
|
84
|
+
for index in range(MAX_AUDIT_HISTORY - 1, 0, -1):
|
|
85
|
+
source = self.path.with_name(f"{self.path.name}.{index}")
|
|
86
|
+
if source.exists():
|
|
87
|
+
os.replace(source, self.path.with_name(f"{self.path.name}.{index + 1}"))
|
|
88
|
+
if self.path.exists():
|
|
89
|
+
os.replace(self.path, self.path.with_name(f"{self.path.name}.1"))
|
|
90
|
+
except OSError:
|
|
91
|
+
# A later append can still succeed if a platform disallows rotation.
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def read_events(path: Path, *, limit: int = 100) -> tuple[dict[str, Any], ...]:
|
|
96
|
+
"""Read a bounded, schema-filtered audit history without exposing arbitrary JSON."""
|
|
97
|
+
if limit < 1:
|
|
98
|
+
return ()
|
|
99
|
+
events: list[dict[str, Any]] = []
|
|
100
|
+
paths = [
|
|
101
|
+
path.with_name(f"{path.name}.{index}")
|
|
102
|
+
for index in range(MAX_AUDIT_HISTORY, 0, -1)
|
|
103
|
+
] + [path]
|
|
104
|
+
for candidate in paths:
|
|
105
|
+
if not candidate.is_file():
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
lines = candidate.read_text(encoding="utf-8").splitlines()
|
|
109
|
+
except OSError:
|
|
110
|
+
continue
|
|
111
|
+
for line in lines:
|
|
112
|
+
try:
|
|
113
|
+
decoded = json.loads(line)
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
continue
|
|
116
|
+
if not isinstance(decoded, Mapping):
|
|
117
|
+
continue
|
|
118
|
+
events.append(
|
|
119
|
+
{key: decoded.get(key) for key in SAFE_FIELDS if key in decoded}
|
|
120
|
+
)
|
|
121
|
+
return tuple(events[-limit:])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv: list[str] | None = None) -> None:
|
|
125
|
+
parser = argparse.ArgumentParser(
|
|
126
|
+
prog="keepm audit", description="Show local KeepM audit metadata for the Vault owner."
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument("--limit", type=int, default=100, help="maximum recent events")
|
|
129
|
+
arguments = parser.parse_args(argv)
|
|
130
|
+
if arguments.limit < 1:
|
|
131
|
+
parser.error("--limit must be positive")
|
|
132
|
+
try:
|
|
133
|
+
config = ConfigStore().load()
|
|
134
|
+
except FileNotFoundError as error:
|
|
135
|
+
parser.error(str(error))
|
|
136
|
+
if not config.audit_enabled:
|
|
137
|
+
parser.error("KeepM audit is disabled; set audit_enabled = true in KeepM config")
|
|
138
|
+
path = default_data_root() / "audit" / f"{vault_id(config)}.jsonl"
|
|
139
|
+
for event in read_events(path, limit=arguments.limit):
|
|
140
|
+
print(json.dumps(event, ensure_ascii=False, sort_keys=True))
|
|
@@ -12,6 +12,11 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|
|
12
12
|
|
|
13
13
|
setup_main(arguments[1:])
|
|
14
14
|
return
|
|
15
|
+
if arguments[:1] == ["audit"]:
|
|
16
|
+
from keepm.audit import main as audit_main
|
|
17
|
+
|
|
18
|
+
audit_main(arguments[1:])
|
|
19
|
+
return
|
|
15
20
|
|
|
16
21
|
from keepm.server import main as server_main
|
|
17
22
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import hashlib
|
|
5
6
|
import tomllib
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
@@ -21,12 +22,19 @@ def default_data_root() -> Path:
|
|
|
21
22
|
return root / "keepm"
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def vault_id(config: "KeepMConfig") -> str:
|
|
26
|
+
"""Return a stable, non-secret identifier for one configured memory root."""
|
|
27
|
+
identity = f"{config.vault_path}\0{config.memory_root.resolve()}"
|
|
28
|
+
return hashlib.sha256(identity.encode("utf-8")).hexdigest()[:24]
|
|
29
|
+
|
|
30
|
+
|
|
24
31
|
@dataclass(frozen=True, slots=True)
|
|
25
32
|
class KeepMConfig:
|
|
26
33
|
vault_path: Path
|
|
27
34
|
memory_dir: str = "Memory"
|
|
28
35
|
stale_after_seconds: int = 30
|
|
29
36
|
language: MemoryLanguage = "en"
|
|
37
|
+
audit_enabled: bool = False
|
|
30
38
|
|
|
31
39
|
def __post_init__(self) -> None:
|
|
32
40
|
memory_path = Path(self.memory_dir)
|
|
@@ -40,6 +48,8 @@ class KeepMConfig:
|
|
|
40
48
|
raise ValueError("stale_after_seconds must be non-negative")
|
|
41
49
|
if self.language not in ("zh", "en"):
|
|
42
50
|
raise ValueError("language must be 'zh' or 'en'")
|
|
51
|
+
if not isinstance(self.audit_enabled, bool):
|
|
52
|
+
raise ValueError("audit_enabled must be true or false")
|
|
43
53
|
object.__setattr__(self, "vault_path", self.vault_path.expanduser().resolve())
|
|
44
54
|
|
|
45
55
|
@property
|
|
@@ -67,11 +77,15 @@ class ConfigStore:
|
|
|
67
77
|
if not self.path.is_file():
|
|
68
78
|
raise FileNotFoundError(f"KeepM is not configured: {self.path}")
|
|
69
79
|
data = tomllib.loads(self.path.read_text(encoding="utf-8"))
|
|
80
|
+
audit_enabled = data.get("audit_enabled", False)
|
|
81
|
+
if not isinstance(audit_enabled, bool):
|
|
82
|
+
raise ValueError("audit_enabled must be true or false")
|
|
70
83
|
return KeepMConfig(
|
|
71
84
|
vault_path=Path(data["vault_path"]),
|
|
72
85
|
memory_dir=str(data.get("memory_dir", "Memory")),
|
|
73
86
|
stale_after_seconds=int(data.get("stale_after_seconds", 30)),
|
|
74
87
|
language=cast(MemoryLanguage, str(data.get("language", "en"))),
|
|
88
|
+
audit_enabled=audit_enabled,
|
|
75
89
|
)
|
|
76
90
|
|
|
77
91
|
def load_optional(self) -> KeepMConfig | None:
|
|
@@ -90,6 +104,7 @@ class ConfigStore:
|
|
|
90
104
|
f"memory_dir = {json.dumps(config.memory_dir)}\n"
|
|
91
105
|
f"stale_after_seconds = {config.stale_after_seconds}\n"
|
|
92
106
|
f"language = {json.dumps(config.language)}\n"
|
|
107
|
+
f"audit_enabled = {str(config.audit_enabled).lower()}\n"
|
|
93
108
|
)
|
|
94
109
|
temporary = self.path.with_suffix(".tmp")
|
|
95
110
|
temporary.write_text(content, encoding="utf-8", newline="\n")
|