claude-brink 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/config.example.json +22 -0
- package/hooks/hooks.json +19 -0
- package/package.json +21 -0
- package/src/adapters/claude.js +43 -0
- package/src/adapters/codex.js +80 -0
- package/src/arm-resume.ps1 +27 -0
- package/src/brink.js +168 -0
- package/src/cli.js +165 -0
- package/src/core/handoff.js +72 -0
- package/src/core/reset.js +46 -0
- package/src/core/resume.js +48 -0
- package/src/core/thresholds.js +44 -0
- package/src/install.js +79 -0
- package/src/notify.js +72 -0
- package/src/notify.ps1 +13 -0
- package/src/resume-once.ps1 +57 -0
- package/src/statusline-brink.js +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Linus Pisano
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Brink
|
|
2
|
+
|
|
3
|
+
> **Usage limits should be checkpoints, not walls.**
|
|
4
|
+
|
|
5
|
+
Brink keeps your AI coding agent's work alive across a usage lockout. When you're about to hit your limit, it gracefully pauses the agent and writes everything in flight to a `HANDOFF.md` — so a surprise mid-task lockout costs you nothing. Optional (experimental): auto-resume when the limit resets.
|
|
6
|
+
|
|
7
|
+
[](#status) [](#license) [](#proof-live-verified-2026-06-26)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g claude-brink
|
|
11
|
+
brink init # wires the hooks + sensor into Claude Code
|
|
12
|
+
brink doctor # verifies the whole chain on YOUR machine
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**What works where, today:**
|
|
16
|
+
|
|
17
|
+
| | Windows | macOS / Linux |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Auto-pause + `HANDOFF.md` (the core) | ✅ live-verified | ⚠️ should work — **not yet tested** |
|
|
20
|
+
| Usage statusline + warnings | ✅ | ⚠️ untested |
|
|
21
|
+
| Desktop toast | ✅ | ❌ not built yet (ntfy push works everywhere) |
|
|
22
|
+
| Auto-resume (**experimental**, off by default) | ✅ Task Scheduler | ❌ launchd/cron planned |
|
|
23
|
+
|
|
24
|
+
**Kill switch:** `brink off` disables everything instantly (`brink on` re-enables, `brink uninstall` removes cleanly and restores your statusline). Brink *blocks tool calls by design* — you always hold the off button.
|
|
25
|
+
|
|
26
|
+
<!-- DEMO GIF GOES HERE — replace with docs/demo.gif -->
|
|
27
|
+
<p align="center"><em>[ demo.gif — Brink pausing a live session and writing HANDOFF.md ]</em></p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## The problem
|
|
32
|
+
|
|
33
|
+
You're deep in a task. The agent is three files into a refactor, mid-thought, holding context you'll never reconstruct by hand. Then it hits the wall — the 5-hour limit, the weekly cap — and stops cold. Whatever it understood about the task is gone. When the limit resets, you start over, re-explaining, re-orienting, paying for the same ramp-up twice.
|
|
34
|
+
|
|
35
|
+
There are 25+ tools that show you your usage. Gauges, countdowns, menu-bar percentages, push notifiers. They tell you the wall is coming. **None of them keep your work alive when you hit it.**
|
|
36
|
+
|
|
37
|
+
That gap is the whole point of Brink. Usage display is a solved, crowded problem. **Work-continuity is not.**
|
|
38
|
+
|
|
39
|
+
## The idea: `HANDOFF.md` is the spine
|
|
40
|
+
|
|
41
|
+
Auto-pause is the mechanism. `HANDOFF.md` is the thing that matters.
|
|
42
|
+
|
|
43
|
+
It's a single Markdown file Brink writes for you — the one artifact that survives a pause, a crash, or a reset. It captures what the agent was doing and what comes next, in plain language, so any fresh session (yours or the agent's) can pick the thread back up.
|
|
44
|
+
|
|
45
|
+
Crucially, **Brink writes it deterministically from the session transcript.** It does not ask the model to write its own handoff and hope it complies. (We tried that — it's a good story, [below](#proof-live-verified-2026-06-26).)
|
|
46
|
+
|
|
47
|
+
> The examples below are illustrative. Because the handoff is generated mechanically (not by a model), the real file is plainer: your task statement verbatim, the recent tool actions, and the pause context — no synthesized "next steps" prose beyond what the transcript itself contains.
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
# Handoff — Refactor auth middleware
|
|
51
|
+
|
|
52
|
+
Paused at 99% of your 5h limit (resets 06:46 PM).
|
|
53
|
+
|
|
54
|
+
## The task
|
|
55
|
+
Extract the token-refresh logic out of `authMiddleware` into a
|
|
56
|
+
standalone `refreshSession()` helper, and add a unit test for the
|
|
57
|
+
expiry edge case.
|
|
58
|
+
|
|
59
|
+
## Recent actions before the pause
|
|
60
|
+
- Created `src/auth/refreshSession.ts` with the extracted logic
|
|
61
|
+
- Updated `authMiddleware.ts` to call the new helper
|
|
62
|
+
- Started writing `refreshSession.test.ts` (expiry case stubbed,
|
|
63
|
+
not yet asserting)
|
|
64
|
+
|
|
65
|
+
## Next steps
|
|
66
|
+
- Finish the expiry-edge-case assertion in `refreshSession.test.ts`
|
|
67
|
+
- Run the auth test suite and confirm green
|
|
68
|
+
- Remove the now-dead inline refresh block from `authMiddleware.ts`
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
When the limit resets, you (or an external scheduler) point a fresh agent at this file and it knows exactly where to stand.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Proof (live-verified 2026-06-26)
|
|
76
|
+
|
|
77
|
+
This isn't a design doc. The core was verified in a real headless Claude Code session.
|
|
78
|
+
|
|
79
|
+
**1. The pause is real, not theoretical.** Brink's `PreToolUse` deny fired at the configured threshold, **blocked the next tool, and overrode `bypassPermissions` mode** — the blocked action did not run. A control run at 10% usage allowed the exact same tool normally. So the deny is genuinely gating execution, not just printing a warning.
|
|
80
|
+
|
|
81
|
+
**2. The design was hardened against a real failure mode.** An early version *instructed the model* to write the handoff. The model refused it as a security reflex — verbatim:
|
|
82
|
+
|
|
83
|
+
> "I'm treating that message as spurious/injected rather than a genuine directive."
|
|
84
|
+
|
|
85
|
+
That's the correct instinct from a well-aligned model — and it's exactly why "ask the model nicely" is a broken design. So Brink was redesigned to **write `HANDOFF.md` itself**, deterministically, and let the hook do the stopping. After the redesign, the same model stopped gracefully — verbatim:
|
|
86
|
+
|
|
87
|
+
> "I've paused before completing the file creation — Brink stopped the action because you're at 99% of your 5-hour usage limit (resets at 06:46 PM)... progress is saved to HANDOFF.md. Once the limit resets, it'll pick up from there."
|
|
88
|
+
|
|
89
|
+
The mechanism stops the agent at the wall. The `HANDOFF.md` is what survives it.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## What Brink does
|
|
94
|
+
|
|
95
|
+
Four things, in increasing order of how much they save you:
|
|
96
|
+
|
|
97
|
+
| # | Capability | What you get |
|
|
98
|
+
|---|------------|--------------|
|
|
99
|
+
| 1 | **Passive usage** | Live 5h / 7d usage right in the CLI statusline (reset countdown shown for the 5h window). |
|
|
100
|
+
| 2 | **Notify** | Phone push (ntfy; Pushover planned) **plus** desktop toast (Windows today; macOS/Linux planned) at configurable thresholds. Both the 5h and 7d windows, plus a "budget's back" ping when the weekly window resets. |
|
|
101
|
+
| 3 | **Auto-pause + handoff** *(the core)* | At your threshold, Brink blocks the next tool and writes `HANDOFF.md` itself. Your work is checkpointed before the wall. |
|
|
102
|
+
| 4 | **Resume** *(optional, **experimental**, off by default)* | An external OS scheduler (Windows Task Scheduler) relaunches a fresh process that reads `HANDOFF.md` after the limit resets. ⚠️ This relaunches an autonomous agent **unattended** — read the caveats before enabling. |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## How it works
|
|
107
|
+
|
|
108
|
+
Brink is a **shared Node core with thin per-CLI adapters**, so the hard logic lives in one place and each CLI just maps its own config and field names.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
┌─────────────┐ writes ┌────────────┐ reads ┌──────────────┐
|
|
112
|
+
│ statusline │ ──────────▶ │ state file │ ─────────▶ │ PreToolUse │
|
|
113
|
+
│ sensor │ usage % │ (small) │ │ hook │
|
|
114
|
+
│ (per CLI) │ └────────────┘ └──────┬───────┘
|
|
115
|
+
└─────────────┘ │ at threshold
|
|
116
|
+
▼
|
|
117
|
+
┌──────────────────────────────┐
|
|
118
|
+
│ 1. block the next tool │
|
|
119
|
+
│ 2. emit a short deny message │
|
|
120
|
+
│ 3. write HANDOFF.md itself │
|
|
121
|
+
│ (from session transcript) │
|
|
122
|
+
└──────────────────────────────┘
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
1. **Sensor.** A statusline sensor reads your current usage and writes a small state file.
|
|
126
|
+
2. **Hook.** A `PreToolUse` hook reads that state file before each tool call. Below threshold, it does nothing. At or above threshold, it blocks the next tool and emits one short, credible, non-contradictory deny:
|
|
127
|
+
|
|
128
|
+
> *Paused by Brink at 99% of your 5h limit, resets 06:46 PM. Saved to HANDOFF.md. Stop and reply in plain text.*
|
|
129
|
+
|
|
130
|
+
3. **Handoff.** Brink writes `HANDOFF.md` itself, deterministically, from the session transcript. It does **not** depend on the model cooperating.
|
|
131
|
+
|
|
132
|
+
Design choices that matter:
|
|
133
|
+
|
|
134
|
+
- **Default threshold ≈ 93%** of the 5h window — enough headroom that the pause lands *before* the wall, not on it.
|
|
135
|
+
- **No auto-commit.** Brink will not write to your branch unattended. An agent committing to your repo while you're away is the genuinely scary failure mode, so it's off by design — Brink saves a file; what you do with it is your call.
|
|
136
|
+
- **No trusting the model to write the handoff.** That path was tested and rejected (see the proof above).
|
|
137
|
+
- **Kill-switch escape hatch.** A single file instantly disables the hook. A bad threshold can never brick your session.
|
|
138
|
+
- **BOM-robust reads.** State and transcript reads tolerate byte-order marks, so Windows/PowerShell-written config won't trip the parser.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## What a generated `HANDOFF.md` looks like
|
|
143
|
+
|
|
144
|
+
Built from the live transcript at pause time — not a template you fill in:
|
|
145
|
+
|
|
146
|
+
```markdown
|
|
147
|
+
# Handoff — feat/payment-retries
|
|
148
|
+
|
|
149
|
+
Paused at 99% of your 5h limit (resets 06:46 PM).
|
|
150
|
+
|
|
151
|
+
## The task
|
|
152
|
+
Add exponential-backoff retry to the Stripe webhook handler so transient
|
|
153
|
+
5xx responses don't drop payment events. Cap at 5 attempts, jittered.
|
|
154
|
+
|
|
155
|
+
## Recent actions before the pause
|
|
156
|
+
- Added `retryWithBackoff()` helper in src/lib/retry.ts
|
|
157
|
+
- Wired it into webhookHandler() for the `charge.failed` path
|
|
158
|
+
- Started updating tests in test/webhook.test.ts (2 of 5 cases done)
|
|
159
|
+
|
|
160
|
+
## Next steps
|
|
161
|
+
- Finish the remaining 3 test cases (timeout, max-attempts, success-on-retry)
|
|
162
|
+
- Confirm jitter is applied per-attempt, not once
|
|
163
|
+
- Run the suite and check the handler still returns 200 on give-up
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Supported CLIs
|
|
169
|
+
|
|
170
|
+
The promise is honest: **auto-pause where the CLI supports it, notify-before-limit everywhere else.**
|
|
171
|
+
|
|
172
|
+
| CLI | Auto-pause | Notify | Notes |
|
|
173
|
+
|-----|:----------:|:------:|-------|
|
|
174
|
+
| **Claude Code** | ✅ Full | ✅ | Built and **live-verified** (see proof above). |
|
|
175
|
+
| **Codex CLI** | ⚙️ Pending upstream | ✅ | Adapter built; field names verified against Codex source. Currently **inert** because `rate_limits` is `null` in Codex rollout files ([openai/codex#14880](https://github.com/openai/codex/issues/14880)) — that nulls out notify too, so today the adapter reads nothing. Both notify and auto-pause activate when the upstream data lands (auto-pause with a caveat: Codex's pre-tool gate reliably covers shell commands only). |
|
|
176
|
+
| **Gemini CLI** | 🔜 Planned | 🔜 Planned | Adapter seam ready. |
|
|
177
|
+
| **opencode** | 🔜 Planned | 🔜 Planned | Adapter seam ready. |
|
|
178
|
+
| **Crush** | ❌ | 🔜 Planned | No usage feed / no hooks to gate on — notify-only is the ceiling, and that adapter isn't written yet. |
|
|
179
|
+
| **aider** | ❌ | 🔜 Planned | No usage feed / no hooks to gate on — notify-only is the ceiling, and that adapter isn't written yet. |
|
|
180
|
+
|
|
181
|
+
> To be explicit: **Codex auto-pause does not work today.** It doesn't yet, because of a bug upstream of Brink. The adapter is real and waiting — when the data lands, it lights up with no change on your side.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Honest caveats
|
|
186
|
+
|
|
187
|
+
These are kept on purpose. Brink is a continuity tool, not magic.
|
|
188
|
+
|
|
189
|
+
- **Resume is EXPERIMENTAL.** It's off by default, Windows-only (**Task Scheduler** v1; launchd/cron planned), and has the least real-world runtime of any Brink feature. It relaunches a **fresh** process that reads `HANDOFF.md` — a clean restart from a written checkpoint, **not** an in-memory restore. Enabling it means an autonomous agent restarts **unattended, while you're away**, spending your quota in your project — and `skip_permissions` maps to `--dangerously-skip-permissions` (no permission prompts at all). Leave both off unless you fully accept that.
|
|
190
|
+
- **A 5h reset does not clear the weekly cap.** Brink tracks both windows; if you're up against the 7d wall, a 5h reset won't save you — but Brink will tell you which window you're hitting.
|
|
191
|
+
- **No unattended commits.** By design (see above). `HANDOFF.md` is written; your branch is left untouched.
|
|
192
|
+
- **`HANDOFF.md` contains your session.** It's built from the transcript, so your task text and recent tool actions land in it verbatim — including anything sensitive you typed. It's written to the project directory and **gitignoring it is on you** (Brink never commits it, but *you* might). Treat it like a scratch file, not documentation.
|
|
193
|
+
- **The usage sensor depends on an undocumented surface.** Claude Code pipes `rate_limits` to statuslines today; that's not a versioned API. If your setup doesn't receive usage data (some API-key/enterprise configurations), Brink can't arm — `brink doctor` tells you if that's you.
|
|
194
|
+
|
|
195
|
+
## Positioning
|
|
196
|
+
|
|
197
|
+
Anthropic explicitly declined the configurable-threshold-alert feature request ([claude-code#17431](https://github.com/anthropics/claude-code/issues/17431)). Brink is **the threshold-aware pause Anthropic said no to, as a drop-in** — plus the handoff and resume layer on top.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Install
|
|
202
|
+
|
|
203
|
+
### npm (recommended)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npm i -g claude-brink
|
|
207
|
+
brink init # wires hooks + usage sensor into ~/.claude/settings.json
|
|
208
|
+
brink doctor # proves the whole chain works on YOUR machine — run it
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
That's the entire install. `brink init` backs up your settings first (`settings.json.brink-bak`), is idempotent, and **tells you loudly** if it replaces an existing custom statusline (yours is restorable — `brink uninstall` puts it back). If your `settings.json` has a syntax error, `init` **aborts instead of touching it**.
|
|
212
|
+
|
|
213
|
+
Then run **`brink doctor`**. It exercises the real chain end-to-end — sensor → state file → pause hook (in a sandbox) → kill switch → an actual desktop notification — and prints a copy-pasteable report. This exists because silent environment-dependent failure is the #1 risk of a tool like this; if something on your machine doesn't work, doctor finds it *now*, not at 93%.
|
|
214
|
+
|
|
215
|
+
Everyday commands:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
brink off # kill switch — instantly disables everything
|
|
219
|
+
brink on # re-enable
|
|
220
|
+
brink uninstall # removes hooks, restores your original statusline
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`brink init` currently targets **Claude Code**. Multi-CLI detection (Codex, Gemini, opencode) is on the roadmap — see [Supported CLIs](#supported-clis).
|
|
224
|
+
|
|
225
|
+
### Plugin marketplace (alternative)
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
/plugin marketplace add LinusPisano/claude-brink
|
|
229
|
+
/plugin install brink@brink
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This wires the **hooks** only — Claude Code plugins can't register a `statusLine` (platform limit), and the statusline is Brink's usage sensor. Complete it with `npm i -g claude-brink && brink init` anyway, which makes the plugin path redundant. Honestly: **just use npm.**
|
|
233
|
+
|
|
234
|
+
> ⚠️ If you wire the statusline by hand, never point it into the plugin's install directory — plugin dirs are versioned per update, so the path silently dies on the next update while the hooks keep acting on frozen usage data. The npm global path is stable; use that.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Status
|
|
239
|
+
|
|
240
|
+
- **The core (pause + handoff) is live-verified** on real sessions and dogfooded daily by the author. It survived a 41-finding adversarial review and a 5-advisor launch council — both in `docs/`.
|
|
241
|
+
- **Resume and notifications are younger** — working and end-to-end tested, but with days (not months) of real-world runtime. That's why resume ships off-by-default and `brink doctor` exists.
|
|
242
|
+
- **Verified on Windows 11; macOS/Linux pause-path untested** — the honesty matrix at the top is the truth. If you run it on a Mac, `brink doctor` output in an issue is gold.
|
|
243
|
+
- **On GitHub, currently private** — the repo goes public alongside the launch of [pisanolinus.com](https://pisanolinus.com) and my [LinkedIn](https://www.linkedin.com/in/linus-pisano); the npm package is live now.
|
|
244
|
+
|
|
245
|
+
Roadmap from here: the remaining adapters (Gemini, opencode), the `npm` package + `brink init`, the demo GIF + public flip, and the Codex pause flipping live the moment upstream lands.
|
|
246
|
+
|
|
247
|
+
If you're reading this at launch: the pause is real, the handoff is real, and the caveats above are the whole truth.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
[MIT](LICENSE).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Author
|
|
258
|
+
|
|
259
|
+
Built by **Linus Pisano** — CAD + full-stack + agentic AI.
|
|
260
|
+
|
|
261
|
+
- 🌐 [pisanolinus.com](https://pisanolinus.com)
|
|
262
|
+
- 💼 [linkedin.com/in/linus-pisano](https://www.linkedin.com/in/linus-pisano)
|
|
263
|
+
- ✉️ [pisanolinus@gmail.com](mailto:pisanolinus@gmail.com)
|
|
264
|
+
|
|
265
|
+
*Follow the launch on X — link coming with the public release.*
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy to config.json in ~/.claude/brink/ and edit. Never commit your ntfy topic (it is the only auth for your push channel). The kill switch is a fixed path: create ~/.claude/brink/DISABLED to instantly disable Brink.",
|
|
3
|
+
"notify": {
|
|
4
|
+
"ntfy_topic": "REPLACE-with-your-secret-topic",
|
|
5
|
+
"windows_toast": true
|
|
6
|
+
},
|
|
7
|
+
"thresholds": {
|
|
8
|
+
"five_hour": { "warn": [75, 85], "pause": 93 },
|
|
9
|
+
"seven_day": { "warn": [80, 90], "pause": 95 }
|
|
10
|
+
},
|
|
11
|
+
"_reset_ping_comment": "Ping when a window rolls over (budget is back). 'enabled' off for 5h (resets several times a day = noise) / on for weekly (days of budget back). 'floor' = only ping if you were at/above this % before it reset, so a reset from low usage stays silent.",
|
|
12
|
+
"reset_ping": {
|
|
13
|
+
"five_hour": { "enabled": false, "floor": 75 },
|
|
14
|
+
"seven_day": { "enabled": true, "floor": 80 }
|
|
15
|
+
},
|
|
16
|
+
"_resume_comment": "Opt-in. skip_permissions=true relaunches with --dangerously-skip-permissions: the resumed agent runs WITHOUT permission prompts, unattended, in your project. Leave false unless you fully accept that.",
|
|
17
|
+
"resume": {
|
|
18
|
+
"enabled": false,
|
|
19
|
+
"buffer_seconds": 90,
|
|
20
|
+
"skip_permissions": false
|
|
21
|
+
}
|
|
22
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comment": "Brink plugin hooks (auto-discovered when installed via the Claude Code plugin marketplace). PreToolUse = graceful auto-pause before the limit; PostToolUse = threshold warning. ${CLAUDE_PLUGIN_ROOT} resolves to the installed plugin dir. NOTE: a plugin CANNOT register the statusLine (the usage sensor) — only 'agent'/'subagentStatusLine' are honored in a plugin's settings.json. The statusLine must be added by the user (one line) or by `node src/install.js --statusline`. Without it, state.json is never written and these hooks have no data to act on.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/src/brink.js\" claude pause" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"PostToolUse": [
|
|
12
|
+
{
|
|
13
|
+
"hooks": [
|
|
14
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/src/brink.js\" claude warn" }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-brink",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code stops at the brink — graceful auto-pause + handoff before you hit a usage limit, with optional auto-resume after reset.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Linus Pisano <pisanolinus@gmail.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/LinusPisano/claude-brink.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/LinusPisano/claude-brink#readme",
|
|
12
|
+
"keywords": ["claude-code", "usage-limit", "rate-limit", "auto-pause", "handoff", "notifications", "ntfy"],
|
|
13
|
+
"bin": { "brink": "src/cli.js" },
|
|
14
|
+
"engines": { "node": ">=18" },
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node tests/run.js && node tests/core.test.js && node tests/handoff.test.js && node tests/install.test.js && node tests/reset.test.js && node tests/resume.test.js && node tests/cli.test.js",
|
|
17
|
+
"install-hooks": "node src/install.js",
|
|
18
|
+
"install-all": "node src/install.js --statusline"
|
|
19
|
+
},
|
|
20
|
+
"files": ["src", "hooks", "config.example.json", "README.md", "LICENSE"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Brink adapter — Claude Code. Implements the two-method adapter contract:
|
|
2
|
+
// readUsage() -> normalized usage | denyOutput(reason) -> how this CLI blocks
|
|
3
|
+
// Usage source: the state.json the Brink statusline writes (the cleanest source of all).
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const dir = () => process.env.BRINK_DIR || path.join(os.homedir(), '.claude', 'brink');
|
|
9
|
+
|
|
10
|
+
function readUsage() {
|
|
11
|
+
const sp = path.join(dir(), 'state.json');
|
|
12
|
+
if (!fs.existsSync(sp)) return null;
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(sp, 'utf8');
|
|
15
|
+
const s = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
|
|
16
|
+
const num = (v) => (typeof v === 'number' ? v : null);
|
|
17
|
+
return {
|
|
18
|
+
provider: 'claude',
|
|
19
|
+
five_pct: num(s.five_pct),
|
|
20
|
+
week_pct: num(s.week_pct),
|
|
21
|
+
five_reset: num(s.five_reset),
|
|
22
|
+
week_reset: num(s.week_reset),
|
|
23
|
+
session_id: s.session_id || '',
|
|
24
|
+
cwd: s.cwd || '',
|
|
25
|
+
};
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Claude Code PreToolUse deny: JSON on stdout, exit 0.
|
|
30
|
+
function denyOutput(reason) {
|
|
31
|
+
return {
|
|
32
|
+
stdout: JSON.stringify({
|
|
33
|
+
hookSpecificOutput: {
|
|
34
|
+
hookEventName: 'PreToolUse',
|
|
35
|
+
permissionDecision: 'deny',
|
|
36
|
+
permissionDecisionReason: reason,
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
exitCode: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { name: 'claude', readUsage, denyOutput };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Brink adapter — OpenAI Codex CLI.
|
|
2
|
+
// readUsage(): parse the newest ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl token_count
|
|
3
|
+
// event, which carries the primary (~5h) + secondary (~weekly) rate-limit windows.
|
|
4
|
+
// denyOutput(): Codex PreToolUse blocks via exit code 2 + reason on stderr.
|
|
5
|
+
//
|
|
6
|
+
// Field names VERIFIED against Codex Rust source (protocol.rs, RateLimitWindow) + 53 real
|
|
7
|
+
// local rollout files: guard .type=="event_msg" && .payload.type=="token_count"; data at
|
|
8
|
+
// .payload.rate_limits.{primary,secondary}.{used_percent, window_minutes, resets_at} (epoch s).
|
|
9
|
+
// ⚠️ UPSTREAM BUG (openai/codex#14880): rate_limits is almost always `null` in rollout files,
|
|
10
|
+
// so this returns null TODAY and pause won't fire until OpenAI populates it (or we add an alt
|
|
11
|
+
// source: auth.json -> ChatGPT usage endpoint). We scan back for a populated line and never
|
|
12
|
+
// act on null (safe no-op). ⚠️ Codex PreToolUse also reliably gates Bash only (edits/MCP leaky).
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const sessionsDir = () => process.env.CODEX_SESSIONS || path.join(os.homedir(), '.codex', 'sessions');
|
|
18
|
+
|
|
19
|
+
function newestRollout(root) {
|
|
20
|
+
if (!fs.existsSync(root)) return null;
|
|
21
|
+
let best = null;
|
|
22
|
+
const walk = (d) => {
|
|
23
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
24
|
+
const p = path.join(d, e.name);
|
|
25
|
+
if (e.isDirectory()) walk(p);
|
|
26
|
+
else if (/^rollout-.*\.jsonl$/.test(e.name)) {
|
|
27
|
+
const m = fs.statSync(p).mtimeMs;
|
|
28
|
+
if (!best || m > best.m) best = { p, m };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
try { walk(root); } catch { return null; }
|
|
33
|
+
return best && best.p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pick = (obj, keys) => {
|
|
37
|
+
for (const k of keys) if (obj && obj[k] != null) return obj[k];
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function parseWindow(w, now) {
|
|
42
|
+
if (!w) return { pct: null, reset: null };
|
|
43
|
+
const pct = pick(w, ['used_percent', 'used_percentage', 'percent', 'percent_used']);
|
|
44
|
+
const at = pick(w, ['resets_at', 'reset_at']);
|
|
45
|
+
const inSecs = pick(w, ['resets_in_seconds', 'reset_in_seconds', 'resets_in']);
|
|
46
|
+
let reset = null;
|
|
47
|
+
if (typeof at === 'number') reset = at > 1e12 ? Math.floor(at / 1000) : at;
|
|
48
|
+
else if (typeof inSecs === 'number') reset = now + inSecs;
|
|
49
|
+
return { pct: typeof pct === 'number' ? pct : null, reset };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readUsage() {
|
|
53
|
+
const f = newestRollout(sessionsDir());
|
|
54
|
+
if (!f) return null;
|
|
55
|
+
let lines;
|
|
56
|
+
try { let raw = fs.readFileSync(f, 'utf8'); if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); lines = raw.split(/\r?\n/).filter(Boolean); } catch { return null; }
|
|
57
|
+
let rl = null;
|
|
58
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
59
|
+
let ev; try { ev = JSON.parse(lines[i]); } catch { continue; }
|
|
60
|
+
rl = ev.rate_limits || (ev.payload && ev.payload.rate_limits) || (ev.info && ev.info.rate_limits) || null;
|
|
61
|
+
if (rl) break;
|
|
62
|
+
}
|
|
63
|
+
if (!rl) return null;
|
|
64
|
+
const now = Math.floor(Date.now() / 1000);
|
|
65
|
+
const primary = parseWindow(rl.primary || rl.five_hour, now);
|
|
66
|
+
const secondary = parseWindow(rl.secondary || rl.seven_day || rl.weekly, now);
|
|
67
|
+
return {
|
|
68
|
+
provider: 'codex',
|
|
69
|
+
five_pct: primary.pct, five_reset: primary.reset,
|
|
70
|
+
week_pct: secondary.pct, week_reset: secondary.reset,
|
|
71
|
+
session_id: '', cwd: process.cwd(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Codex PreToolUse deny: exit code 2 + reason on stderr (the reliable path).
|
|
76
|
+
function denyOutput(reason) {
|
|
77
|
+
return { stderr: reason, exitCode: 2 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { name: 'codex', readUsage, denyOutput };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Brink - arm resume (Phase 7, opt-in)
|
|
2
|
+
# Registers a one-shot Windows Task Scheduler job for the reset time that relaunches
|
|
3
|
+
# Claude from HANDOFF.md. Driven by the authoritative resets_at (epoch seconds).
|
|
4
|
+
# Buffer/Skip are passed through from config.json by brink.js (core/resume.js).
|
|
5
|
+
# Invoked SYNCHRONOUSLY by brink.js (detached PowerShell dies without a console -
|
|
6
|
+
# live-fire finding 2026-07-04).
|
|
7
|
+
param(
|
|
8
|
+
[Parameter(Mandatory)][string]$ResetsAt,
|
|
9
|
+
[string]$Sid,
|
|
10
|
+
[string]$Proj,
|
|
11
|
+
[int]$Buffer = 90,
|
|
12
|
+
[string]$Skip = '0'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
$fireAt = [DateTimeOffset]::FromUnixTimeSeconds([int64]$ResetsAt).ToLocalTime().AddSeconds($Buffer).DateTime
|
|
16
|
+
# Sanitize the task name: session ids are normally uuid-safe, but Task Scheduler
|
|
17
|
+
# rejects several characters, and the name must round-trip to resume-once.ps1.
|
|
18
|
+
$name = 'BrinkResume_' + ($Sid -replace '[^\w\-]', '_')
|
|
19
|
+
Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue
|
|
20
|
+
|
|
21
|
+
$resume = Join-Path $PSScriptRoot 'resume-once.ps1'
|
|
22
|
+
# -ExecutionPolicy Bypass: stock Windows is Restricted and would refuse the script.
|
|
23
|
+
$arg = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$resume`" -Sid `"$Sid`" -Proj `"$Proj`" -Buffer $Buffer -Skip `"$Skip`""
|
|
24
|
+
Register-ScheduledTask -TaskName $name -RunLevel Limited `
|
|
25
|
+
-Action (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arg) `
|
|
26
|
+
-Trigger (New-ScheduledTaskTrigger -Once -At $fireAt) `
|
|
27
|
+
-Settings(New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries) -ErrorAction Stop | Out-Null
|