cherry-docs 0.2.0__py3-none-any.whl
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.
- app/__init__.py +0 -0
- app/repo_scope.py +24 -0
- app/services/__init__.py +0 -0
- app/services/agent_protocol.py +59 -0
- app/services/auto_promote_sessions.py +245 -0
- app/services/capture_adapters.py +89 -0
- app/services/capture_core.py +164 -0
- app/services/internal_memory_agent.py +214 -0
- app/services/memory_evidence.py +89 -0
- app/services/memory_extraction_normalize.py +134 -0
- app/services/memory_lifecycle.py +258 -0
- app/services/memory_profiles.py +88 -0
- app/services/memory_providers.py +113 -0
- app/services/memory_retrieval.py +327 -0
- app/services/memory_retrieval_scoring.py +106 -0
- app/services/memory_retrieval_text.py +113 -0
- app/services/memory_similarity.py +135 -0
- app/services/privacy.py +72 -0
- app/services/promoted_memory_answer.py +157 -0
- app/services/promoted_memory_pipeline.py +194 -0
- app/services/promoted_memory_store.py +57 -0
- cherry_docs-0.2.0.dist-info/METADATA +143 -0
- cherry_docs-0.2.0.dist-info/RECORD +42 -0
- cherry_docs-0.2.0.dist-info/WHEEL +5 -0
- cherry_docs-0.2.0.dist-info/entry_points.txt +4 -0
- cherry_docs-0.2.0.dist-info/top_level.txt +3 -0
- cherrydocs/__init__.py +3 -0
- cherrydocs/cli.py +213 -0
- cherrydocs/hook.py +27 -0
- cherrydocs/mcp.py +22 -0
- scripts/__init__.py +0 -0
- scripts/auto_promote_capture.py +63 -0
- scripts/check_size_limits.py +115 -0
- scripts/ci_auto_capture.py +289 -0
- scripts/claude_hooks/__init__.py +0 -0
- scripts/claude_hooks/state_manager.py +526 -0
- scripts/coverage_regression_gate.py +121 -0
- scripts/eval_projects.py +247 -0
- scripts/install.py +212 -0
- scripts/pr_gate_report.py +282 -0
- scripts/promptfoo_regression_gate.py +176 -0
- scripts/render_agent_prompts.py +57 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Project-scoped local store for promoted CherryDocs memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from app.repo_scope import normalize_project_id
|
|
10
|
+
from app.services.memory_lifecycle import MemoryRecord
|
|
11
|
+
|
|
12
|
+
DEFAULT_PROMOTED_ROOT: str = os.environ.get(
|
|
13
|
+
"CHERRY_PROMOTED_ROOT",
|
|
14
|
+
str(Path.home() / ".cherrydocs" / "promoted"),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalPromotedMemoryStore:
|
|
19
|
+
def __init__(self, root: str | Path):
|
|
20
|
+
self.root = Path(root)
|
|
21
|
+
|
|
22
|
+
def path_for(self, project_id: str) -> Path:
|
|
23
|
+
normalized = normalize_project_id(project_id)
|
|
24
|
+
return self.root / f"{normalized}.json"
|
|
25
|
+
|
|
26
|
+
def load_records(self, project_id: str) -> list[MemoryRecord]:
|
|
27
|
+
path = self.path_for(project_id)
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return []
|
|
30
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
31
|
+
if not isinstance(payload, list):
|
|
32
|
+
return []
|
|
33
|
+
return [MemoryRecord.model_validate(item) for item in payload]
|
|
34
|
+
|
|
35
|
+
def save_records(self, project_id: str, records: list[MemoryRecord]) -> Path:
|
|
36
|
+
normalized = normalize_project_id(project_id)
|
|
37
|
+
path = self.path_for(normalized)
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
path.write_text(
|
|
40
|
+
json.dumps([record.model_dump(mode="json") for record in records], indent=2),
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
def upsert_records(self, project_id: str, records: list[MemoryRecord]) -> list[MemoryRecord]:
|
|
46
|
+
existing = {record.memory_id: record for record in self.load_records(project_id)}
|
|
47
|
+
for record in records:
|
|
48
|
+
existing[record.memory_id] = record
|
|
49
|
+
merged = sorted(
|
|
50
|
+
existing.values(),
|
|
51
|
+
key=lambda item: (item.project_id or "", item.created_at, item.memory_id),
|
|
52
|
+
)
|
|
53
|
+
self.save_records(project_id, merged)
|
|
54
|
+
return merged
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["LocalPromotedMemoryStore", "DEFAULT_PROMOTED_ROOT"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cherry-docs
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Local-first AI memory for Claude Code — capture, distill, and retrieve project knowledge automatically.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/freebeiro/cherry-docs
|
|
7
|
+
Project-URL: Repository, https://github.com/freebeiro/cherry-docs
|
|
8
|
+
Keywords: ai,memory,claude,mcp,developer-tools
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: mcp>=1.0
|
|
21
|
+
Provides-Extra: anthropic
|
|
22
|
+
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
26
|
+
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.9; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# CherryDocs
|
|
31
|
+
|
|
32
|
+
CherryDocs is a local-first memory layer for AI coding chats.
|
|
33
|
+
|
|
34
|
+
The intended flow is simple:
|
|
35
|
+
|
|
36
|
+
1. connect your AI client to CherryDocs via MCP
|
|
37
|
+
2. start with `onboard()` — get project context in one call
|
|
38
|
+
3. work normally in the repo
|
|
39
|
+
4. ask `answer()` when continuity matters
|
|
40
|
+
|
|
41
|
+
## What It Does
|
|
42
|
+
|
|
43
|
+
CherryDocs helps an AI answer questions like:
|
|
44
|
+
|
|
45
|
+
- Why is this code here?
|
|
46
|
+
- What did we already try?
|
|
47
|
+
- What failed before?
|
|
48
|
+
- How do I continue this work without rereading everything?
|
|
49
|
+
|
|
50
|
+
The core product shape is:
|
|
51
|
+
|
|
52
|
+
- `onboard()` for the smallest useful startup view
|
|
53
|
+
- passive capture of work traces via Claude Code hooks
|
|
54
|
+
- local Ollama distillation of sessions into durable project memory
|
|
55
|
+
- `answer()` for retrieval when a new chat needs context
|
|
56
|
+
|
|
57
|
+
## Current Architecture
|
|
58
|
+
|
|
59
|
+
- **Durable memory store**: local JSON at `~/.cherrydocs/promoted/{project_id}.json`
|
|
60
|
+
- **Transport**: MCP via stdio (FastMCP) — 4 tools
|
|
61
|
+
- **Distillation**: local Ollama (qwen2.5:7b-instruct by default)
|
|
62
|
+
- **Capture**: Claude Code hooks + MCP log tools
|
|
63
|
+
|
|
64
|
+
CherryDocs is project-scoped first and branch-aware second.
|
|
65
|
+
|
|
66
|
+
## MCP Tools
|
|
67
|
+
|
|
68
|
+
| Tool | Purpose |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `onboard` | Session start — loads top memories + recent sessions |
|
|
71
|
+
| `log_activity` | Record a decision, fix, or insight to the capture buffer |
|
|
72
|
+
| `save_checkpoint` | Structured handoff — blind AI must be able to continue |
|
|
73
|
+
| `answer` | Query promoted memory for project questions |
|
|
74
|
+
|
|
75
|
+
## Setup
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install cherry-docs
|
|
79
|
+
cherry install # installs Claude Code hooks
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then add to your `.mcp.json`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"cherry-docs": {
|
|
88
|
+
"command": "cherry-docs-mcp"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Minimal AI Rule
|
|
95
|
+
|
|
96
|
+
```md
|
|
97
|
+
Use CherryDocs.
|
|
98
|
+
- On start: call `onboard()`.
|
|
99
|
+
- Work normally.
|
|
100
|
+
- Use `answer()` when history could change the decision.
|
|
101
|
+
- Use `log_activity()` when something important would otherwise be lost.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The canonical source for generated agent rules is [docs/agent_protocol.toml](docs/agent_protocol.toml).
|
|
105
|
+
|
|
106
|
+
## Workflow
|
|
107
|
+
|
|
108
|
+
In a new session:
|
|
109
|
+
|
|
110
|
+
1. Claude calls `onboard()` — gets top memories + recent session state
|
|
111
|
+
2. Work happens normally; hooks capture tool use and code changes
|
|
112
|
+
3. On git commit, auto-distillation fires via Ollama
|
|
113
|
+
4. Ask `answer("Why did we change this?")` in any future session
|
|
114
|
+
|
|
115
|
+
## What Works Today
|
|
116
|
+
|
|
117
|
+
- Local file-backed promoted memory (no cloud, no graph DB)
|
|
118
|
+
- MCP stdio server with 4 tools
|
|
119
|
+
- Claude Code hook-based passive capture
|
|
120
|
+
- Ollama distillation pipeline (per-session + commit-triggered)
|
|
121
|
+
- `cherry eval` — heuristic + LLM judge for memory quality
|
|
122
|
+
- `cherry why <file>` — show memories anchored to commits touching a file
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install -e .
|
|
128
|
+
python -m pytest tests/ -q
|
|
129
|
+
python scripts/check_size_limits.py
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For PR hardening:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bash scripts/local_pr_gate.sh fast
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
- [Product Brief](docs/PRODUCT_BRIEF.md)
|
|
141
|
+
- [System Deep Dive](docs/SYSTEM_DEEP_DIVE.md)
|
|
142
|
+
|
|
143
|
+
> Would another AI actually want to keep this on because it helps achieve the goal?
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
app/repo_scope.py,sha256=6uX3xSfEy96jbgztw7vRjwAOQaKsz2WeljcEOrhlQuA,842
|
|
3
|
+
app/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
app/services/agent_protocol.py,sha256=DEXm3pDRoX6ahMuT_oNVMXUZV50-el9MVq6jPSAR2S8,2085
|
|
5
|
+
app/services/auto_promote_sessions.py,sha256=-B_6qC-zliZ2JjzyGsYBLvYOY-fcIczeYfSdDlcCpB8,8615
|
|
6
|
+
app/services/capture_adapters.py,sha256=ZBbC0gIkkFs39Pwf7s2I8u2vQ7zeXvNWoTdBsf-sNl8,2465
|
|
7
|
+
app/services/capture_core.py,sha256=M-EmGs7tKDMpW75RDrrvsQJPEfSdUXx6j0T3oYFLRTY,4931
|
|
8
|
+
app/services/internal_memory_agent.py,sha256=qH6En8u97Tn2yPSYPiOTIP_h5dOH0uhrenjUVX30mtY,7878
|
|
9
|
+
app/services/memory_evidence.py,sha256=31ok2CdnDnt_QhxWJURUT_69q6NgldrQi9k96Vcf9pY,3255
|
|
10
|
+
app/services/memory_extraction_normalize.py,sha256=B8_KnjkyW7aAwORFBvzd2vl_QW5n6_DpcPnQr03SfiE,4898
|
|
11
|
+
app/services/memory_lifecycle.py,sha256=DLvu3rhHeONDE5CEVIaOxKQHNAQOdK34tdlrebOIoyw,9713
|
|
12
|
+
app/services/memory_profiles.py,sha256=3nnefae4nX3OX2Ko7RYlsYeQkJrtE6BA5OD-TtBKVpE,4346
|
|
13
|
+
app/services/memory_providers.py,sha256=8hAFljCIprMqpbSJ0D6LP8bMGoL3KiXUGE8_I-EDTEE,3851
|
|
14
|
+
app/services/memory_retrieval.py,sha256=hyy0g_FU-oa_QpjTqZaOcy-OR865n2xwX9H5MwSBWAI,12296
|
|
15
|
+
app/services/memory_retrieval_scoring.py,sha256=bgnfW8F55mOU2NH_oQHh9adqvQ9ej8F8Uj8dB3-hUmw,3121
|
|
16
|
+
app/services/memory_retrieval_text.py,sha256=jfAsppy2R5JaDRR9bBoTUG-fV1rbeK61FBWKIGnCtsE,2990
|
|
17
|
+
app/services/memory_similarity.py,sha256=tQlMBp7eb3AXQNOaQ3RhxX5S5EyB5wQSU7uUu6tqZTg,4994
|
|
18
|
+
app/services/privacy.py,sha256=7yGNofkyogYeEJdp7DVSGuZkwRC8mqD0xrOsb3fxMd0,3097
|
|
19
|
+
app/services/promoted_memory_answer.py,sha256=TJF6Vm-wdijSabj49gS3zz467IpmPGxKt4yDfkqp3pE,5161
|
|
20
|
+
app/services/promoted_memory_pipeline.py,sha256=Iyy38z23emWzTEgpxJafDEpRGJie0j9n2uwQ7FjKZBU,6869
|
|
21
|
+
app/services/promoted_memory_store.py,sha256=jrruqbUpRpLmL1B9mPBnvhnfXN5B7bUIvZS-RJV1pnw,1961
|
|
22
|
+
cherrydocs/__init__.py,sha256=9C6yXI1iBUV9yVf0AxLb4U6qa_X4Nl8blK8PuhX-Cng,83
|
|
23
|
+
cherrydocs/cli.py,sha256=MNr-7mC6sej7Mb8TKgAjUNDURRRTaXqLYOP7MAY6QI0,7762
|
|
24
|
+
cherrydocs/hook.py,sha256=xdIgmvSvf_Z0YcEJCVvSRjNcEeDipkip7GvqUB-01_o,812
|
|
25
|
+
cherrydocs/mcp.py,sha256=2Idg4cp-NIAPzoa8uW9Bx1Xu5l2FgFu0J-5yIJlshTY,578
|
|
26
|
+
scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
scripts/auto_promote_capture.py,sha256=W8m4eaVG-GeQ83SV3Y_NhO3IF_pKRcYxo60F9DkeVwI,2670
|
|
28
|
+
scripts/check_size_limits.py,sha256=Ewh99opzN485UCgRkNNE5TpgdXkgBtayXstWAm2wSOw,3350
|
|
29
|
+
scripts/ci_auto_capture.py,sha256=6J1MhOZczAJ21M6uUksLo0alUQzNNa3Wv7lCEkyrqtE,11564
|
|
30
|
+
scripts/coverage_regression_gate.py,sha256=NYbst5mEw8mHwozoaSGT0-ZUeViwILDOvFRUBxGaKfE,4298
|
|
31
|
+
scripts/eval_projects.py,sha256=PwOM6xXdgXk0Om4qF1dVnx2Og8Rf9Dky_IxLJ-_gig4,8359
|
|
32
|
+
scripts/install.py,sha256=RnP69G5eqgO91hL1mbCsM2LWLZFEPXXVvRxy2XPG2pU,7125
|
|
33
|
+
scripts/pr_gate_report.py,sha256=g1Iql-8vAdRdWsT5SGhYeAD6uejlBG8tFJf-b3RO_To,10985
|
|
34
|
+
scripts/promptfoo_regression_gate.py,sha256=hy50dHqPvMpA19UsUiDK41qZcTN5Sg_EakEAXA67Nhk,5861
|
|
35
|
+
scripts/render_agent_prompts.py,sha256=-SP12hhw4u9s-cx79qyA8GvVEVDa12vckMIVV2R-Qzw,1948
|
|
36
|
+
scripts/claude_hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
scripts/claude_hooks/state_manager.py,sha256=DCsespEIVl015Zz4DYbaRc_52yg5L8kAbIjLrarMk9Q,18418
|
|
38
|
+
cherry_docs-0.2.0.dist-info/METADATA,sha256=_SAeCP9SSfNBmYE3Xd_HDeVm68U1i48xSAWsiFlUupM,4137
|
|
39
|
+
cherry_docs-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
40
|
+
cherry_docs-0.2.0.dist-info/entry_points.txt,sha256=8dydb3U_sVhcXNytyTUFbwVr3zpT_98TN4yLvQZOqvo,120
|
|
41
|
+
cherry_docs-0.2.0.dist-info/top_level.txt,sha256=wtvfFLYf6tx66P5KqfU0pBNxSoctKviWbW46IxsLRe0,23
|
|
42
|
+
cherry_docs-0.2.0.dist-info/RECORD,,
|
cherrydocs/__init__.py
ADDED
cherrydocs/cli.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Entry point for the `cherry` CLI command.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
cherry install — wire CherryDocs into Claude Code globally
|
|
5
|
+
cherry status — show current hook + MCP health
|
|
6
|
+
cherry eval — evaluate memory quality across all projects
|
|
7
|
+
cherry uninstall — remove global hooks and MCP server entry
|
|
8
|
+
cherry why <file> — show memories anchored to commits that touched <file>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_PKG_ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
if str(_PKG_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_PKG_ROOT))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
args = sys.argv[1:]
|
|
23
|
+
if not args or args[0] in ("-h", "--help"):
|
|
24
|
+
_usage()
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
|
|
27
|
+
cmd = args[0]
|
|
28
|
+
if cmd == "install":
|
|
29
|
+
from scripts.install import main as _install # noqa: PLC0415
|
|
30
|
+
sys.exit(_install())
|
|
31
|
+
elif cmd == "status":
|
|
32
|
+
_status()
|
|
33
|
+
elif cmd == "eval":
|
|
34
|
+
from scripts.eval_projects import main as _eval # noqa: PLC0415
|
|
35
|
+
sys.argv = ["cherry", *args[1:]]
|
|
36
|
+
sys.exit(_eval())
|
|
37
|
+
elif cmd == "uninstall":
|
|
38
|
+
_uninstall()
|
|
39
|
+
elif cmd == "why":
|
|
40
|
+
_why(args[1:])
|
|
41
|
+
else:
|
|
42
|
+
print(f"cherry: unknown command '{cmd}'", file=sys.stderr)
|
|
43
|
+
_usage()
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _usage() -> None:
|
|
48
|
+
print(
|
|
49
|
+
"Usage: cherry <command>\n"
|
|
50
|
+
"\n"
|
|
51
|
+
"Commands:\n"
|
|
52
|
+
" install Wire CherryDocs into Claude Code globally\n"
|
|
53
|
+
" status Show current hook + MCP health\n"
|
|
54
|
+
" eval Evaluate memory quality across all projects\n"
|
|
55
|
+
" eval --no-llm Heuristic only (no Ollama required)\n"
|
|
56
|
+
" eval --project X Evaluate a single project\n"
|
|
57
|
+
" uninstall Remove global hooks and MCP entry\n"
|
|
58
|
+
" why <file> Show memories anchored to commits that touched <file>\n"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _status() -> None:
|
|
63
|
+
import json
|
|
64
|
+
import os
|
|
65
|
+
import shutil
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
|
|
68
|
+
settings_path = Path.home() / ".claude" / "settings.json"
|
|
69
|
+
has_hooks = False
|
|
70
|
+
if settings_path.exists():
|
|
71
|
+
try:
|
|
72
|
+
d = json.loads(settings_path.read_text())
|
|
73
|
+
hooks = d.get("hooks", {})
|
|
74
|
+
has_hooks = any(
|
|
75
|
+
"state_manager" in h.get("command", "") or "cherry-hook" in h.get("command", "")
|
|
76
|
+
for event in hooks.values()
|
|
77
|
+
for group in event
|
|
78
|
+
for h in group.get("hooks", [])
|
|
79
|
+
)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
mcp_cmd = shutil.which("cherry-docs-mcp") or shutil.which("cherry-hook")
|
|
84
|
+
central = Path.home() / ".cherrydocs" / "capture"
|
|
85
|
+
|
|
86
|
+
explicit = os.getenv("CHERRY_DISTILL_PROVIDER", "").strip().lower()
|
|
87
|
+
if explicit == "anthropic" or (not explicit and os.getenv("ANTHROPIC_API_KEY")):
|
|
88
|
+
model = os.getenv("CHERRY_ANTHROPIC_MODEL", "claude-3-5-haiku-20241022")
|
|
89
|
+
provider_line = f"✓ Anthropic ({model})"
|
|
90
|
+
else:
|
|
91
|
+
model = os.getenv("CHERRY_OLLAMA_MODEL", "qwen2.5:7b-instruct")
|
|
92
|
+
provider_line = f"✓ Ollama local ({model}) — set ANTHROPIC_API_KEY for prod"
|
|
93
|
+
|
|
94
|
+
print(f"Hooks in ~/.claude/settings.json : {'✓' if has_hooks else '✗ missing — run cherry install'}")
|
|
95
|
+
print(f"cherry-docs-mcp in PATH : {'✓' if mcp_cmd else '✗ missing'}")
|
|
96
|
+
print(f"Central capture store : {'✓' if central.exists() else '✗ missing — run cherry install'}")
|
|
97
|
+
print(f"Distillation provider : {provider_line}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _uninstall() -> None:
|
|
101
|
+
import json
|
|
102
|
+
import subprocess
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
# Remove MCP
|
|
106
|
+
subprocess.run(["claude", "mcp", "remove", "cherry-docs", "-s", "user"], check=False)
|
|
107
|
+
print("→ Removed cherry-docs MCP (user scope)")
|
|
108
|
+
|
|
109
|
+
# Remove hooks from ~/.claude/settings.json
|
|
110
|
+
settings_path = Path.home() / ".claude" / "settings.json"
|
|
111
|
+
if settings_path.exists():
|
|
112
|
+
try:
|
|
113
|
+
d = json.loads(settings_path.read_text())
|
|
114
|
+
hooks = d.get("hooks", {})
|
|
115
|
+
for event in list(hooks.keys()):
|
|
116
|
+
hooks[event] = [
|
|
117
|
+
g for g in hooks[event]
|
|
118
|
+
if not any(
|
|
119
|
+
"state_manager" in h.get("command", "") or "cherry-hook" in h.get("command", "")
|
|
120
|
+
for h in g.get("hooks", [])
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
if not hooks[event]:
|
|
124
|
+
del hooks[event]
|
|
125
|
+
d["hooks"] = hooks
|
|
126
|
+
settings_path.write_text(json.dumps(d, indent=2))
|
|
127
|
+
print("→ Removed hooks from ~/.claude/settings.json")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f" ✗ Could not update settings: {e}")
|
|
130
|
+
print("✅ Uninstalled.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _why(args: list[str]) -> None:
|
|
134
|
+
"""Show memories anchored to commits that touched <file>."""
|
|
135
|
+
import json
|
|
136
|
+
import os
|
|
137
|
+
import subprocess
|
|
138
|
+
from pathlib import Path
|
|
139
|
+
|
|
140
|
+
if not args:
|
|
141
|
+
print("Usage: cherry why <file>", file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
target = args[0]
|
|
145
|
+
|
|
146
|
+
# Collect commit hashes that touched the file.
|
|
147
|
+
try:
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
["git", "log", "--pretty=format:%H %h", "--", target],
|
|
150
|
+
capture_output=True, text=True, check=True, timeout=5,
|
|
151
|
+
)
|
|
152
|
+
commit_pairs = [line.split() for line in result.stdout.strip().splitlines() if line.strip()]
|
|
153
|
+
long_hashes = {pair[0] for pair in commit_pairs if pair}
|
|
154
|
+
short_hashes = {pair[1] for pair in commit_pairs if len(pair) > 1}
|
|
155
|
+
all_hashes = long_hashes | short_hashes
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
print(f"cherry why: git log failed — {exc}", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
if not all_hashes:
|
|
161
|
+
print(f"No commits found touching '{target}' in this repository.")
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
|
|
164
|
+
# Detect project id.
|
|
165
|
+
try:
|
|
166
|
+
from app.repo_scope import normalize_project_id
|
|
167
|
+
from app.services.capture_core import capture_repo_context
|
|
168
|
+
ctx = capture_repo_context(os.getcwd())
|
|
169
|
+
project_id = normalize_project_id(ctx.get("repo") or Path(os.getcwd()).name)
|
|
170
|
+
except Exception:
|
|
171
|
+
project_id = Path(os.getcwd()).name
|
|
172
|
+
|
|
173
|
+
# Load promoted memories.
|
|
174
|
+
promoted_root = Path(os.environ.get("CHERRY_PROMOTED_ROOT", str(Path.home() / ".cherrydocs" / "promoted")))
|
|
175
|
+
store_file = promoted_root / f"{project_id}.json"
|
|
176
|
+
if not store_file.exists():
|
|
177
|
+
print(f"No promoted memories found for project '{project_id}' ({store_file}).")
|
|
178
|
+
sys.exit(0)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
records = json.loads(store_file.read_text(encoding="utf-8"))
|
|
182
|
+
if not isinstance(records, list):
|
|
183
|
+
records = records.get("records", [])
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
print(f"cherry why: could not read store — {exc}", file=sys.stderr)
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
|
|
188
|
+
# Filter by commit overlap.
|
|
189
|
+
hits = [
|
|
190
|
+
r for r in records
|
|
191
|
+
if isinstance(r, dict) and r.get("commit") and r["commit"] in all_hashes
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
if not hits:
|
|
195
|
+
print(
|
|
196
|
+
f"No commit-anchored memories found for '{target}' in project '{project_id}'.\n"
|
|
197
|
+
f"({len(all_hashes)} commit(s) checked, {len(records)} memories in store)"
|
|
198
|
+
)
|
|
199
|
+
sys.exit(0)
|
|
200
|
+
|
|
201
|
+
print(f"Memories anchored to commits touching '{target}' [{project_id}]:\n")
|
|
202
|
+
for record in hits:
|
|
203
|
+
kind = record.get("kind", "?")
|
|
204
|
+
mtype = record.get("memory_type", "?")
|
|
205
|
+
summary = record.get("summary", "")
|
|
206
|
+
rationale = record.get("rationale", "")
|
|
207
|
+
commit = record.get("commit", "")
|
|
208
|
+
confidence = record.get("confidence", 0.0)
|
|
209
|
+
print(f" [{mtype}/{kind}] {summary}")
|
|
210
|
+
if rationale:
|
|
211
|
+
print(f" why: {rationale}")
|
|
212
|
+
print(f" commit: {commit} confidence: {confidence:.2f}")
|
|
213
|
+
print()
|
cherrydocs/hook.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Entry point for the cherry-hook CLI command.
|
|
2
|
+
|
|
3
|
+
Delegates to scripts/claude_hooks/state_manager.py so Claude Code hooks work
|
|
4
|
+
when cherry-docs is installed as a package (not run from source).
|
|
5
|
+
|
|
6
|
+
Usage (set by Claude Code hooks):
|
|
7
|
+
cherry-hook session-start
|
|
8
|
+
cherry-hook post-tool-use
|
|
9
|
+
cherry-hook stop
|
|
10
|
+
cherry-hook reset
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Ensure the installed package root is on sys.path so app.* imports work.
|
|
19
|
+
_PKG_ROOT = Path(__file__).resolve().parent.parent
|
|
20
|
+
if str(_PKG_ROOT) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_PKG_ROOT))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
# Import and delegate — state_manager uses its own __file__ to find ROOT.
|
|
26
|
+
from scripts.claude_hooks import state_manager # noqa: PLC0415
|
|
27
|
+
sys.exit(state_manager.main())
|
cherrydocs/mcp.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Entry point for the cherry-docs-mcp CLI command.
|
|
2
|
+
|
|
3
|
+
Runs the MCP stdio server. Used when cherry-docs is installed as a package:
|
|
4
|
+
claude mcp add cherry-docs cherry-docs-mcp --scope user
|
|
5
|
+
|
|
6
|
+
Instead of the source-path form:
|
|
7
|
+
claude mcp add cherry-docs python /absolute/path/to/mcp_server.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_PKG_ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
if str(_PKG_ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_PKG_ROOT))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
import mcp_server # noqa: PLC0415
|
|
22
|
+
mcp_server.run()
|
scripts/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Auto-promote recent captured sessions into durable CherryDocs memory."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
if str(ROOT) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(ROOT))
|
|
15
|
+
|
|
16
|
+
from app.services.auto_promote_sessions import AutoPromotionPolicy, auto_promote_captured_sessions
|
|
17
|
+
from app.services.memory_providers import OllamaMemoryProvider, resolve_provider
|
|
18
|
+
from app.services.promoted_memory_store import DEFAULT_PROMOTED_ROOT
|
|
19
|
+
|
|
20
|
+
_HOME_CHERRY = Path.home() / ".cherrydocs"
|
|
21
|
+
_DEFAULT_BUFFER = os.environ.get("CHERRY_CAPTURE_BUFFER_DIR", str(_HOME_CHERRY / "capture"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parser() -> argparse.ArgumentParser:
|
|
25
|
+
parser = argparse.ArgumentParser(description="CherryDocs auto-promote recent captured sessions")
|
|
26
|
+
parser.add_argument("--project-id", required=True, help="Project id to promote into")
|
|
27
|
+
parser.add_argument("--buffer-dir", default=_DEFAULT_BUFFER, help="Capture buffer directory")
|
|
28
|
+
parser.add_argument("--promoted-root", default=DEFAULT_PROMOTED_ROOT, help="Promoted memory store root")
|
|
29
|
+
parser.add_argument("--model", default="qwen2.5:7b-instruct", help="Ollama model name")
|
|
30
|
+
parser.add_argument("--project-hint", help="Optional project label for the extraction prompt")
|
|
31
|
+
parser.add_argument("--memory-profile", default="default",
|
|
32
|
+
help="Prompt profile (default, noise_strict, verification_first)")
|
|
33
|
+
parser.add_argument("--branch", help="Optional branch filter")
|
|
34
|
+
parser.add_argument("--min-event-count", type=int, default=3)
|
|
35
|
+
parser.add_argument("--min-candidate-confidence", type=float, default=0.8)
|
|
36
|
+
parser.add_argument("--max-sessions", type=int, default=10)
|
|
37
|
+
parser.add_argument("--commit-hash", default=None, help="Git commit hash to anchor promoted memories to")
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> int:
|
|
42
|
+
args = _parser().parse_args()
|
|
43
|
+
report = auto_promote_captured_sessions(
|
|
44
|
+
project_id=args.project_id,
|
|
45
|
+
buffer_dir=args.buffer_dir,
|
|
46
|
+
promoted_root=args.promoted_root,
|
|
47
|
+
provider=OllamaMemoryProvider(model=args.model),
|
|
48
|
+
project_hint=args.project_hint,
|
|
49
|
+
memory_profile=args.memory_profile,
|
|
50
|
+
branch=args.branch,
|
|
51
|
+
commit=args.commit_hash or None,
|
|
52
|
+
policy=AutoPromotionPolicy(
|
|
53
|
+
min_event_count=args.min_event_count,
|
|
54
|
+
min_candidate_confidence=args.min_candidate_confidence,
|
|
55
|
+
max_sessions=args.max_sessions,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
print(json.dumps(report.model_dump(mode="json"), indent=2))
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main())
|