spare10 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +49 -45
  2. package/dist/spare10.js +24 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,38 +1,29 @@
1
1
  # spare10
2
2
 
3
- **A circuit breaker for Claude Code.** It keeps a slice of your 5-hour quota in reserve and
4
- stops the agent before that reserve is spent — so you still have budget left to steer it,
5
- instead of the session dying mid-edit.
3
+ **A circuit breaker for Claude Code.** Use spare10 when you run low-priority agents on Claude Code and you want to keep a reserve of quota for important stuff. Spare10 watches your session limits and stops the agent when you reach 90%. Then it's up to you to decide whether to continue or not.
4
+
5
+ To launch a session with spare10, just prefix the `claude` command with `spare10`:
6
6
 
7
7
  ```
8
8
  spare10 claude
9
9
  ```
10
10
 
11
- That's the whole integration. spare10 configures the session and gets out of the way.
12
-
13
- ---
14
-
15
- ## The problem
16
-
17
- A long autonomous session burns quota invisibly. When it hits 100%, Claude Code stops
18
- immediately: a half-finished refactor, uncommitted, and no agent available to finish it or
19
- back it out. Your context isn't lost — `claude --continue` restores it — but the hours until
20
- the window resets are, and so is any chance to say "wait, commit that first."
21
-
22
- spare10 spends the last slice of your quota on **you**.
23
-
24
- ## What it does
11
+ ## How it works
25
12
 
26
- When usage starts eating the reserve (the last 10% by default) it hands control back:
13
+ When usage starts eating the reserve (the last 10% by default) it stops Claude Code at the
14
+ agent's next tool call — the whole process, subagents and background tasks included — and asks
15
+ you in the terminal whether to carry on:
27
16
 
28
17
  ```
29
- spare10 — 9% of the 5-hour window left, which is your 10% reserve.
30
- Resets at 14:00. Continue anyway?
31
- Yes No (Esc)
18
+ spare10 — into your 10% reserve · 9% of quota left · resets 14:00
19
+ spare10 stopped the agent between operations — nothing is left half-written.
20
+ Resume anyway? [y/N]
32
21
  ```
33
22
 
34
- Approve and it stays quiet for the rest of the window. Press Esc and the turn ends there,
35
- with your working tree in a state you chose.
23
+ Answer `y` and the session is resumed right where it stopped, with its whole conversation,
24
+ and spare10 stays quiet for the rest of the window. Answer anything else and you are back at
25
+ your shell with the session saved; `claude --resume <id>` picks it up later, when the window
26
+ has reset.
36
27
 
37
28
  For unattended runs, hand it an instruction instead of a question:
38
29
 
@@ -41,8 +32,10 @@ spare10 --pause-prompt "Finish this block, commit, then stop." claude
41
32
  ```
42
33
 
43
34
  That text is injected into the running agent **without blocking it**, so the agent can
44
- actually carry out the wind-down you asked for. It arrives with the situation attached, since
45
- an instruction turning up mid-turn otherwise has no context:
35
+ actually carry out the wind-down you asked for. Every agent gets it — the main thread and each
36
+ subagent, on its own next tool call because hook context only reaches the agent whose call
37
+ it rode in on, and a subagent left untold would keep working. It arrives with the situation
38
+ attached, since an instruction turning up mid-turn otherwise has no context:
46
39
 
47
40
  ```
48
41
  spare10 budget guard. You have reached the safe usage limit for this session
@@ -73,7 +66,7 @@ than installing something that cannot run.
73
66
  spare10 [options] <command> [args...]
74
67
 
75
68
  --reserve <1-99> Keep this much of the 5-hour window back for yourself (default: 10)
76
- --pause-prompt <text> Inject this instruction instead of asking
69
+ --pause-prompt <text> Inject this instruction instead of stopping
77
70
  --refresh <seconds> Quota poll interval (default: 2)
78
71
  --no-badge Never draw the spare10 marker in the status line
79
72
  doctor Report what spare10 detected and what it would do
@@ -89,16 +82,19 @@ quota in integer percentages, so `--reserve 10.5` is rejected rather than silent
89
82
  The status line shows a gray `⧗ spare10` until the first quota reading arrives, a green
90
83
  `● spare10` while the reserve is untouched, then an orange
91
84
  `⚠ Pausing at next tool call` once it is reached, its icon pulsing once per refresh. Once you
92
- have consented it drops back to a quiet orange `⨯ spare10`. A non-default reserve is spelled
93
- out either way, as `● spare10 (20%)` the name already accounts for 10.
85
+ have consented it drops back to a quiet orange `⨯ spare10`; once a `--pause-prompt` has gone
86
+ out it shows `⏸ spare10`. A non-default reserve is spelled out either way, as
87
+ `● spare10 (20%)` — the name already accounts for 10.
94
88
 
95
89
  The pulse is driven by spare10's own render cadence rather than the ANSI blink attribute,
96
90
  which most terminals ignore.
97
91
 
98
92
  Consenting at the pre-flight prompt counts for the whole window: the session starts disarmed
99
- rather than asking the same question again on the first tool call.
93
+ rather than stopping again on the first tool call.
100
94
 
101
- ## How it works
95
+ State lives under `~/.spare10`; set `SPARE10_HOME` to put it somewhere else.
96
+
97
+ ## Under the hood
102
98
 
103
99
  Claude Code exposes quota in exactly one place: the `rate_limits` object handed to your
104
100
  **status line** command. It is not available to hooks. Hooks, meanwhile, are the only thing
@@ -106,24 +102,31 @@ that can *stop* anything. So spare10 splits in two and joins them through a stat
106
102
 
107
103
  ```
108
104
  spare10 claude
109
- └─ exec claude --settings '{ statusLine: …, hooks: { PreToolUse: …, PostToolUse: … } }'
105
+ └─ spawn claude --settings '{ statusLine: …, hooks: { PreToolUse: … } }'
110
106
 
111
- sensor (status line, every 2s) reads rate_limits → writes state
112
- gate (PreToolUse, every call) reads state → passes, asks, injects, or denies
113
- post (PostToolUse) the tool ran, so the user approveddisarm
107
+ sensor (status line, every 2s) reads rate_limits → writes state
108
+ gate (PreToolUse, every call) reads state → passes, injects, or stops Claude Code
109
+ launcher (after Claude Code exits) sees the stop asks claude --resume <session>
114
110
  ```
