context-keeper-mcp 0.1.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.
- context_keeper_mcp-0.1.0/.gitignore +2 -0
- context_keeper_mcp-0.1.0/CLAUDE.md +60 -0
- context_keeper_mcp-0.1.0/LICENSE +21 -0
- context_keeper_mcp-0.1.0/PKG-INFO +196 -0
- context_keeper_mcp-0.1.0/README.md +173 -0
- context_keeper_mcp-0.1.0/hooks/post_compact.py +134 -0
- context_keeper_mcp-0.1.0/hooks/pre_compact.py +74 -0
- context_keeper_mcp-0.1.0/pyproject.toml +51 -0
- context_keeper_mcp-0.1.0/server.py +869 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Context Keeper MCP Server
|
|
2
|
+
|
|
3
|
+
Context Keeper maintains project memory across Claude conversations: architectural decisions, pipeline flows, and constraints that must not be forgotten or violated.
|
|
4
|
+
|
|
5
|
+
## When to Record
|
|
6
|
+
|
|
7
|
+
### Record a Decision when:
|
|
8
|
+
- You and the user choose between multiple approaches
|
|
9
|
+
- A technical trade-off is made (e.g., "JSON over SQLite because human-editable")
|
|
10
|
+
- A library, pattern, or architecture is selected
|
|
11
|
+
- The user says "let's go with X" after discussing options
|
|
12
|
+
|
|
13
|
+
Call `record_decision` with summary, rationale, and alternatives considered. Always include constraints_created if the decision limits future choices.
|
|
14
|
+
|
|
15
|
+
### Record a Pipeline when:
|
|
16
|
+
- A multi-step workflow is established (build, deploy, data processing)
|
|
17
|
+
- Steps have ordering dependencies (A must happen before B)
|
|
18
|
+
- The user describes "the flow" or "the process"
|
|
19
|
+
|
|
20
|
+
Call `record_pipeline` with ordered steps. Include constraints like "never skip step 2" or "step 3 requires output from step 1."
|
|
21
|
+
|
|
22
|
+
### Record a Constraint when:
|
|
23
|
+
- The user says "never do X" or "always do Y"
|
|
24
|
+
- A gotcha or footgun is discovered ("running from source breaks the scheduler")
|
|
25
|
+
- A project convention is established ("all API responses use camelCase")
|
|
26
|
+
- An external requirement exists ("must support Python 3.12+")
|
|
27
|
+
|
|
28
|
+
Call `record_constraint` with the rule, reason, scope, and hardness. Use hardness=absolute for true invariants, advisory for preferences.
|
|
29
|
+
|
|
30
|
+
## When to Retrieve
|
|
31
|
+
|
|
32
|
+
### At conversation start:
|
|
33
|
+
1. Call `get_compaction_report` first. If the report shows discrepancies (missing or modified entries), surface them to the user before doing anything else. Missing entries may need to be re-recorded.
|
|
34
|
+
2. Then call `get_project_summary` to orient yourself on the project's decisions, pipelines, and constraints. This prevents you from suggesting changes that violate established patterns.
|
|
35
|
+
|
|
36
|
+
### Before making architectural changes:
|
|
37
|
+
Call `get_context` with tags or a query describing what you're about to change. Check for conflicting decisions or constraints before proposing changes.
|
|
38
|
+
|
|
39
|
+
### When the user asks "why did we...":
|
|
40
|
+
Call `get_context` with relevant tags to find the decision with its rationale.
|
|
41
|
+
|
|
42
|
+
### Before modifying a pipeline:
|
|
43
|
+
Call `get_context` with the pipeline name or tags to see the current flow and its constraints.
|
|
44
|
+
|
|
45
|
+
## When NOT to Record
|
|
46
|
+
- Trivial implementation details (variable names, formatting choices)
|
|
47
|
+
- Temporary workarounds that will be removed
|
|
48
|
+
- Information already in the code comments or README
|
|
49
|
+
- One-off debugging steps
|
|
50
|
+
|
|
51
|
+
## Staleness Management
|
|
52
|
+
Periodically (every few sessions or when the user asks), call `prune_stale` to find entries that haven't been verified recently. Present stale entries to the user and ask: "Is this still accurate?" Then either:
|
|
53
|
+
- Call `update_entry` to refresh verified_at (confirming it's still valid)
|
|
54
|
+
- Call `deprecate_entry` if it's no longer relevant
|
|
55
|
+
|
|
56
|
+
## Tags Convention
|
|
57
|
+
Use lowercase, hyphen-separated tags. Common categories:
|
|
58
|
+
- Component names: auth, api, database, ui, deployment
|
|
59
|
+
- Cross-cutting: architecture, security, performance, testing
|
|
60
|
+
- Project names: skillmatch, conductor (for cross-project references)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan Armstrong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: context-keeper-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that maintains project context (decisions, pipelines, constraints) across Claude conversations
|
|
5
|
+
Project-URL: Homepage, https://github.com/jarmstrong158/context-keeper
|
|
6
|
+
Project-URL: Repository, https://github.com/jarmstrong158/context-keeper
|
|
7
|
+
Project-URL: Issues, https://github.com/jarmstrong158/context-keeper/issues
|
|
8
|
+
Author-email: Jonathan Armstrong <jarmstrong158@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude,context,decisions,mcp,memory,model-context-protocol
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<!-- mcp-name: io.github.jarmstrong158/context-keeper -->
|
|
25
|
+
|
|
26
|
+
# Context Keeper
|
|
27
|
+
|
|
28
|
+
Project memory for Claude. Records design decisions, pipeline flows, and constraints so Claude maintains context across conversations.
|
|
29
|
+
|
|
30
|
+
## The Problem
|
|
31
|
+
|
|
32
|
+
As conversations get long, Claude loses the "why" behind earlier decisions. New conversations start blank. This causes Claude to make changes that break established patterns — like rewriting a pipeline step it doesn't remember exists.
|
|
33
|
+
|
|
34
|
+
## The Solution
|
|
35
|
+
|
|
36
|
+
Context Keeper gives Claude 8 tools to record and retrieve structured project context:
|
|
37
|
+
|
|
38
|
+
| Tool | Purpose |
|
|
39
|
+
|------|---------|
|
|
40
|
+
| `record_decision` | Save a decision with rationale and alternatives |
|
|
41
|
+
| `record_pipeline` | Save a multi-step workflow with ordering |
|
|
42
|
+
| `record_constraint` | Save a rule with scope and enforcement level |
|
|
43
|
+
| `get_context` | Retrieve relevant entries by query, tags, scope, or ID |
|
|
44
|
+
| `get_project_summary` | Compact overview for conversation start |
|
|
45
|
+
| `update_entry` | Update any entry by ID |
|
|
46
|
+
| `deprecate_entry` | Retire an entry with reason |
|
|
47
|
+
| `prune_stale` | Find entries not verified recently |
|
|
48
|
+
| `get_compaction_report` | Check if last compaction lost any context |
|
|
49
|
+
|
|
50
|
+
All data stored as human-editable JSON files in `.context/` inside your project directory. Zero external dependencies.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install context-keeper
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Claude Code
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add --scope user context-keeper -- python /path/to/context-keeper/server.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Claude Desktop
|
|
65
|
+
|
|
66
|
+
Add to your `claude_desktop_config.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"context-keeper": {
|
|
72
|
+
"command": "python",
|
|
73
|
+
"args": ["/path/to/context-keeper/server.py"],
|
|
74
|
+
"env": {
|
|
75
|
+
"CONTEXT_KEEPER_PROJECT": "/path/to/your/project"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Set `CONTEXT_KEEPER_PROJECT` to the root of your project. If omitted, it uses the current working directory.
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
### Recording Context
|
|
87
|
+
|
|
88
|
+
When you make a design decision:
|
|
89
|
+
```
|
|
90
|
+
You: Let's use JSON files instead of SQLite for storage.
|
|
91
|
+
Claude: [calls record_decision with summary, rationale, and alternatives]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
When you establish a workflow:
|
|
95
|
+
```
|
|
96
|
+
You: The deploy pipeline is: run tests, build, push to registry, deploy.
|
|
97
|
+
Claude: [calls record_pipeline with ordered steps]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When you set a rule:
|
|
101
|
+
```
|
|
102
|
+
You: Never run Conductor from source. Always use the exe.
|
|
103
|
+
Claude: [calls record_constraint with rule, reason, and hardness=absolute]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Retrieving Context
|
|
107
|
+
|
|
108
|
+
At conversation start, Claude calls `get_project_summary` to see all active decisions, pipelines, and constraints. Before making changes, it calls `get_context` with relevant tags to check for conflicts.
|
|
109
|
+
|
|
110
|
+
### Relevance Scoring
|
|
111
|
+
|
|
112
|
+
Without embeddings or external services, Context Keeper scores entries using:
|
|
113
|
+
- **Tag match** — overlap between query and entry tags
|
|
114
|
+
- **Text match** — query words found in summary/rationale/rule text
|
|
115
|
+
- **Recency** — recently verified entries score higher
|
|
116
|
+
- **Status** — active entries prioritized over superseded
|
|
117
|
+
|
|
118
|
+
Results are capped by a configurable token budget (default: 4000 tokens).
|
|
119
|
+
|
|
120
|
+
## Claude Code Hook Setup
|
|
121
|
+
|
|
122
|
+
Context Keeper includes hooks that snapshot your context before Claude Code compaction and detect if anything was lost afterward.
|
|
123
|
+
|
|
124
|
+
Add to your Claude Code hooks config (`~/.claude/settings.json`):
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"hooks": {
|
|
129
|
+
"PreCompact": [
|
|
130
|
+
{
|
|
131
|
+
"matcher": "",
|
|
132
|
+
"hooks": [
|
|
133
|
+
{
|
|
134
|
+
"type": "command",
|
|
135
|
+
"command": "python /path/to/context-keeper/hooks/pre_compact.py"
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"Stop": [
|
|
141
|
+
{
|
|
142
|
+
"matcher": "",
|
|
143
|
+
"hooks": [
|
|
144
|
+
{
|
|
145
|
+
"type": "command",
|
|
146
|
+
"command": "python /path/to/context-keeper/hooks/post_compact.py"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Replace `/path/to/context-keeper` with the actual install path. Set `CONTEXT_KEEPER_PROJECT` env var if your project isn't in the current working directory.
|
|
156
|
+
|
|
157
|
+
At session start, Claude calls `get_compaction_report` to check if the last compaction lost any context entries. If discrepancies are found, they're surfaced before any work begins.
|
|
158
|
+
|
|
159
|
+
## Data Storage
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
your-project/
|
|
163
|
+
.context/
|
|
164
|
+
decisions.json # Design decisions with rationale
|
|
165
|
+
pipelines.json # Multi-step workflows
|
|
166
|
+
constraints.json # Rules and invariants
|
|
167
|
+
config.json # Token budget, stale threshold
|
|
168
|
+
compaction_snapshot.json # Pre-compaction snapshot (auto-generated)
|
|
169
|
+
compaction_report.json # Post-compaction diff report (auto-generated)
|
|
170
|
+
hook.log # Hook activity log
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
All files are human-readable JSON. You can edit them directly. IDs are sequential and readable: `dec-001`, `pipe-001`, `con-001`.
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
Create `.context/config.json` to customize:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"project_name": "my-project",
|
|
182
|
+
"token_budget": 4000,
|
|
183
|
+
"max_entry_tokens": 1000,
|
|
184
|
+
"stale_threshold_days": 30
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Cross-Project Context
|
|
189
|
+
|
|
190
|
+
Query another project's context by passing `project_dir`:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
Claude: [calls get_context with project_dir="/path/to/other-project"]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or tag entries with other project names for cross-referencing.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.jarmstrong158/context-keeper -->
|
|
2
|
+
|
|
3
|
+
# Context Keeper
|
|
4
|
+
|
|
5
|
+
Project memory for Claude. Records design decisions, pipeline flows, and constraints so Claude maintains context across conversations.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
As conversations get long, Claude loses the "why" behind earlier decisions. New conversations start blank. This causes Claude to make changes that break established patterns — like rewriting a pipeline step it doesn't remember exists.
|
|
10
|
+
|
|
11
|
+
## The Solution
|
|
12
|
+
|
|
13
|
+
Context Keeper gives Claude 8 tools to record and retrieve structured project context:
|
|
14
|
+
|
|
15
|
+
| Tool | Purpose |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| `record_decision` | Save a decision with rationale and alternatives |
|
|
18
|
+
| `record_pipeline` | Save a multi-step workflow with ordering |
|
|
19
|
+
| `record_constraint` | Save a rule with scope and enforcement level |
|
|
20
|
+
| `get_context` | Retrieve relevant entries by query, tags, scope, or ID |
|
|
21
|
+
| `get_project_summary` | Compact overview for conversation start |
|
|
22
|
+
| `update_entry` | Update any entry by ID |
|
|
23
|
+
| `deprecate_entry` | Retire an entry with reason |
|
|
24
|
+
| `prune_stale` | Find entries not verified recently |
|
|
25
|
+
| `get_compaction_report` | Check if last compaction lost any context |
|
|
26
|
+
|
|
27
|
+
All data stored as human-editable JSON files in `.context/` inside your project directory. Zero external dependencies.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install context-keeper
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Claude Code
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add --scope user context-keeper -- python /path/to/context-keeper/server.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Claude Desktop
|
|
42
|
+
|
|
43
|
+
Add to your `claude_desktop_config.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"context-keeper": {
|
|
49
|
+
"command": "python",
|
|
50
|
+
"args": ["/path/to/context-keeper/server.py"],
|
|
51
|
+
"env": {
|
|
52
|
+
"CONTEXT_KEEPER_PROJECT": "/path/to/your/project"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Set `CONTEXT_KEEPER_PROJECT` to the root of your project. If omitted, it uses the current working directory.
|
|
60
|
+
|
|
61
|
+
## How It Works
|
|
62
|
+
|
|
63
|
+
### Recording Context
|
|
64
|
+
|
|
65
|
+
When you make a design decision:
|
|
66
|
+
```
|
|
67
|
+
You: Let's use JSON files instead of SQLite for storage.
|
|
68
|
+
Claude: [calls record_decision with summary, rationale, and alternatives]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
When you establish a workflow:
|
|
72
|
+
```
|
|
73
|
+
You: The deploy pipeline is: run tests, build, push to registry, deploy.
|
|
74
|
+
Claude: [calls record_pipeline with ordered steps]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
When you set a rule:
|
|
78
|
+
```
|
|
79
|
+
You: Never run Conductor from source. Always use the exe.
|
|
80
|
+
Claude: [calls record_constraint with rule, reason, and hardness=absolute]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Retrieving Context
|
|
84
|
+
|
|
85
|
+
At conversation start, Claude calls `get_project_summary` to see all active decisions, pipelines, and constraints. Before making changes, it calls `get_context` with relevant tags to check for conflicts.
|
|
86
|
+
|
|
87
|
+
### Relevance Scoring
|
|
88
|
+
|
|
89
|
+
Without embeddings or external services, Context Keeper scores entries using:
|
|
90
|
+
- **Tag match** — overlap between query and entry tags
|
|
91
|
+
- **Text match** — query words found in summary/rationale/rule text
|
|
92
|
+
- **Recency** — recently verified entries score higher
|
|
93
|
+
- **Status** — active entries prioritized over superseded
|
|
94
|
+
|
|
95
|
+
Results are capped by a configurable token budget (default: 4000 tokens).
|
|
96
|
+
|
|
97
|
+
## Claude Code Hook Setup
|
|
98
|
+
|
|
99
|
+
Context Keeper includes hooks that snapshot your context before Claude Code compaction and detect if anything was lost afterward.
|
|
100
|
+
|
|
101
|
+
Add to your Claude Code hooks config (`~/.claude/settings.json`):
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"hooks": {
|
|
106
|
+
"PreCompact": [
|
|
107
|
+
{
|
|
108
|
+
"matcher": "",
|
|
109
|
+
"hooks": [
|
|
110
|
+
{
|
|
111
|
+
"type": "command",
|
|
112
|
+
"command": "python /path/to/context-keeper/hooks/pre_compact.py"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"Stop": [
|
|
118
|
+
{
|
|
119
|
+
"matcher": "",
|
|
120
|
+
"hooks": [
|
|
121
|
+
{
|
|
122
|
+
"type": "command",
|
|
123
|
+
"command": "python /path/to/context-keeper/hooks/post_compact.py"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Replace `/path/to/context-keeper` with the actual install path. Set `CONTEXT_KEEPER_PROJECT` env var if your project isn't in the current working directory.
|
|
133
|
+
|
|
134
|
+
At session start, Claude calls `get_compaction_report` to check if the last compaction lost any context entries. If discrepancies are found, they're surfaced before any work begins.
|
|
135
|
+
|
|
136
|
+
## Data Storage
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
your-project/
|
|
140
|
+
.context/
|
|
141
|
+
decisions.json # Design decisions with rationale
|
|
142
|
+
pipelines.json # Multi-step workflows
|
|
143
|
+
constraints.json # Rules and invariants
|
|
144
|
+
config.json # Token budget, stale threshold
|
|
145
|
+
compaction_snapshot.json # Pre-compaction snapshot (auto-generated)
|
|
146
|
+
compaction_report.json # Post-compaction diff report (auto-generated)
|
|
147
|
+
hook.log # Hook activity log
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
All files are human-readable JSON. You can edit them directly. IDs are sequential and readable: `dec-001`, `pipe-001`, `con-001`.
|
|
151
|
+
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
Create `.context/config.json` to customize:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"project_name": "my-project",
|
|
159
|
+
"token_budget": 4000,
|
|
160
|
+
"max_entry_tokens": 1000,
|
|
161
|
+
"stale_threshold_days": 30
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Cross-Project Context
|
|
166
|
+
|
|
167
|
+
Query another project's context by passing `project_dir`:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
Claude: [calls get_context with project_dir="/path/to/other-project"]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or tag entries with other project names for cross-referencing.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Context Keeper — PostCompact hook.
|
|
3
|
+
|
|
4
|
+
Fires after Claude Code compaction (via Stop hook). Compares current context
|
|
5
|
+
state against the pre-compaction snapshot and reports any discrepancies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
CONTEXT_DIR_NAME = ".context"
|
|
14
|
+
PROJECT_DIR = os.environ.get("CONTEXT_KEEPER_PROJECT", os.getcwd())
|
|
15
|
+
CONTEXT_DIR = os.path.join(PROJECT_DIR, CONTEXT_DIR_NAME)
|
|
16
|
+
SNAPSHOT_PATH = os.path.join(CONTEXT_DIR, "compaction_snapshot.json")
|
|
17
|
+
REPORT_PATH = os.path.join(CONTEXT_DIR, "compaction_report.json")
|
|
18
|
+
LOG_PATH = os.path.join(CONTEXT_DIR, "hook.log")
|
|
19
|
+
|
|
20
|
+
FILES = {
|
|
21
|
+
"decisions": os.path.join(CONTEXT_DIR, "decisions.json"),
|
|
22
|
+
"pipelines": os.path.join(CONTEXT_DIR, "pipelines.json"),
|
|
23
|
+
"constraints": os.path.join(CONTEXT_DIR, "constraints.json"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def read_json(path):
|
|
28
|
+
if not os.path.exists(path):
|
|
29
|
+
return []
|
|
30
|
+
try:
|
|
31
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
32
|
+
data = json.load(f)
|
|
33
|
+
return data if isinstance(data, list) else []
|
|
34
|
+
except Exception:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def log(message):
|
|
39
|
+
try:
|
|
40
|
+
os.makedirs(CONTEXT_DIR, exist_ok=True)
|
|
41
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
42
|
+
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
|
43
|
+
f.write(f"[{ts}] {message}\n")
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def diff_entries(before, after):
|
|
49
|
+
"""Compare two entry dicts. Returns dict of changed fields."""
|
|
50
|
+
changes = {}
|
|
51
|
+
all_keys = set(before.keys()) | set(after.keys())
|
|
52
|
+
skip = {"verified_at", "updated_at"} # timestamps change naturally
|
|
53
|
+
for key in all_keys:
|
|
54
|
+
if key in skip:
|
|
55
|
+
continue
|
|
56
|
+
bval = before.get(key)
|
|
57
|
+
aval = after.get(key)
|
|
58
|
+
if bval != aval:
|
|
59
|
+
changes[key] = {"before": bval, "after": aval}
|
|
60
|
+
return changes
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main():
|
|
64
|
+
if not os.path.exists(SNAPSHOT_PATH):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with open(SNAPSHOT_PATH, "r", encoding="utf-8") as f:
|
|
69
|
+
snapshot = json.load(f)
|
|
70
|
+
except Exception:
|
|
71
|
+
log("POST_COMPACT: Could not read snapshot file")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
missing = []
|
|
75
|
+
modified = []
|
|
76
|
+
|
|
77
|
+
for type_name, before_entries in snapshot.get("entries", {}).items():
|
|
78
|
+
path = FILES.get(type_name)
|
|
79
|
+
if not path:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
after_entries = read_json(path)
|
|
83
|
+
after_by_id = {e.get("id"): e for e in after_entries}
|
|
84
|
+
|
|
85
|
+
for before_entry in before_entries:
|
|
86
|
+
eid = before_entry.get("id")
|
|
87
|
+
if not eid:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
after_entry = after_by_id.get(eid)
|
|
91
|
+
if after_entry is None:
|
|
92
|
+
missing.append({
|
|
93
|
+
"type": type_name,
|
|
94
|
+
"entry": before_entry,
|
|
95
|
+
})
|
|
96
|
+
else:
|
|
97
|
+
changes = diff_entries(before_entry, after_entry)
|
|
98
|
+
if changes:
|
|
99
|
+
modified.append({
|
|
100
|
+
"type": type_name,
|
|
101
|
+
"id": eid,
|
|
102
|
+
"changes": changes,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
has_discrepancies = len(missing) > 0 or len(modified) > 0
|
|
106
|
+
status = "discrepancies_found" if has_discrepancies else "clean"
|
|
107
|
+
|
|
108
|
+
report = {
|
|
109
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
110
|
+
"snapshot_timestamp": snapshot.get("timestamp"),
|
|
111
|
+
"status": status,
|
|
112
|
+
"missing_entries": missing,
|
|
113
|
+
"modified_entries": modified,
|
|
114
|
+
"missing_count": len(missing),
|
|
115
|
+
"modified_count": len(modified),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
os.makedirs(CONTEXT_DIR, exist_ok=True)
|
|
119
|
+
with open(REPORT_PATH, "w", encoding="utf-8") as f:
|
|
120
|
+
json.dump(report, f, indent=2)
|
|
121
|
+
|
|
122
|
+
if has_discrepancies:
|
|
123
|
+
print(f"[Context Keeper] WARNING: Compaction discrepancies detected!", file=sys.stderr)
|
|
124
|
+
print(f" Missing entries: {len(missing)}", file=sys.stderr)
|
|
125
|
+
print(f" Modified entries: {len(modified)}", file=sys.stderr)
|
|
126
|
+
print(f" Report: {REPORT_PATH}", file=sys.stderr)
|
|
127
|
+
print(f" Call get_compaction_report to review details.", file=sys.stderr)
|
|
128
|
+
log(f"POST_COMPACT: DISCREPANCIES — {len(missing)} missing, {len(modified)} modified")
|
|
129
|
+
else:
|
|
130
|
+
log("POST_COMPACT: clean — no discrepancies")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Context Keeper — PreCompact hook.
|
|
3
|
+
|
|
4
|
+
Fires before Claude Code compaction. Snapshots all active context entries
|
|
5
|
+
so post_compact.py can detect if anything was lost.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
CONTEXT_DIR_NAME = ".context"
|
|
14
|
+
PROJECT_DIR = os.environ.get("CONTEXT_KEEPER_PROJECT", os.getcwd())
|
|
15
|
+
CONTEXT_DIR = os.path.join(PROJECT_DIR, CONTEXT_DIR_NAME)
|
|
16
|
+
SNAPSHOT_PATH = os.path.join(CONTEXT_DIR, "compaction_snapshot.json")
|
|
17
|
+
LOG_PATH = os.path.join(CONTEXT_DIR, "hook.log")
|
|
18
|
+
|
|
19
|
+
FILES = {
|
|
20
|
+
"decisions": os.path.join(CONTEXT_DIR, "decisions.json"),
|
|
21
|
+
"pipelines": os.path.join(CONTEXT_DIR, "pipelines.json"),
|
|
22
|
+
"constraints": os.path.join(CONTEXT_DIR, "constraints.json"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_json(path):
|
|
27
|
+
if not os.path.exists(path):
|
|
28
|
+
return []
|
|
29
|
+
try:
|
|
30
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
return data if isinstance(data, list) else []
|
|
33
|
+
except Exception:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def log(message):
|
|
38
|
+
try:
|
|
39
|
+
os.makedirs(CONTEXT_DIR, exist_ok=True)
|
|
40
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
41
|
+
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
|
42
|
+
f.write(f"[{ts}] {message}\n")
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
if not os.path.exists(CONTEXT_DIR):
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
snapshot = {
|
|
52
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
53
|
+
"entries": {},
|
|
54
|
+
"counts": {},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for type_name, path in FILES.items():
|
|
58
|
+
entries = read_json(path)
|
|
59
|
+
active = [e for e in entries if e.get("status", "active") != "deprecated"]
|
|
60
|
+
snapshot["entries"][type_name] = active
|
|
61
|
+
snapshot["counts"][type_name] = len(active)
|
|
62
|
+
|
|
63
|
+
total = sum(snapshot["counts"].values())
|
|
64
|
+
|
|
65
|
+
os.makedirs(CONTEXT_DIR, exist_ok=True)
|
|
66
|
+
with open(SNAPSHOT_PATH, "w", encoding="utf-8") as f:
|
|
67
|
+
json.dump(snapshot, f, indent=2)
|
|
68
|
+
|
|
69
|
+
counts_str = ", ".join(f"{k}={v}" for k, v in snapshot["counts"].items())
|
|
70
|
+
log(f"PRE_COMPACT: {total} active entries ({counts_str})")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|