context-handoff 0.1.1__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_handoff-0.1.1/LICENSE +21 -0
- context_handoff-0.1.1/PKG-INFO +182 -0
- context_handoff-0.1.1/README.md +161 -0
- context_handoff-0.1.1/pyproject.toml +41 -0
- context_handoff-0.1.1/setup.cfg +4 -0
- context_handoff-0.1.1/src/context_handoff/__init__.py +3 -0
- context_handoff-0.1.1/src/context_handoff/__main__.py +58 -0
- context_handoff-0.1.1/src/context_handoff/config.py +90 -0
- context_handoff-0.1.1/src/context_handoff/meter.py +177 -0
- context_handoff-0.1.1/src/context_handoff/prices.py +66 -0
- context_handoff-0.1.1/src/context_handoff/report.py +50 -0
- context_handoff-0.1.1/src/context_handoff/resume.py +63 -0
- context_handoff-0.1.1/src/context_handoff/transcript.py +97 -0
- context_handoff-0.1.1/src/context_handoff.egg-info/PKG-INFO +182 -0
- context_handoff-0.1.1/src/context_handoff.egg-info/SOURCES.txt +18 -0
- context_handoff-0.1.1/src/context_handoff.egg-info/dependency_links.txt +1 -0
- context_handoff-0.1.1/src/context_handoff.egg-info/entry_points.txt +2 -0
- context_handoff-0.1.1/src/context_handoff.egg-info/top_level.txt +1 -0
- context_handoff-0.1.1/tests/test_context_handoff.py +227 -0
- context_handoff-0.1.1/tests/test_versions.py +34 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yauhen Bichel
|
|
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,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: context-handoff
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Keep Claude Code sessions cheap: meter cost, write a handoff note, resume after /clear
|
|
5
|
+
Author: Yauhen Bichel
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YauhenBichel/context-handoff
|
|
8
|
+
Project-URL: Changelog, https://github.com/YauhenBichel/context-handoff/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/YauhenBichel/context-handoff/issues
|
|
10
|
+
Keywords: claude,claude-code,llm,cost,context,handoff
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# context-handoff
|
|
23
|
+
|
|
24
|
+
[](https://github.com/YauhenBichel/context-handoff/actions/workflows/ci.yml)
|
|
25
|
+
[](https://github.com/YauhenBichel/context-handoff/releases)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
A Claude Code plugin that keeps long sessions from getting expensive: it shows
|
|
29
|
+
what each turn costs, writes a handoff note when the session grows, and resumes
|
|
30
|
+
from it after `/clear`.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/plugin marketplace add YauhenBichel/context-handoff
|
|
34
|
+
/plugin install context-handoff@context-handoff
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Python 3.10+ as `python3`, nothing else. Details in [Install](#install).
|
|
38
|
+
|
|
39
|
+
Every Claude Code turn re-sends the whole conversation. While the prompt cache
|
|
40
|
+
is warm that re-read is cheap; after a pause the whole context is written to
|
|
41
|
+
the cache again at up to 2× the input price. A 400K-token session on Claude
|
|
42
|
+
Opus 5.5 pays about $3.20 for one such cold turn, before Claude writes a word.
|
|
43
|
+
Long sessions also get slower and lose focus.
|
|
44
|
+
|
|
45
|
+
context-handoff watches what each turn costs. When a session gets too big or
|
|
46
|
+
too expensive, Claude writes a short handoff note at the end of the current
|
|
47
|
+
task. You run `/clear`, type `go`, and a fresh, small session continues from
|
|
48
|
+
the note.
|
|
49
|
+
|
|
50
|
+
What you see above a prompt once a session is past the warn limit:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
[context-handoff] 182K context, last turn $0.61, session $14.20, a cold turn would cost $1.46
|
|
54
|
+
(claude-opus-5-5 prices). Claude will write a handoff note after this task; then /clear and type go.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
1. **Meter** (`UserPromptSubmit` hook). Before each prompt it reads the
|
|
60
|
+
session transcript and computes, at API list prices:
|
|
61
|
+
- the context the next turn will re-send,
|
|
62
|
+
- what the last turn cost,
|
|
63
|
+
- what the whole session cost,
|
|
64
|
+
- what one cold turn would cost now.
|
|
65
|
+
2. **Auto handoff**. Over a warn limit, it asks Claude (once per session) to
|
|
66
|
+
write a handoff note when the current task is done.
|
|
67
|
+
3. **Block**. Over a block limit, it holds the prompt until you type `handoff`
|
|
68
|
+
or add `#override`.
|
|
69
|
+
4. **Resume** (`SessionStart` hook, after `/clear`). It loads the project's
|
|
70
|
+
newest unused note into the new session, so you type `go` instead of
|
|
71
|
+
pasting a prompt. Each note is loaded once.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
In Claude Code:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
/plugin marketplace add YauhenBichel/context-handoff
|
|
79
|
+
/plugin install context-handoff@context-handoff
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or from a terminal:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
claude plugin marketplace add YauhenBichel/context-handoff
|
|
86
|
+
claude plugin install context-handoff@context-handoff
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Restart Claude Code (or start a new session) to load the hooks. The meter
|
|
90
|
+
line appears from the second prompt of a session.
|
|
91
|
+
|
|
92
|
+
Requires Python 3.10 or newer on `PATH` as `python3`. No other dependencies.
|
|
93
|
+
|
|
94
|
+
### Without Claude Code plugins
|
|
95
|
+
|
|
96
|
+
The same code is on PyPI. It gives the `context-handoff` command for reports
|
|
97
|
+
and checks, for example in a shell or on a machine where you don't use the
|
|
98
|
+
plugin:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
pipx install context-handoff # or: uv tool install context-handoff
|
|
102
|
+
context-handoff report 14
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The hooks still come from the plugin above; the package does not install them.
|
|
106
|
+
|
|
107
|
+
Update or remove:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
claude plugin marketplace update context-handoff
|
|
111
|
+
claude plugin update context-handoff@context-handoff
|
|
112
|
+
claude plugin uninstall context-handoff@context-handoff
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Use
|
|
116
|
+
|
|
117
|
+
- **Normal flow**: keep working. When the meter asks, Claude writes the note
|
|
118
|
+
after the current task and tells you. Run `/clear`, then type `go`.
|
|
119
|
+
- **By hand**: `/context-handoff:handoff`, then `/clear` and `go`.
|
|
120
|
+
- **Past a block**: start the prompt with `handoff`, or add `#override`.
|
|
121
|
+
- **Check a session**: `python3 bin/context-handoff check <transcript.jsonl>`.
|
|
122
|
+
- **Report** of the last N days (default 7):
|
|
123
|
+
`python3 bin/context-handoff report 14`.
|
|
124
|
+
|
|
125
|
+
## Settings
|
|
126
|
+
|
|
127
|
+
Defaults, then `~/.claude/context-handoff.json`, then environment variables
|
|
128
|
+
(`CONTEXT_HANDOFF_` + the key in upper case). A limit of 0 turns it off.
|
|
129
|
+
|
|
130
|
+
| Key | Default | Meaning |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `warn_k` | 150 | context size (thousands of tokens) that triggers a handoff |
|
|
133
|
+
| `block_k` | 400 | context size that blocks prompts |
|
|
134
|
+
| `warn_turn_usd` | 0.50 | cost of the last turn that triggers a handoff |
|
|
135
|
+
| `block_turn_usd` | 3.00 | cost of the last turn that blocks prompts |
|
|
136
|
+
| `warn_session_usd` | 20 | session total that triggers a handoff |
|
|
137
|
+
| `auto_handoff` | true | Claude writes the note itself; false = only a warning |
|
|
138
|
+
| `max_age_min` | 180 | notes older than this are not resumed |
|
|
139
|
+
| `handoff_dir` | `~/.claude/handoffs` | where notes go |
|
|
140
|
+
| `state_dir` | `~/.claude/context-handoff` | log (`meter.jsonl`) and per-session markers |
|
|
141
|
+
| `prices` | see [prices.py](src/context_handoff/prices.py) | `{"model-prefix": [input, output, cache_read]}` per million tokens |
|
|
142
|
+
|
|
143
|
+
Example `~/.claude/context-handoff.json`:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{ "warn_turn_usd": 1.0, "warn_session_usd": 50, "prices": { "claude-opus-5-5": [4.0, 20.0, 0.20] } }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Prices
|
|
150
|
+
|
|
151
|
+
The default table holds Anthropic's first-party API list prices as of November
|
|
152
|
+
2026. Cache writes are priced at 1.25× input (5-minute TTL) or 2× input (1-hour
|
|
153
|
+
TTL). On a Claude subscription you do not pay per token, but the same numbers
|
|
154
|
+
show how fast a session uses your limits. Prices change: override them in the
|
|
155
|
+
config file.
|
|
156
|
+
|
|
157
|
+
## Limits
|
|
158
|
+
|
|
159
|
+
- A plugin cannot run `/clear` for you. That one step stays manual.
|
|
160
|
+
- Notes are matched by the project folder's name, and only those newer than
|
|
161
|
+
`max_age_min` are loaded.
|
|
162
|
+
- The meter reads the transcript, so the first prompt of a session is not
|
|
163
|
+
metered.
|
|
164
|
+
- Costs are estimates from the transcript's usage fields, not your bill.
|
|
165
|
+
|
|
166
|
+
## Privacy
|
|
167
|
+
|
|
168
|
+
Everything runs locally. The plugin reads your transcripts in `~/.claude` and
|
|
169
|
+
writes notes and a log there. Nothing is sent anywhere. Do not put secrets in
|
|
170
|
+
handoff notes.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
python3 -m unittest discover -s tests
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# context-handoff
|
|
2
|
+
|
|
3
|
+
[](https://github.com/YauhenBichel/context-handoff/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/YauhenBichel/context-handoff/releases)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A Claude Code plugin that keeps long sessions from getting expensive: it shows
|
|
8
|
+
what each turn costs, writes a handoff note when the session grows, and resumes
|
|
9
|
+
from it after `/clear`.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/plugin marketplace add YauhenBichel/context-handoff
|
|
13
|
+
/plugin install context-handoff@context-handoff
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Python 3.10+ as `python3`, nothing else. Details in [Install](#install).
|
|
17
|
+
|
|
18
|
+
Every Claude Code turn re-sends the whole conversation. While the prompt cache
|
|
19
|
+
is warm that re-read is cheap; after a pause the whole context is written to
|
|
20
|
+
the cache again at up to 2× the input price. A 400K-token session on Claude
|
|
21
|
+
Opus 5.5 pays about $3.20 for one such cold turn, before Claude writes a word.
|
|
22
|
+
Long sessions also get slower and lose focus.
|
|
23
|
+
|
|
24
|
+
context-handoff watches what each turn costs. When a session gets too big or
|
|
25
|
+
too expensive, Claude writes a short handoff note at the end of the current
|
|
26
|
+
task. You run `/clear`, type `go`, and a fresh, small session continues from
|
|
27
|
+
the note.
|
|
28
|
+
|
|
29
|
+
What you see above a prompt once a session is past the warn limit:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
[context-handoff] 182K context, last turn $0.61, session $14.20, a cold turn would cost $1.46
|
|
33
|
+
(claude-opus-5-5 prices). Claude will write a handoff note after this task; then /clear and type go.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
1. **Meter** (`UserPromptSubmit` hook). Before each prompt it reads the
|
|
39
|
+
session transcript and computes, at API list prices:
|
|
40
|
+
- the context the next turn will re-send,
|
|
41
|
+
- what the last turn cost,
|
|
42
|
+
- what the whole session cost,
|
|
43
|
+
- what one cold turn would cost now.
|
|
44
|
+
2. **Auto handoff**. Over a warn limit, it asks Claude (once per session) to
|
|
45
|
+
write a handoff note when the current task is done.
|
|
46
|
+
3. **Block**. Over a block limit, it holds the prompt until you type `handoff`
|
|
47
|
+
or add `#override`.
|
|
48
|
+
4. **Resume** (`SessionStart` hook, after `/clear`). It loads the project's
|
|
49
|
+
newest unused note into the new session, so you type `go` instead of
|
|
50
|
+
pasting a prompt. Each note is loaded once.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
In Claude Code:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
/plugin marketplace add YauhenBichel/context-handoff
|
|
58
|
+
/plugin install context-handoff@context-handoff
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or from a terminal:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
claude plugin marketplace add YauhenBichel/context-handoff
|
|
65
|
+
claude plugin install context-handoff@context-handoff
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Restart Claude Code (or start a new session) to load the hooks. The meter
|
|
69
|
+
line appears from the second prompt of a session.
|
|
70
|
+
|
|
71
|
+
Requires Python 3.10 or newer on `PATH` as `python3`. No other dependencies.
|
|
72
|
+
|
|
73
|
+
### Without Claude Code plugins
|
|
74
|
+
|
|
75
|
+
The same code is on PyPI. It gives the `context-handoff` command for reports
|
|
76
|
+
and checks, for example in a shell or on a machine where you don't use the
|
|
77
|
+
plugin:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
pipx install context-handoff # or: uv tool install context-handoff
|
|
81
|
+
context-handoff report 14
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The hooks still come from the plugin above; the package does not install them.
|
|
85
|
+
|
|
86
|
+
Update or remove:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
claude plugin marketplace update context-handoff
|
|
90
|
+
claude plugin update context-handoff@context-handoff
|
|
91
|
+
claude plugin uninstall context-handoff@context-handoff
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Use
|
|
95
|
+
|
|
96
|
+
- **Normal flow**: keep working. When the meter asks, Claude writes the note
|
|
97
|
+
after the current task and tells you. Run `/clear`, then type `go`.
|
|
98
|
+
- **By hand**: `/context-handoff:handoff`, then `/clear` and `go`.
|
|
99
|
+
- **Past a block**: start the prompt with `handoff`, or add `#override`.
|
|
100
|
+
- **Check a session**: `python3 bin/context-handoff check <transcript.jsonl>`.
|
|
101
|
+
- **Report** of the last N days (default 7):
|
|
102
|
+
`python3 bin/context-handoff report 14`.
|
|
103
|
+
|
|
104
|
+
## Settings
|
|
105
|
+
|
|
106
|
+
Defaults, then `~/.claude/context-handoff.json`, then environment variables
|
|
107
|
+
(`CONTEXT_HANDOFF_` + the key in upper case). A limit of 0 turns it off.
|
|
108
|
+
|
|
109
|
+
| Key | Default | Meaning |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `warn_k` | 150 | context size (thousands of tokens) that triggers a handoff |
|
|
112
|
+
| `block_k` | 400 | context size that blocks prompts |
|
|
113
|
+
| `warn_turn_usd` | 0.50 | cost of the last turn that triggers a handoff |
|
|
114
|
+
| `block_turn_usd` | 3.00 | cost of the last turn that blocks prompts |
|
|
115
|
+
| `warn_session_usd` | 20 | session total that triggers a handoff |
|
|
116
|
+
| `auto_handoff` | true | Claude writes the note itself; false = only a warning |
|
|
117
|
+
| `max_age_min` | 180 | notes older than this are not resumed |
|
|
118
|
+
| `handoff_dir` | `~/.claude/handoffs` | where notes go |
|
|
119
|
+
| `state_dir` | `~/.claude/context-handoff` | log (`meter.jsonl`) and per-session markers |
|
|
120
|
+
| `prices` | see [prices.py](src/context_handoff/prices.py) | `{"model-prefix": [input, output, cache_read]}` per million tokens |
|
|
121
|
+
|
|
122
|
+
Example `~/.claude/context-handoff.json`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{ "warn_turn_usd": 1.0, "warn_session_usd": 50, "prices": { "claude-opus-5-5": [4.0, 20.0, 0.20] } }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Prices
|
|
129
|
+
|
|
130
|
+
The default table holds Anthropic's first-party API list prices as of November
|
|
131
|
+
2026. Cache writes are priced at 1.25× input (5-minute TTL) or 2× input (1-hour
|
|
132
|
+
TTL). On a Claude subscription you do not pay per token, but the same numbers
|
|
133
|
+
show how fast a session uses your limits. Prices change: override them in the
|
|
134
|
+
config file.
|
|
135
|
+
|
|
136
|
+
## Limits
|
|
137
|
+
|
|
138
|
+
- A plugin cannot run `/clear` for you. That one step stays manual.
|
|
139
|
+
- Notes are matched by the project folder's name, and only those newer than
|
|
140
|
+
`max_age_min` are loaded.
|
|
141
|
+
- The meter reads the transcript, so the first prompt of a session is not
|
|
142
|
+
metered.
|
|
143
|
+
- Costs are estimates from the transcript's usage fields, not your bill.
|
|
144
|
+
|
|
145
|
+
## Privacy
|
|
146
|
+
|
|
147
|
+
Everything runs locally. The plugin reads your transcripts in `~/.claude` and
|
|
148
|
+
writes notes and a log there. Nothing is sent anywhere. Do not put secrets in
|
|
149
|
+
handoff notes.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
python3 -m unittest discover -s tests
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "context-handoff"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Keep Claude Code sessions cheap: meter cost, write a handoff note, resume after /clear"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Yauhen Bichel" }]
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["claude", "claude-code", "llm", "cost", "context", "handoff"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Software Development",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
context-handoff = "context_handoff.__main__:main"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/YauhenBichel/context-handoff"
|
|
30
|
+
Changelog = "https://github.com/YauhenBichel/context-handoff/blob/main/CHANGELOG.md"
|
|
31
|
+
Issues = "https://github.com/YauhenBichel/context-handoff/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.dynamic]
|
|
34
|
+
version = { attr = "context_handoff.__version__" }
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 110
|
|
41
|
+
target-version = "py310"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""context-handoff entry point.
|
|
2
|
+
|
|
3
|
+
context-handoff meter UserPromptSubmit hook (reads hook JSON on stdin)
|
|
4
|
+
context-handoff resume SessionStart hook (reads hook JSON on stdin)
|
|
5
|
+
context-handoff report [days]
|
|
6
|
+
context-handoff check <transcript.jsonl> print what the meter sees
|
|
7
|
+
|
|
8
|
+
Hooks never fail the session: any error ends with no output and exit 0.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from . import config, meter, report, resume
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _hook(fn) -> int:
|
|
20
|
+
try:
|
|
21
|
+
event = json.load(sys.stdin)
|
|
22
|
+
out = fn(event, config.load())
|
|
23
|
+
except Exception as exc: # a hook must never break the session
|
|
24
|
+
print(f"context-handoff: {exc}", file=sys.stderr)
|
|
25
|
+
return 0
|
|
26
|
+
if out:
|
|
27
|
+
print(json.dumps(out))
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main(argv: list[str] | None = None) -> int:
|
|
32
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
33
|
+
cmd = argv[0] if argv else ""
|
|
34
|
+
if cmd == "meter":
|
|
35
|
+
return _hook(meter.run)
|
|
36
|
+
if cmd == "resume":
|
|
37
|
+
return _hook(resume.run)
|
|
38
|
+
if cmd == "report":
|
|
39
|
+
days = int(argv[1]) if len(argv) > 1 else 7
|
|
40
|
+
print(report.render(config.load(), days))
|
|
41
|
+
return 0
|
|
42
|
+
if cmd == "check" and len(argv) > 1:
|
|
43
|
+
cfg = config.load()
|
|
44
|
+
r = meter.measure(argv[1], cfg)
|
|
45
|
+
if r is None:
|
|
46
|
+
print("no Claude replies in that transcript")
|
|
47
|
+
return 1
|
|
48
|
+
block, warn = meter.reasons(r, cfg)
|
|
49
|
+
print(r.summary())
|
|
50
|
+
print("block:", "; ".join(block) or "no")
|
|
51
|
+
print("warn: ", "; ".join(warn) or "no")
|
|
52
|
+
return 0
|
|
53
|
+
print(__doc__.strip())
|
|
54
|
+
return 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
sys.exit(main())
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Settings: defaults, then ~/.claude/context-handoff.json, then environment.
|
|
2
|
+
|
|
3
|
+
Config file example (all keys optional):
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
"warn_k": 150, "block_k": 400,
|
|
7
|
+
"warn_turn_usd": 0.50, "block_turn_usd": 3.00,
|
|
8
|
+
"warn_session_usd": 20, "auto_handoff": true,
|
|
9
|
+
"prices": {"claude-opus-5-5": [4.0, 20.0, 0.20]}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Environment variables use the same names, upper case, with a
|
|
13
|
+
CONTEXT_HANDOFF_ prefix: CONTEXT_HANDOFF_WARN_TURN_USD=1.0 and so on.
|
|
14
|
+
A limit set to 0 is turned off.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from dataclasses import dataclass, field, fields
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from .prices import DEFAULT_PRICES
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _home() -> Path:
|
|
28
|
+
return Path(os.environ.get("HOME") or Path.home())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Config:
|
|
33
|
+
warn_k: int = 150 # context size (thousands of tokens) that triggers a handoff
|
|
34
|
+
block_k: int = 400 # context size that blocks prompts until a handoff
|
|
35
|
+
warn_turn_usd: float = 0.50 # cost of the last turn that triggers a handoff
|
|
36
|
+
block_turn_usd: float = 3.00 # cost of the last turn that blocks prompts
|
|
37
|
+
warn_session_usd: float = 20.0 # whole-session cost that triggers a handoff
|
|
38
|
+
auto_handoff: bool = True # ask Claude to write the note itself
|
|
39
|
+
max_age_min: int = 180 # notes older than this are not resumed
|
|
40
|
+
handoff_dir: str = ""
|
|
41
|
+
state_dir: str = ""
|
|
42
|
+
prices: dict = field(default_factory=lambda: dict(DEFAULT_PRICES))
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def notes(self) -> Path:
|
|
46
|
+
return Path(self.handoff_dir) if self.handoff_dir else _home() / ".claude" / "handoffs"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def state(self) -> Path:
|
|
50
|
+
return Path(self.state_dir) if self.state_dir else _home() / ".claude" / "context-handoff"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _coerce(kind, value):
|
|
54
|
+
if kind is bool or kind == "bool":
|
|
55
|
+
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
|
56
|
+
if kind is int or kind == "int":
|
|
57
|
+
return int(float(value))
|
|
58
|
+
if kind is float or kind == "float":
|
|
59
|
+
return float(value)
|
|
60
|
+
return str(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load(path: str | Path | None = None) -> Config:
|
|
64
|
+
cfg = Config()
|
|
65
|
+
path = Path(path) if path else Path(
|
|
66
|
+
os.environ.get("CONTEXT_HANDOFF_CONFIG") or _home() / ".claude" / "context-handoff.json"
|
|
67
|
+
)
|
|
68
|
+
data: dict = {}
|
|
69
|
+
if path.is_file():
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads(path.read_text())
|
|
72
|
+
except (OSError, ValueError):
|
|
73
|
+
data = {}
|
|
74
|
+
for f in fields(Config):
|
|
75
|
+
if f.name == "prices":
|
|
76
|
+
continue
|
|
77
|
+
env = os.environ.get("CONTEXT_HANDOFF_" + f.name.upper())
|
|
78
|
+
raw = env if env not in (None, "") else data.get(f.name)
|
|
79
|
+
if raw is not None:
|
|
80
|
+
try:
|
|
81
|
+
setattr(cfg, f.name, _coerce(f.type, raw))
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
pass
|
|
84
|
+
for prefix, row in (data.get("prices") or {}).items():
|
|
85
|
+
try:
|
|
86
|
+
i, o, r = (float(x) for x in row)
|
|
87
|
+
cfg.prices[prefix] = (i, o, r)
|
|
88
|
+
except (TypeError, ValueError):
|
|
89
|
+
pass
|
|
90
|
+
return cfg
|