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.
Files changed (42) hide show
  1. app/__init__.py +0 -0
  2. app/repo_scope.py +24 -0
  3. app/services/__init__.py +0 -0
  4. app/services/agent_protocol.py +59 -0
  5. app/services/auto_promote_sessions.py +245 -0
  6. app/services/capture_adapters.py +89 -0
  7. app/services/capture_core.py +164 -0
  8. app/services/internal_memory_agent.py +214 -0
  9. app/services/memory_evidence.py +89 -0
  10. app/services/memory_extraction_normalize.py +134 -0
  11. app/services/memory_lifecycle.py +258 -0
  12. app/services/memory_profiles.py +88 -0
  13. app/services/memory_providers.py +113 -0
  14. app/services/memory_retrieval.py +327 -0
  15. app/services/memory_retrieval_scoring.py +106 -0
  16. app/services/memory_retrieval_text.py +113 -0
  17. app/services/memory_similarity.py +135 -0
  18. app/services/privacy.py +72 -0
  19. app/services/promoted_memory_answer.py +157 -0
  20. app/services/promoted_memory_pipeline.py +194 -0
  21. app/services/promoted_memory_store.py +57 -0
  22. cherry_docs-0.2.0.dist-info/METADATA +143 -0
  23. cherry_docs-0.2.0.dist-info/RECORD +42 -0
  24. cherry_docs-0.2.0.dist-info/WHEEL +5 -0
  25. cherry_docs-0.2.0.dist-info/entry_points.txt +4 -0
  26. cherry_docs-0.2.0.dist-info/top_level.txt +3 -0
  27. cherrydocs/__init__.py +3 -0
  28. cherrydocs/cli.py +213 -0
  29. cherrydocs/hook.py +27 -0
  30. cherrydocs/mcp.py +22 -0
  31. scripts/__init__.py +0 -0
  32. scripts/auto_promote_capture.py +63 -0
  33. scripts/check_size_limits.py +115 -0
  34. scripts/ci_auto_capture.py +289 -0
  35. scripts/claude_hooks/__init__.py +0 -0
  36. scripts/claude_hooks/state_manager.py +526 -0
  37. scripts/coverage_regression_gate.py +121 -0
  38. scripts/eval_projects.py +247 -0
  39. scripts/install.py +212 -0
  40. scripts/pr_gate_report.py +282 -0
  41. scripts/promptfoo_regression_gate.py +176 -0
  42. 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ cherry = cherrydocs.cli:main
3
+ cherry-docs-mcp = cherrydocs.mcp:main
4
+ cherry-hook = cherrydocs.hook:main
@@ -0,0 +1,3 @@
1
+ app
2
+ cherrydocs
3
+ scripts
cherrydocs/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """CherryDocs — local-first AI memory for Claude Code."""
2
+
3
+ __version__ = "0.1.0"
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())