115
111
 
116
112
  **It stops between operations, not mid-write.** The gate fires on tool calls, and that is the
117
113
  point rather than a limitation: it interrupts in the gap between one call and the next, where
118
- nothing is half-written and no command is in flight. A keystroke-level interrupt would land
119
- wherever the agent happened to be which is the mess spare10 exists to avoid.
114
+ nothing is half-written and no command is in flight. Claude Code runs write-capable tools one
115
+ at a time, so when the gate fires nothing else that writes can be running either. A
116
+ keystroke-level interrupt would land wherever the agent happened to be — which is the mess
117
+ spare10 exists to avoid.
118
+
119
+ **It stops the process, not the turn.** A hook on its own can only deny a tool call or ask
120
+ through Claude Code's permission dialog — which non-interactive modes auto-approve, and which
121
+ the model can keep retrying around. So the gate sends Claude Code `SIGTERM` instead. Claude
122
+ Code exits cleanly, the transcript is already on disk, and spare10 asks its own question on
123
+ the terminal it now owns; `y` starts `claude --resume` on the same session. The question is the
124
+ same one in every permission mode, and subagents go down with the process rather than each
125
+ raising a dialog of their own.
120
126
 
121
127
  The cost is that a turn producing only text is not gated. Before launching, spare10 already
122
128
  knows your quota from the previous run, so `spare10 claude` asks for confirmation rather than
123
- starting a session that would stop on its first move. Once a session is running, the status
124
- line badge is the signal — a hook's only user-visible channel is exit code 2, which on
125
- `UserPromptSubmit` erases what you typed, and plain output there becomes model context that
126
- Claude paraphrases rather than a message you can rely on.
129
+ starting a session that would stop on its first move.
127
130
 
128
131
  Three consequences worth knowing:
129
132
 
@@ -165,10 +168,11 @@ Not in this version, deliberately:
165
168
 
166
169
  ## Known limitations
167
170
 
168
- - In `bypassPermissions` and `dontAsk` modes an `ask` dialog would be auto-approved, so
169
- spare10 falls back to `deny`. The agent may then retry with a different tool and be denied
170
- again, burning tokens in a small loop. The deny message tells it to stop; there is no retry
171
- counter yet.
171
+ - Stopping the process drops what only lived in it: permissions granted for the session,
172
+ anything queued or half-typed in the prompt. The resumed session asks again.
173
+ - A session that cannot be resumed started with `--no-session-persistence`, or inside
174
+ another Claude Code session, where transcripts are not saved — is not stopped. The gate
175
+ denies the tool call instead, and the model may retry with another tool before it gives up.
172
176
  - Status line chaining reads user-level settings only. A status line configured in project or
173
177
  local settings is not detected.
174
178
 
@@ -176,7 +180,7 @@ Not in this version, deliberately:
176
180
 
177
181
  ```bash
178
182
  npm install
179
- npm test # 128 tests: unit, plus the hooks and CLI as real subprocesses
183
+ npm test # 151 tests: unit, plus the hooks and CLI as real subprocesses
180
184
  npm run typecheck
181
185
  npm run build # single dependency-free bundle in dist/
182
186
  ```
package/dist/spare10.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- var C={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjected:!1,awaitingApproval:!1,tick:0},N=10,ye=2,c={reserve:N,pausePrompt:null,refresh:ye,badge:!0,chain:null},K=3,W=3,Y=3600;function w(e){return 100-e.reserve}function P(e){return 100-e}var u=class extends Error{},T=`spare10 \u2014 pause Claude Code before the 5-hour quota runs out
2
+ var T={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjectedTo:[],halted:null,tick:0},_e="main";function Y(e){return e??_e}var N=10,ve=2,p={reserve:N,pausePrompt:null,refresh:ve,badge:!0,chain:null},Q=3,X=3,z=3600;function w(e){return 100-e.reserve}function E(e){return 100-e}var c=class extends Error{},U=`spare10 \u2014 pause Claude Code before the 5-hour quota runs out
3
3
 
4
4
  spare10 [options] <command> [args...]
5
5
 
6
6
  Options:
7
7
  --reserve <1-99> Keep this much of the 5-hour window back for yourself (default: 10)
8
- --pause-prompt <text> Instead of asking, inject this instruction into the running agent
8
+ --pause-prompt <text> Instead of stopping, inject this instruction into the running agent
9
9
  --refresh <seconds> Status line poll interval, also the staleness unit (default: 5)
10
10
  --no-badge Never draw the spare10 marker in the status line
11
11
  -h, --help Show this message
@@ -16,19 +16,25 @@ Commands:
16
16
  Examples:
17
17
  spare10 claude
18
18
  spare10 --reserve 15 claude
19
- spare10 --pause-prompt "Finish this block, commit, then stop." claude --resume`;function ke(e){if(!/^\d+$/.test(e))throw new u(`--reserve must be a whole number: the API reports quota in integer percentages, so "${e}" cannot be honoured.`);let t=Number(e);if(t<1||t>99)throw new u(`--reserve must be between 1 and 99, got ${t}.`);return t}function Ae(e){if(!/^\d+$/.test(e)||Number(e)<1)throw new u(`--refresh must be a whole number of seconds >= 1, got "${e}".`);return Number(e)}function Q(e){let t={reserve:c.reserve,pausePrompt:c.pausePrompt,refresh:c.refresh,badge:c.badge},n=0,r=o=>{let i=e[n+1];if(i===void 0)throw new u(`${o} requires a value.`);return n+=1,i};for(;n<e.length;n+=1){let o=e[n];if(!o.startsWith("-"))break;switch(o){case"--reserve":t.reserve=ke(r(o));break;case"--threshold":throw new u('--threshold was replaced by --reserve, which is the quota kept back rather than the level that trips. "--threshold 90" is now "--reserve 10".');case"--pause-prompt":t.pausePrompt=r(o);break;case"--refresh":t.refresh=Ae(r(o));break;case"--no-badge":t.badge=!1;break;case"-h":case"--help":throw new u("");default:throw new u(`Unknown option "${o}".`)}}let s=e.slice(n);if(s.length===0)throw new u("No command given. Try: spare10 claude");return{config:t,command:s}}import{readFileSync as Ne}from"node:fs";var $e=new Set(["bypassPermissions","dontAsk"]),k={kind:"pass"};function U(e,t,n){return e.resetsAt!==null?n<e.resetsAt:e.updatedAt===null?!1:n-e.updatedAt<=t.refresh*W}function xe(e){return e===null?"an unknown time":new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function z(e,t){return`into your ${t.reserve}% reserve \xB7 ${P(e.pct??0)}% of quota left \xB7 resets ${xe(e.resetsAt)}`}function Z(e,t){return`spare10 \u2014 ${z(e,t)}`}function Pe(e,t){return`spare10 budget guard. You have reached the safe usage limit for this session (${z(e,t)}). Wrap up your work and stop.
20
-
21
- User instructions: ${t.pausePrompt??""}`}var X=Z;function ee(e,t,n){return R({state:e,config:t,now:n,permissionMode:null}).kind!=="pass"}function te(e,t){return`${Z(e,t)}
22
- The agent will pause safely at its first tool call \u2014 between operations, with nothing left half-written.`}function R({state:e,config:t,now:n,permissionMode:r}){return e.pct===null||e.updatedAt===null||e.blind||!U(e,t,n)||e.disarmedUntil!==null&&n<e.disarmedUntil||e.pct<w(t)?k:t.pausePrompt!==null&&!e.pausePromptInjected?{kind:"inject",text:Pe(e,t)}:r!==null&&$e.has(r)?{kind:"deny",reason:`${X(e,t)}. Stop now and wait for the user. Do not call any further tools.`}:{kind:"ask",reason:X(e,t)}}function ne(e){try{let t=JSON.parse(e);if(typeof t!="object"||t===null)return{permissionMode:null,toolName:null};let n=t;return{permissionMode:typeof n.permission_mode=="string"?n.permission_mode:null,toolName:typeof n.tool_name=="string"?n.tool_name:null}}catch{return{permissionMode:null,toolName:null}}}import{readFileSync as Re,writeFileSync as ve,renameSync as Ee,mkdirSync as _e,unlinkSync as Ce}from"node:fs";import{join as re}from"node:path";var se=e=>re(e,"state.json"),I=e=>re(e,"config.json");function d(){return Math.floor(Date.now()/1e3)}function oe(e){try{let t=JSON.parse(Re(e,"utf8"));return typeof t=="object"&&t!==null?t:null}catch{return null}}var h=(e,t)=>typeof e=="number"&&Number.isFinite(e)?e:t,v=(e,t)=>typeof e=="boolean"?e:t;function f(e){let t=oe(se(e));return t?{pct:h(t.pct,null),resetsAt:h(t.resetsAt,null),updatedAt:h(t.updatedAt,null),missingStreak:h(t.missingStreak,0)??0,blind:v(t.blind,!1),disarmedUntil:h(t.disarmedUntil,null),pausePromptInjected:v(t.pausePromptInjected,!1),awaitingApproval:v(t.awaitingApproval,!1),tick:h(t.tick,0)??0}:{...C}}function m(e,t){let n=se(e),r=`${n}.${process.pid}.tmp`;try{_e(e,{recursive:!0}),ve(r,JSON.stringify(t),"utf8"),Ee(r,n)}catch{try{Ce(r)}catch{}}}function y(e){let t=oe(I(e));if(!t)return{...c};let n=h(t.reserve,c.reserve)??c.reserve,r=h(t.refresh,c.refresh)??c.refresh;return{reserve:Math.min(99,Math.max(1,Math.round(n))),pausePrompt:typeof t.pausePrompt=="string"&&t.pausePrompt?t.pausePrompt:null,refresh:Math.max(1,Math.round(r)),badge:v(t.badge,c.badge),chain:typeof t.chain=="string"&&t.chain?t.chain:null}}function A(e,t){return e.resetsAt??t+Y}function ie(e,t){let r=e.filter(s=>s.pct!==null&&s.updatedAt!==null&&s.resetsAt!==null&&s.resetsAt>t).reduce((s,o)=>s===null||o.updatedAt>s.updatedAt?o:s,null);return r===null?null:{...C,pct:r.pct,resetsAt:r.resetsAt,updatedAt:r.updatedAt}}function F(){try{return Ne(0,"utf8")}catch{return""}}function D(e){process.stdout.write(JSON.stringify({hookSpecificOutput:e}))}function ae(e){let t=y(e),n=f(e),r=d();if(R({state:n,config:t,now:r,permissionMode:null}).kind==="pass")return F(),0;let{permissionMode:s}=ne(F()),o=R({state:n,config:t,now:r,permissionMode:s});switch(o.kind){case"pass":return 0;case"inject":return m(e,{...n,pausePromptInjected:!0,disarmedUntil:A(n,r)}),D({hookEventName:"PreToolUse",additionalContext:o.text}),0;case"ask":return m(e,{...n,awaitingApproval:!0}),D({hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:o.reason}),0;case"deny":return D({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o.reason}),0}}function ue(e){F();let t=f(e);return t.awaitingApproval&&m(e,{...t,awaitingApproval:!1,disarmedUntil:A(t,d())}),0}import{execFileSync as me}from"node:child_process";import{accessSync as Qe,constants as Xe,readdirSync as ze,statSync as q}from"node:fs";import{join as Ze}from"node:path";function le(e,t,n){return e.pct===null||e.updatedAt===null?{status:"no-data",detail:"no quota reading yet \u2014 start a session and wait a moment"}:e.blind?{status:"blind",detail:"Claude Code is not reporting rate_limits on this plan"}:U(e,t,n)?e.disarmedUntil!==null&&n<e.disarmedUntil?{status:"disarmed",detail:`consent given; quiet until ${M(e.disarmedUntil)}`}:e.pct>=w(t)?{status:"tripped",detail:`into the ${t.reserve}% reserve`}:{status:"armed",detail:`${P(e.pct)}% left, of which ${t.reserve}% is reserved`}:{status:"stale",detail:"the window this reading described has already reset"}}function j(e){let t=Math.abs(Math.round(e));if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function M(e){return new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawnSync as Ue}from"node:child_process";import{closeSync as Ie,mkdirSync as De,openSync as Fe,readSync as je,readdirSync as de,rmSync as Me,statSync as Oe,writeFileSync as Le}from"node:fs";import{homedir as pe}from"node:os";import{basename as He,join as $}from"node:path";import{fileURLToPath as Ge}from"node:url";import{readFileSync as Te}from"node:fs";function O(e){return`'${e.replace(/'/g,"'\\''")}'`}function E(e){let t;try{t=JSON.parse(Te(e,"utf8"))}catch{return{command:null,padding:void 0}}if(typeof t!="object"||t===null)return{command:null,padding:void 0};let n=t.statusLine;if(typeof n!="object"||n===null)return{command:null,padding:void 0};let r=n,s=typeof r.command=="string"?r.command:null,o=typeof r.padding=="number"?r.padding:void 0;return s!==null&&s.includes("spare10")?{command:null,padding:o}:{command:s,padding:o}}function ce(e){let t=s=>`${O(e.nodePath)} ${O(e.selfPath)} ${s} --run ${O(e.runDir)}`,n=s=>[{matcher:"",hooks:[{type:"command",command:t(s)}]}],r={type:"command",command:t("sensor"),refreshInterval:e.refresh};return e.padding!==void 0&&(r.padding=e.padding),{statusLine:r,hooks:{PreToolUse:n("gate"),PostToolUse:n("post")}}}var qe=7*24*60*60*1e3,L=()=>$(pe(),".spare10","runs"),H=()=>$(pe(),".claude","settings.json");function Be(e,t){let n;try{n=de(e)}catch{return}for(let r of n){let s=$(e,r);try{t-Oe(s).mtimeMs>qe&&Me(s,{recursive:!0,force:!0})}catch{}}}function Je(e,t){let n;try{n=de(e)}catch{return}let r=n.map(o=>$(e,o)).filter(o=>o!==t).map(f),s=ie(r,d());s!==null&&m(t,s)}function Ve(e){let t=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=$(e,t);return De(n,{recursive:!0}),n}function Ke(e){let t;try{t=Fe("/dev/tty","r")}catch{return null}process.stderr.write(e);let n=Buffer.alloc(1),r="";try{for(;je(t,n,0,1,null)>0;){let s=n.toString("utf8");if(s===`
23
- `||s==="\r")break;r+=s}}catch{return null}finally{Ie(t)}return r.trim()}function We(e,t,n){return e?t||n===null?"proceeding":/^y(es)?$/i.test(n.trim())?"consented":"declined":"clear"}function Ye(e,t){let n=f(e);if(!ee(n,t,d()))return"clear";process.stderr.write(`
24
- ${te(n,t)}
25
- `);let r=t.pausePrompt!==null?null:Ke("Start anyway? [y/N] ");return We(!0,t.pausePrompt!==null,r)}function G(){return Ge(import.meta.url)}function fe({config:e,command:t}){let[n,...r]=t;if(He(n)!=="claude")throw new u(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let s=L();Be(s,Date.now());let o=Ve(s);Je(s,o);let i=Ye(o,{...e,chain:null});if(i==="declined")return process.stderr.write(`Not started.
26
- `),0;if(i==="consented"){let x=f(o);m(o,{...x,disarmedUntil:A(x,d())}),process.stderr.write(`Continuing into the reserve for this window.
27
-
28
- `)}let l=E(H());Le(I(o),JSON.stringify({...e,chain:l.command},null,2));let p=ce({nodePath:process.execPath,selfPath:G(),runDir:o,refresh:e.refresh,padding:l.padding}),b=()=>{};process.on("SIGINT",b),process.on("SIGTERM",b);let g=Ue(n,["--settings",JSON.stringify(p),...r],{stdio:"inherit"});return g.error?(process.stderr.write(`spare10: could not start ${n}: ${g.error.message}
29
- `),127):g.signal?128+(g.signal==="SIGINT"?2:15):g.status??0}var et="0.2.0",a={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function tt(e){try{return ze(e).map(n=>Ze(e,n)).filter(n=>{try{return q(n).isDirectory()}catch{return!1}}).sort((n,r)=>q(r).mtimeMs-q(n).mtimeMs)[0]??null}catch{return null}}function nt(e){try{return me("command",["-v",e],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function rt(){try{return me("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function st(e){try{return Qe(e,Xe.X_OK),!0}catch{return!1}}function ge(){let e=(S="")=>process.stdout.write(`${S}
30
- `),t=d(),n=0;e(`spare10 ${et}`),e(),e("Environment"),e(` ${a.info} node ${process.execPath} (${process.version})`),e(` ${a.info} spare10 ${G()}`);let r=nt("claude"),s=r?rt():null;r&&s?e(` ${a.ok} claude ${r} (${s})`):(n+=1,e(` ${a.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),e(),e("Status line");let o=E(H());if(o.command===null)e(` ${a.info} nothing to chain \u2014 spare10 will own the status line`);else{let S=o.command.split(/\s+/)[0]??"",_=st(S);_||(n+=1),e(` ${_?a.ok:a.warn} chains into ${o.command}`),_||e(" that path is not executable; its output will be dropped")}e();let i=tt(L());if(i===null)return e("No runs yet. Start one with: spare10 claude"),n>0?1:0;let l=y(i),p=f(i),b=le(p,l,t);if(e(`Latest run ${i}`),e(` ${a.info} reserve ${l.reserve}% of the 5-hour window`),e(` ${a.info} on trip ${l.pausePrompt===null?"ask for confirmation":`inject: ${JSON.stringify(l.pausePrompt)}`}`),e(` ${a.info} refresh ${l.refresh}s`),p.pct!==null&&p.updatedAt!==null){e(` ${a.ok} quota ${p.pct}% used, ${100-p.pct}% left`);let S=t-p.updatedAt;e(` ${a.info} last reading ${j(S)} ago`+(S>l.refresh*3?" (no session running; still valid for this window)":""))}p.resetsAt!==null&&e(` ${a.info} resets ${M(p.resetsAt)} (in ${j(p.resetsAt-t)})`);let g=b.status==="blind";g&&(n+=1);let x=g?a.warn:b.status==="no-data"?a.info:a.ok;return e(` ${x} state ${b.status.toUpperCase()} \u2014 ${b.detail}`),n>0?1:0}import{spawnSync as ot}from"node:child_process";import{readFileSync as it}from"node:fs";function he(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function be(e){let t;try{t=JSON.parse(e)}catch{return{sessionId:null,fiveHour:null}}if(typeof t!="object"||t===null)return{sessionId:null,fiveHour:null};let n=t,r=typeof n.session_id=="string"?n.session_id:null,s=n.rate_limits;if(typeof s!="object"||s===null)return{sessionId:r,fiveHour:null};let o=s.five_hour;if(typeof o!="object"||o===null)return{sessionId:r,fiveHour:null};let i=o,l=he(i.used_percentage);return l===null?{sessionId:r,fiveHour:null}:{sessionId:r,fiveHour:{usedPercentage:Math.min(100,Math.max(0,l)),resetsAt:he(i.resets_at)}}}function at(e,t,n){let r=t.fiveHour,s=e.tick+1;if(!r){let i=e.missingStreak+1;return{...e,tick:s,missingStreak:i,blind:i>=K}}let o=r.resetsAt!==null&&e.resetsAt!==null&&r.resetsAt!==e.resetsAt;return{tick:s,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:o?null:e.disarmedUntil,pausePromptInjected:o?!1:e.pausePromptInjected,awaitingApproval:o?!1:e.awaitingApproval}}var ut="\x1B[38;5;208m",lt="\x1B[38;5;40m",ct="\x1B[38;5;245m",J="\x1B[39m",Se=e=>`${ut}${e}${J}`,dt=e=>`${lt}${e}${J}`,pt=e=>`${ct}${e}${J}`,B=e=>e.reserve===N?"spare10":`spare10 (${e.reserve}%)`;function ft(e,t,n){if(!t.badge)return"";if(e.blind)return"\u26A0 spare10 quota unavailable";if(e.pct===null)return pt(`\u29D7 ${B(t)}`);if(e.pct<w(t))return dt(`\u25CF ${B(t)}`);if(e.disarmedUntil!==null&&n<e.disarmedUntil)return Se(`\u2A2F ${B(t)}`);let s=e.tick%2===0?"\u26A0":" ";return Se(`${s} Pausing at next tool call`)}function mt(e,t){if(!e||e.includes("spare10 sensor"))return"";try{return(ot(e,{shell:!0,input:t,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function gt(){try{return it(0,"utf8")}catch{return""}}function we(e){let t=gt(),n=y(e),r=at(f(e),be(t),d());m(e,r);let s=[ft(r,n,d()),mt(n.chain,t)].filter(Boolean);return s.length>0&&process.stdout.write(s.join(" ")),0}function V(e,t){let n=e.indexOf(t);return n===-1?null:e[n+1]??null}function ht(e){let[t,...n]=e;switch(t){case"sensor":{let r=V(n,"--run");return r?we(r):0}case"gate":{let r=V(n,"--run");return r?ae(r):0}case"post":{let r=V(n,"--run");return r?ue(r):0}case"doctor":return ge();default:return fe(Q(e))}}try{process.exit(ht(process.argv.slice(2)))}catch(e){throw e instanceof u&&(e.message&&(process.stderr.write(`spare10: ${e.message}
31
-
32
- ${T}
33
- `),process.exit(2)),process.stdout.write(`${T}
34
- `),process.exit(0)),e}
19
+ spare10 --pause-prompt "Finish this block, commit, then stop." claude --resume`;function Ie(e){if(!/^\d+$/.test(e))throw new c(`--reserve must be a whole number: the API reports quota in integer percentages, so "${e}" cannot be honoured.`);let t=Number(e);if(t<1||t>99)throw new c(`--reserve must be between 1 and 99, got ${t}.`);return t}function Ce(e){if(!/^\d+$/.test(e)||Number(e)<1)throw new c(`--refresh must be a whole number of seconds >= 1, got "${e}".`);return Number(e)}function Z(e){let t={reserve:p.reserve,pausePrompt:p.pausePrompt,refresh:p.refresh,badge:p.badge},n=0,r=s=>{let i=e[n+1];if(i===void 0)throw new c(`${s} requires a value.`);return n+=1,i};for(;n<e.length;n+=1){let s=e[n];if(!s.startsWith("-"))break;switch(s){case"--reserve":t.reserve=Ie(r(s));break;case"--threshold":throw new c('--threshold was replaced by --reserve, which is the quota kept back rather than the level that trips. "--threshold 90" is now "--reserve 10".');case"--pause-prompt":t.pausePrompt=r(s);break;case"--refresh":t.refresh=Ce(r(s));break;case"--no-badge":t.badge=!1;break;case"-h":case"--help":throw new c("");default:throw new c(`Unknown option "${s}".`)}}let o=e.slice(n);if(o.length===0)throw new c("No command given. Try: spare10 claude");return{config:t,command:o}}import{spawnSync as Me}from"node:child_process";import{readFileSync as le}from"node:fs";var A={kind:"pass"};function D(e,t,n){return e.resetsAt!==null?n<e.resetsAt:e.updatedAt===null?!1:n-e.updatedAt<=t.refresh*X}function Te(e){return e===null?"an unknown time":new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function ee(e,t){return`into your ${t.reserve}% reserve \xB7 ${E(e.pct??0)}% of quota left \xB7 resets ${Te(e.resetsAt)}`}function _(e,t){return`spare10 \u2014 ${ee(e,t)}`}function Ne(e,t){return`spare10 budget guard. You have reached the safe usage limit for this session (${ee(e,t)}). Immediately wrap up your work and stop. Immediately stop any subagent, unless the user instructs otherwise.
20
+
21
+ User instructions: ${t.pausePrompt??""}`}function te(e,t,n){return v({state:e,config:t,now:n,agent:null}).kind!=="pass"}function v({state:e,config:t,now:n,agent:r}){return e.pct===null||e.updatedAt===null||e.blind||!D(e,t,n)||e.disarmedUntil!==null&&n<e.disarmedUntil||e.pct<w(t)?A:t.pausePrompt!==null?r!==null&&e.pausePromptInjectedTo.includes(r)?A:{kind:"inject",text:Ne(e,t)}:{kind:"halt",reason:`${_(e,t)}. Stop now and wait for the user. Do not call any further tools.`}}var ne={sessionId:null,toolName:null,agentId:null};function re(e){try{let t=JSON.parse(e);if(typeof t!="object"||t===null)return ne;let n=t;return{sessionId:typeof n.session_id=="string"?n.session_id:null,toolName:typeof n.tool_name=="string"?n.tool_name:null,agentId:typeof n.agent_id=="string"?n.agent_id:null}}catch{return ne}}import{readFileSync as Ue,writeFileSync as De,renameSync as Le,mkdirSync as Oe,unlinkSync as Fe}from"node:fs";import{join as L}from"node:path";var se=e=>L(e,"state.json"),O=e=>L(e,"config.json"),R=e=>L(e,"claude.pid");function m(){return Math.floor(Date.now()/1e3)}function oe(e){try{let t=JSON.parse(Ue(e,"utf8"));return typeof t=="object"&&t!==null?t:null}catch{return null}}var b=(e,t)=>typeof e=="number"&&Number.isFinite(e)?e:t,ie=(e,t)=>typeof e=="boolean"?e:t,je=e=>Array.isArray(e)?e.filter(t=>typeof t=="string"):[];function f(e){let t=oe(se(e));return t?{pct:b(t.pct,null),resetsAt:b(t.resetsAt,null),updatedAt:b(t.updatedAt,null),missingStreak:b(t.missingStreak,0)??0,blind:ie(t.blind,!1),disarmedUntil:b(t.disarmedUntil,null),pausePromptInjectedTo:je(t.pausePromptInjectedTo),halted:typeof t.halted=="string"&&t.halted?t.halted:null,tick:b(t.tick,0)??0}:{...T}}function h(e,t){let n=se(e),r=`${n}.${process.pid}.tmp`;try{Oe(e,{recursive:!0}),De(r,JSON.stringify(t),"utf8"),Le(r,n)}catch{try{Fe(r)}catch{}}}function x(e){let t=oe(O(e));if(!t)return{...p};let n=b(t.reserve,p.reserve)??p.reserve,r=b(t.refresh,p.refresh)??p.refresh;return{reserve:Math.min(99,Math.max(1,Math.round(n))),pausePrompt:typeof t.pausePrompt=="string"&&t.pausePrompt?t.pausePrompt:null,refresh:Math.max(1,Math.round(r)),badge:ie(t.badge,p.badge),chain:typeof t.chain=="string"&&t.chain?t.chain:null}}function F(e,t){return e.resetsAt??t+z}function ue(e,t){let r=e.filter(o=>o.pct!==null&&o.updatedAt!==null&&o.resetsAt!==null&&o.resetsAt>t).reduce((o,s)=>o===null||s.updatedAt>o.updatedAt?s:o,null);return r===null?null:{...T,pct:r.pct,resetsAt:r.resetsAt,updatedAt:r.updatedAt}}function ae(){try{return le(0,"utf8")}catch{return""}}function ce(e){process.stdout.write(JSON.stringify({hookSpecificOutput:e}))}var He=2e3;function Ge(e){try{let t=Number(le(R(e),"utf8").trim());return Number.isInteger(t)&&t>1?t:null}catch{return null}}function Ve(e){let t=Me("ps",["-o","ppid=","-p",String(e)],{encoding:"utf8"}),n=Number(t.stdout.trim());return t.status===0&&Number.isInteger(n)&&n>0?n:null}function qe(e,t=process.ppid){let n=t;for(let r=0;n!==null&&n>1&&r<32;r+=1){if(n===e)return!0;n=Ve(n)}return!1}function Be(e){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e)}function Je(e,t,n,r){let o=n===null?null:Ge(e);if(o!==null&&qe(o)){h(e,{...t,halted:n});try{process.kill(o,"SIGTERM"),Be(He)}catch{}}return ce({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:r}),0}function de(e){let t=x(e),n=f(e),r=m();if(v({state:n,config:t,now:r,agent:null}).kind==="pass")return ae(),0;let{sessionId:o,agentId:s}=re(ae()),i=Y(s),u=v({state:n,config:t,now:r,agent:i});switch(u.kind){case"pass":return 0;case"inject":return h(e,{...n,pausePromptInjectedTo:[...n.pausePromptInjectedTo,i]}),ce({hookEventName:"PreToolUse",additionalContext:u.text}),0;case"halt":return Je(e,n,o,u.reason)}}import{execFileSync as Ae}from"node:child_process";import{accessSync as xt,constants as Pt,readdirSync as kt,statSync as B}from"node:fs";import{join as Rt}from"node:path";function pe(e,t,n){if(e.pct===null||e.updatedAt===null)return{status:"no-data",detail:"no quota reading yet \u2014 start a session and wait a moment"};if(e.blind)return{status:"blind",detail:"Claude Code is not reporting rate_limits on this plan"};if(!D(e,t,n))return{status:"stale",detail:"the window this reading described has already reset"};if(e.disarmedUntil!==null&&n<e.disarmedUntil)return{status:"disarmed",detail:`consent given; quiet until ${M(e.disarmedUntil)}`};if(e.pct>=w(t)){let r=e.pausePromptInjectedTo.length,o=r>0?`; pause prompt delivered to ${r} agent${r===1?"":"s"}`:"";return{status:"tripped",detail:`into the ${t.reserve}% reserve${o}`}}return{status:"armed",detail:`${E(e.pct)}% left, of which ${t.reserve}% is reserved`}}function j(e){let t=Math.abs(Math.round(e));if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function M(e){return new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawn as We}from"node:child_process";import{closeSync as Ye,mkdirSync as Qe,openSync as Xe,readSync as ze,readdirSync as ge,rmSync as Ze,statSync as et,unlinkSync as tt,writeFileSync as he}from"node:fs";import{homedir as be}from"node:os";import{basename as nt,join as P}from"node:path";import{fileURLToPath as rt}from"node:url";import{readFileSync as Ke}from"node:fs";function H(e){return`'${e.replace(/'/g,"'\\''")}'`}function I(e){let t;try{t=JSON.parse(Ke(e,"utf8"))}catch{return{command:null,padding:void 0}}if(typeof t!="object"||t===null)return{command:null,padding:void 0};let n=t.statusLine;if(typeof n!="object"||n===null)return{command:null,padding:void 0};let r=n,o=typeof r.command=="string"?r.command:null,s=typeof r.padding=="number"?r.padding:void 0;return o!==null&&o.includes("spare10")?{command:null,padding:s}:{command:o,padding:s}}function me(e){let t=o=>`${H(e.nodePath)} ${H(e.selfPath)} ${o} --run ${H(e.runDir)}`,n=o=>[{matcher:"",hooks:[{type:"command",command:t(o)}]}],r={type:"command",command:t("sensor"),refreshInterval:e.refresh};return e.padding!==void 0&&(r.padding=e.padding),{statusLine:r,hooks:{PreToolUse:n("gate")}}}var st=7*24*60*60*1e3,G=()=>P(process.env.SPARE10_HOME??P(be(),".spare10"),"runs"),V=()=>P(be(),".claude","settings.json");function ot(e,t){let n;try{n=ge(e)}catch{return}for(let r of n){let o=P(e,r);try{t-et(o).mtimeMs>st&&Ze(o,{recursive:!0,force:!0})}catch{}}}function it(e,t){let n;try{n=ge(e)}catch{return}let r=n.map(s=>P(e,s)).filter(s=>s!==t).map(f),o=ue(r,m());o!==null&&h(t,o)}function ut(e){let t=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=P(e,t);return Qe(n,{recursive:!0}),n}function Se(e){let t;try{t=Xe("/dev/tty","r")}catch{return null}process.stderr.write(e);let n=Buffer.alloc(1),r="";try{for(;ze(t,n,0,1,null)>0;){let o=n.toString("utf8");if(o===`
22
+ `||o==="\r")break;r+=o}}catch{return null}finally{Ye(t)}return r.trim()}var ye=e=>/^y(es)?$/i.test(e.trim());function at(e,t){return e?t===null?"proceeding":ye(t)?"consented":"declined":"clear"}function lt(e,t){let n=f(e);return te(n,t,m())?(process.stderr.write(`
23
+ ${_(n,t)}
24
+ `),at(!0,Se("Start anyway? [y/N] "))):"clear"}function q(){return rt(import.meta.url)}function ct(e,t=process.env){return!(e.includes("--no-session-persistence")||t.CLAUDE_CODE_CHILD_SESSION&&!t.CLAUDE_CODE_FORCE_SESSION_PERSISTENCE)}var dt=new Set(["--agent","--agents","--append-system-prompt","--append-system-prompt-file","--autocompact","--debug-file","--effort","--environment","--fallback-model","--input-format","--json-schema","--max-budget-usd","--model","-n","--name","--output-format","--permission-mode","--permission-prompts","--plugin-dir","--plugin-url","--remote-control-session-name-prefix","--setting-sources","--settings","--system-prompt","--system-prompt-file","--system-prompt-snapshot"]),pt=new Set(["--add-dir","--allowedTools","--allowed-tools","--betas","--disallowedTools","--disallowed-tools","--file","--mcp-config","--tools"]),mt=new Set(["-d","--debug","--prompt-suggestions","--remote-control"]),ft=new Set(["-r","--resume","-w","--worktree","--from-pr","--teleport","--cloud"]),gt=new Set(["--session-id"]),ht=new Set(["-c","--continue","--tmux","--bg","--background"]),bt="spare10 budget guard: the user stopped this session as it entered the reserve and has now chosen to resume it. Continue from where you left off.";function St(e,t){let n=[],r=s=>e[s],o=s=>r(s)!==void 0&&!r(s).startsWith("-");for(let s=0;s<e.length;s+=1){let i=r(s);if(i==="--")break;let u=i.startsWith("-")&&i.includes("="),a=u?i.split("=")[0]:i;if(!ht.has(a)){if(gt.has(a)){u||(s+=1);continue}if(ft.has(a)){!u&&o(s+1)&&(s+=1);continue}if(dt.has(a)){n.push(i),!u&&r(s+1)!==void 0&&n.push(r(++s));continue}if(mt.has(a)){n.push(i),!u&&o(s+1)&&n.push(r(++s));continue}if(pt.has(a)){for(n.push(i);!u&&o(s+1);)n.push(r(++s));continue}if(i.startsWith("-")){n.push(i);continue}}}return["--resume",t,...n,bt]}function yt(e,t,n,r){return new Promise(o=>{let s=We(e,t,{stdio:"inherit"});r&&s.pid!==void 0&&he(R(n),String(s.pid));let i=u=>{try{tt(R(n))}catch{}o(u)};s.once("error",u=>i({status:null,signal:null,error:u})),s.once("exit",(u,a)=>i({status:u,signal:a,error:null}))})}var wt=e=>e.signal==="SIGTERM"||e.status===143;function fe(e){return e.signal?128+(e.signal==="SIGINT"?2:15):e.status??0}function At(e){return e===null?"unattended":ye(e)?"resume":"declined"}async function we({config:e,command:t}){let[n,...r]=t;if(nt(n)!=="claude")throw new c(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let o=G();ot(o,Date.now());let s=ut(o);it(o,s);let i=lt(s,{...e,chain:null});if(i==="declined")return process.stderr.write(`Not started.
25
+ `),0;if(i==="consented"){let d=f(s);h(s,{...d,disarmedUntil:F(d,m())}),process.stderr.write(`Continuing into the reserve for this window.
26
+
27
+ `)}let u=I(V()),a={...e,chain:u.command};he(O(s),JSON.stringify(a,null,2));let S=JSON.stringify(me({nodePath:process.execPath,selfPath:q(),runDir:s,refresh:e.refresh,padding:u.padding})),k=ct(r);k||process.stderr.write(`spare10: this session will not be saved, so it cannot be stopped and resumed; on trip the gate will deny tool calls instead.
28
+ `);let $=()=>{};process.on("SIGINT",$),process.on("SIGTERM",$);let g=r;for(;;){let d=await yt(n,["--settings",S,...g],s,k);if(d.error)return process.stderr.write(`spare10: could not start ${n}: ${d.error.message}
29
+ `),127;let y=f(s);if(!wt(d)||y.halted===null)return fe(d);process.stderr.write(`
30
+ ${_(y,a)}
31
+ spare10 stopped the agent between operations \u2014 nothing is left half-written.
32
+ `);let W=At(Se("Resume anyway? [y/N] "));if(W!=="resume")return process.stderr.write(`Not resumed. To pick it up later: claude --resume ${y.halted}
33
+ `),W==="declined"?0:fe(d);h(s,{...y,halted:null,disarmedUntil:F(y,m())}),process.stderr.write(`Resuming into the reserve for this window.
34
+
35
+ `),g=St(r,y.halted)}}var $t="0.3.0",l={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function Et(e){try{return kt(e).map(n=>Rt(e,n)).filter(n=>{try{return B(n).isDirectory()}catch{return!1}}).sort((n,r)=>B(r).mtimeMs-B(n).mtimeMs)[0]??null}catch{return null}}function _t(e){try{return Ae("command",["-v",e],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function vt(){try{return Ae("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function It(e){try{return xt(e,Pt.X_OK),!0}catch{return!1}}function xe(){let e=(g="")=>process.stdout.write(`${g}
36
+ `),t=m(),n=0;e(`spare10 ${$t}`),e(),e("Environment"),e(` ${l.info} node ${process.execPath} (${process.version})`),e(` ${l.info} spare10 ${q()}`);let r=_t("claude"),o=r?vt():null;r&&o?e(` ${l.ok} claude ${r} (${o})`):(n+=1,e(` ${l.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),e(),e("Status line");let s=I(V());if(s.command===null)e(` ${l.info} nothing to chain \u2014 spare10 will own the status line`);else{let g=s.command.split(/\s+/)[0]??"",d=It(g);d||(n+=1),e(` ${d?l.ok:l.warn} chains into ${s.command}`),d||e(" that path is not executable; its output will be dropped")}e();let i=Et(G());if(i===null)return e("No runs yet. Start one with: spare10 claude"),n>0?1:0;let u=x(i),a=f(i),S=pe(a,u,t);if(e(`Latest run ${i}`),e(` ${l.info} reserve ${u.reserve}% of the 5-hour window`),e(` ${l.info} on trip ${u.pausePrompt===null?"stop the session and ask in the terminal":`inject: ${JSON.stringify(u.pausePrompt)}`}`),e(` ${l.info} refresh ${u.refresh}s`),a.pct!==null&&a.updatedAt!==null){e(` ${l.ok} quota ${a.pct}% used, ${100-a.pct}% left`);let g=t-a.updatedAt;e(` ${l.info} last reading ${j(g)} ago`+(g>u.refresh*3?" (no session running; still valid for this window)":""))}a.resetsAt!==null&&e(` ${l.info} resets ${M(a.resetsAt)} (in ${j(a.resetsAt-t)})`);let k=S.status==="blind";k&&(n+=1);let $=k?l.warn:S.status==="no-data"?l.info:l.ok;return e(` ${$} state ${S.status.toUpperCase()} \u2014 ${S.detail}`),n>0?1:0}import{spawnSync as Ct}from"node:child_process";import{readFileSync as Tt}from"node:fs";function Pe(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function ke(e){let t;try{t=JSON.parse(e)}catch{return{sessionId:null,fiveHour:null}}if(typeof t!="object"||t===null)return{sessionId:null,fiveHour:null};let n=t,r=typeof n.session_id=="string"?n.session_id:null,o=n.rate_limits;if(typeof o!="object"||o===null)return{sessionId:r,fiveHour:null};let s=o.five_hour;if(typeof s!="object"||s===null)return{sessionId:r,fiveHour:null};let i=s,u=Pe(i.used_percentage);return u===null?{sessionId:r,fiveHour:null}:{sessionId:r,fiveHour:{usedPercentage:Math.min(100,Math.max(0,u)),resetsAt:Pe(i.resets_at)}}}function Nt(e,t,n){let r=t.fiveHour,o=e.tick+1;if(!r){let i=e.missingStreak+1;return{...e,tick:o,missingStreak:i,blind:i>=Q}}let s=r.resetsAt!==null&&e.resetsAt!==null&&r.resetsAt!==e.resetsAt;return{tick:o,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:s?null:e.disarmedUntil,pausePromptInjectedTo:s?[]:e.pausePromptInjectedTo,halted:e.halted}}var Ut="\x1B[38;5;208m",Dt="\x1B[38;5;40m",Lt="\x1B[38;5;245m",K="\x1B[39m",J=e=>`${Ut}${e}${K}`,Ot=e=>`${Dt}${e}${K}`,Ft=e=>`${Lt}${e}${K}`,C=e=>e.reserve===N?"spare10":`spare10 (${e.reserve}%)`;function jt(e,t,n){if(!t.badge)return"";if(e.blind)return"\u26A0 spare10 quota unavailable";if(e.pct===null)return Ft(`\u29D7 ${C(t)}`);if(e.pct<w(t))return Ot(`\u25CF ${C(t)}`);if(e.disarmedUntil!==null&&n<e.disarmedUntil)return J(`\u2A2F ${C(t)}`);if(e.pausePromptInjectedTo.length>0)return J(`\u23F8 ${C(t)}`);let o=e.tick%2===0?"\u26A0":" ";return J(`${o} Pausing at next tool call`)}function Mt(e,t){if(!e||e.includes("spare10 sensor"))return"";try{return(Ct(e,{shell:!0,input:t,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function Ht(){try{return Tt(0,"utf8")}catch{return""}}function Re(e){let t=Ht(),n=x(e),r=Nt(f(e),ke(t),m());h(e,r);let o=[jt(r,n,m()),Mt(n.chain,t)].filter(Boolean);return o.length>0&&process.stdout.write(o.join(" ")),0}function $e(e,t){let n=e.indexOf(t);return n===-1?null:e[n+1]??null}function Gt(e){let[t,...n]=e;switch(t){case"sensor":{let r=$e(n,"--run");return r?Re(r):0}case"gate":{let r=$e(n,"--run");return r?de(r):0}case"doctor":return xe();default:return we(Z(e))}}function Ee(e){throw e instanceof c&&(e.message&&(process.stderr.write(`spare10: ${e.message}
37
+
38
+ ${U}
39
+ `),process.exit(2)),process.stdout.write(`${U}
40
+ `),process.exit(0)),e}try{Promise.resolve(Gt(process.argv.slice(2))).then(e=>process.exit(e),Ee)}catch(e){Ee(e)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spare10",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Circuit breaker for Claude Code: pause autonomous sessions before the 5-hour quota runs out",
5
5
  "keywords": [
6
6
  "claude-code",