spare10 0.3.1 → 0.5.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 +47 -11
  2. package/dist/spare10.js +29 -23
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -14,16 +14,23 @@ When usage starts eating the reserve (the last 10% by default) it stops Claude C
14
14
  agent's next tool call — the whole process, subagents and background tasks included — and asks
15
15
  you in the terminal whether to carry on:
16
16
 
17
- ```
18
- spare10 — into your 10% reserve · 9% of quota left · resets 14:00
19
- spare10 stopped the agent between operationsnothing is left half-written.
20
- Resume anyway? [y/N]
21
- ```
17
+ <img width="1080" height="525" alt="spare10" src="https://github.com/user-attachments/assets/7e0107b1-a389-44dd-b5cc-638b6837cf88" />
18
+
19
+ The header above the card is the paused session's own version, model, effort level and
20
+ directory as that session reported them in the status line payload it was sending spare10 all
21
+ along, not the installed version or the default model. It stands outside the frame because it
22
+ describes the session, not the question.
23
+
24
+ `←`/`→` move between the options and Enter confirms; `y` and `n` still answer outright, and
25
+ Escape leaves the session alone. The selection starts on *Stop here*, so a stray Enter never
26
+ spends the reserve.
22
27
 
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.
28
+ Choose **Resume** and the session picks up right where it stopped, with its whole conversation,
29
+ and spare10 stays quiet until the limit resets. Choose **Stop here** and you are back at your
30
+ shell with the session saved; `claude --resume <id>` picks it up later, when the window has
31
+ reset. Terminals that cannot draw the panel — a pipe, `TERM=dumb`, `NO_COLOR`, a narrow
32
+ window — get the plain one-line question instead, and a run with nobody attached carries on
33
+ rather than waiting for an answer that cannot come.
27
34
 
28
35
  For unattended runs, hand it an instruction instead of a question:
29
36
 
@@ -128,6 +135,23 @@ The cost is that a turn producing only text is not gated. Before launching, spar
128
135
  knows your quota from the previous run, so `spare10 claude` asks for confirmation rather than
129
136
  starting a session that would stop on its first move.
130
137
 
138
+ **Background sessions are stopped through the CLI.** `spare10 claude agents` guards the whole
139
+ fleet: the agent view passes spare10's settings to every session it dispatches, so each one
140
+ carries its own sensor and gate. But a background session does not run under the launcher — it
141
+ lives under Claude Code's daemon — so there is no process for the gate to signal. Each kind of
142
+ session offers exactly one handle, and they are different ones:
143
+
144
+ | | listed with | stopped with | resumed with |
145
+ |---|---|---|---|
146
+ | interactive | a `pid` | `SIGTERM` | `claude --resume <session>` |
147
+ | background | a short `id` | `claude stop <id>` | `claude --bg --resume <session>` |
148
+
149
+ Nothing is attached to a background session to ask a question on, so spare10 stops it, keeps the
150
+ record, and asks once the wrapper is back in front of you: one panel listing every session it
151
+ stopped, resuming them all into the reserve if you say so. Anything left stopped is listed by
152
+ `spare10 doctor` with the command to reopen it — which is also how `spare10 claude --bg …` ends,
153
+ since that returns before there is anything to ask about.
154
+
131
155
  Three consequences worth knowing:
132
156
 
133
157
  **Nothing you own is modified.** `claude --settings` accepts a raw JSON string, so a run
@@ -164,7 +188,9 @@ Not in this version, deliberately:
164
188
  - **Overshoot correction.** The reserve is indicative, not predictive: readings land at 1%
165
189
  granularity and up to one refresh interval late, and parallel subagents can land several
166
190
  requests at once. Keep back more than you think you need.
167
- - **Coordination across sessions.** Quota is account-wide, but each run tracks its own state.
191
+ - **Coordination across runs.** Sessions started under one `spare10` command share its state,
192
+ including every background session dispatched from it. Separate runs do not: quota is
193
+ account-wide, but each run tracks it on its own, seeded from the last one.
168
194
 
169
195
  ## Known limitations
170
196
 
@@ -175,16 +201,26 @@ Not in this version, deliberately:
175
201
  denies the tool call instead, and the model may retry with another tool before it gives up.
176
202
  - Status line chaining reads user-level settings only. A status line configured in project or
177
203
  local settings is not detected.
204
+ - Stopping a background session goes through `claude stop`, called from inside the session being
205
+ stopped. If it does not land, the gate falls back to denying the tool call, as it does
206
+ everywhere else.
178
207
 
179
208
  ## Development
180
209
 
181
210
  ```bash
182
211
  npm install
183
- npm test # 151 tests: unit, plus the hooks and CLI as real subprocesses
212
+ npm test # 254 tests: unit, plus the hooks and CLI as real subprocesses
184
213
  npm run typecheck
185
214
  npm run build # single dependency-free bundle in dist/
215
+ npm run demo # the pause prompt at 91% usage, without burning a session
186
216
  ```
187
217
 
218
+ `npm run demo` draws the real panel from `src/`, so it cannot drift from what spare10 shows.
219
+ It is interactive by default; `-- --static` prints every state at once (both selections, the
220
+ no-colour fallback and the plain line prompt), `-- --preflight` shows the question asked
221
+ before launch rather than the one after a stop, and `-- --width 64` forces a narrower
222
+ terminal.
223
+
188
224
  Test fixtures are real payloads captured from a live Claude Code session, sanitized. When
189
225
  Anthropic changes the status line schema, the tolerant parser keeps spare10 failing open and
190
226
  `doctor` reports the drift.
package/dist/spare10.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var T={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjectedTo:[],halted:null,tick:0},_t="main";function Y(t){return t??_t}var N=10,vt=2,p={reserve:N,pausePrompt:null,refresh:vt,badge:!0,chain:null},Q=3,X=3,z=3600;function A(t){return 100-t.reserve}function E(t){return 100-t}var c=class extends Error{},U=`spare10 \u2014 pause Claude Code before the 5-hour quota runs out
2
+ var kt={version:null,model:null,effort:null,cwd:null,fastMode:!1},tt={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjectedTo:[],halted:null,tick:0,session:null},Me="main",Oe="unknown";function At(t,e){return`${t??Oe}:${e??Me}`}var et=10,Le=2,y={reserve:et,pausePrompt:null,refresh:Le,badge:!0,chain:null},Rt=3,Pt=3,It=3600;function C(t){return 100-t.reserve}function _(t){return 100-t}var b=class extends Error{},nt=`spare10 \u2014 pause Claude Code before the 5-hour quota runs out
3
3
 
4
4
  spare10 [options] <command> [args...]
5
5
 
@@ -16,25 +16,31 @@ 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 It(t){if(!/^\d+$/.test(t))throw new c(`--reserve must be a whole number: the API reports quota in integer percentages, so "${t}" cannot be honoured.`);let e=Number(t);if(e<1||e>99)throw new c(`--reserve must be between 1 and 99, got ${e}.`);return e}function Ct(t){if(!/^\d+$/.test(t)||Number(t)<1)throw new c(`--refresh must be a whole number of seconds >= 1, got "${t}".`);return Number(t)}function Z(t){let e={reserve:p.reserve,pausePrompt:p.pausePrompt,refresh:p.refresh,badge:p.badge},n=0,r=s=>{let i=t[n+1];if(i===void 0)throw new c(`${s} requires a value.`);return n+=1,i};for(;n<t.length;n+=1){let s=t[n];if(!s.startsWith("-"))break;switch(s){case"--reserve":e.reserve=It(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":e.pausePrompt=r(s);break;case"--refresh":e.refresh=Ct(r(s));break;case"--no-badge":e.badge=!1;break;case"-h":case"--help":throw new c("");default:throw new c(`Unknown option "${s}".`)}}let o=t.slice(n);if(o.length===0)throw new c("No command given. Try: spare10 claude");return{config:e,command:o}}import{spawnSync as Mt}from"node:child_process";import{readFileSync as lt}from"node:fs";var x={kind:"pass"};function D(t,e,n){return t.resetsAt!==null?n<t.resetsAt:t.updatedAt===null?!1:n-t.updatedAt<=e.refresh*X}function Tt(t){return t===null?"an unknown time":new Date(t*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function tt(t,e){return`into your ${e.reserve}% reserve \xB7 ${E(t.pct??0)}% of quota left \xB7 resets ${Tt(t.resetsAt)}`}function _(t,e){return`spare10 \u2014 ${tt(t,e)}`}function Nt(t,e){return`spare10 budget guard. You have reached the safe usage limit for this session (${tt(t,e)}). Immediately wrap up your work and stop. Immediately stop any subagent, unless the user instructs otherwise.
20
-
21
- User instructions: ${e.pausePrompt??""}`}function et(t,e,n){return v({state:t,config:e,now:n,agent:null}).kind!=="pass"}function v({state:t,config:e,now:n,agent:r}){return t.pct===null||t.updatedAt===null||t.blind||!D(t,e,n)||t.disarmedUntil!==null&&n<t.disarmedUntil||t.pct<A(e)?x:e.pausePrompt!==null?r!==null&&t.pausePromptInjectedTo.includes(r)?x:{kind:"inject",text:Nt(t,e)}:{kind:"halt",reason:`${_(t,e)}. Stop now and wait for the user. Do not call any further tools.`}}var nt={sessionId:null,toolName:null,agentId:null};function rt(t){try{let e=JSON.parse(t);if(typeof e!="object"||e===null)return nt;let n=e;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 nt}}import{readFileSync as Ut,writeFileSync as Dt,renameSync as Lt,mkdirSync as Ot,unlinkSync as Ft}from"node:fs";import{join as L}from"node:path";var st=t=>L(t,"state.json"),O=t=>L(t,"config.json"),R=t=>L(t,"claude.pid");function f(){return Math.floor(Date.now()/1e3)}function ot(t){try{let e=JSON.parse(Ut(t,"utf8"));return typeof e=="object"&&e!==null?e:null}catch{return null}}var b=(t,e)=>typeof t=="number"&&Number.isFinite(t)?t:e,it=(t,e)=>typeof t=="boolean"?t:e,jt=t=>Array.isArray(t)?t.filter(e=>typeof e=="string"):[];function m(t){let e=ot(st(t));return e?{pct:b(e.pct,null),resetsAt:b(e.resetsAt,null),updatedAt:b(e.updatedAt,null),missingStreak:b(e.missingStreak,0)??0,blind:it(e.blind,!1),disarmedUntil:b(e.disarmedUntil,null),pausePromptInjectedTo:jt(e.pausePromptInjectedTo),halted:typeof e.halted=="string"&&e.halted?e.halted:null,tick:b(e.tick,0)??0}:{...T}}function h(t,e){let n=st(t),r=`${n}.${process.pid}.tmp`;try{Ot(t,{recursive:!0}),Dt(r,JSON.stringify(e),"utf8"),Lt(r,n)}catch{try{Ft(r)}catch{}}}function P(t){let e=ot(O(t));if(!e)return{...p};let n=b(e.reserve,p.reserve)??p.reserve,r=b(e.refresh,p.refresh)??p.refresh;return{reserve:Math.min(99,Math.max(1,Math.round(n))),pausePrompt:typeof e.pausePrompt=="string"&&e.pausePrompt?e.pausePrompt:null,refresh:Math.max(1,Math.round(r)),badge:it(e.badge,p.badge),chain:typeof e.chain=="string"&&e.chain?e.chain:null}}function F(t,e){return t.resetsAt??e+z}function ut(t,e){let r=t.filter(o=>o.pct!==null&&o.updatedAt!==null&&o.resetsAt!==null&&o.resetsAt>e).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 at(){try{return lt(0,"utf8")}catch{return""}}function ct(t){process.stdout.write(JSON.stringify({hookSpecificOutput:t}))}var Ht=2e3;function Gt(t){try{let e=Number(lt(R(t),"utf8").trim());return Number.isInteger(e)&&e>1?e:null}catch{return null}}function Vt(t){let e=Mt("ps",["-o","ppid=","-p",String(t)],{encoding:"utf8"}),n=Number(e.stdout.trim());return e.status===0&&Number.isInteger(n)&&n>0?n:null}function qt(t,e=process.ppid){let n=e;for(let r=0;n!==null&&n>1&&r<32;r+=1){if(n===t)return!0;n=Vt(n)}return!1}function Bt(t){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,t)}function Jt(t,e,n,r){let o=n===null?null:Gt(t);if(o!==null&&qt(o)){h(t,{...e,halted:n});try{process.kill(o,"SIGTERM"),Bt(Ht)}catch{}}return ct({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:r}),0}function dt(t){let e=P(t),n=m(t),r=f();if(v({state:n,config:e,now:r,agent:null}).kind==="pass")return at(),0;let{sessionId:o,agentId:s}=rt(at()),i=Y(s),u=v({state:n,config:e,now:r,agent:i});switch(u.kind){case"pass":return 0;case"inject":return h(t,{...n,pausePromptInjectedTo:[...n.pausePromptInjectedTo,i]}),ct({hookEventName:"PreToolUse",additionalContext:u.text}),0;case"halt":return Jt(t,n,o,u.reason)}}import{execFileSync as At}from"node:child_process";import{accessSync as ke,constants as Re,readdirSync as $e,statSync as B}from"node:fs";import{join as Ee}from"node:path";function pt(t,e,n){if(t.pct===null||t.updatedAt===null)return{status:"no-data",detail:"no quota reading yet \u2014 start a session and wait a moment"};if(t.blind)return{status:"blind",detail:"Claude Code is not reporting rate_limits on this plan"};if(!D(t,e,n))return{status:"stale",detail:"the window this reading described has already reset"};if(t.disarmedUntil!==null&&n<t.disarmedUntil)return{status:"disarmed",detail:`consent given; quiet until ${M(t.disarmedUntil)}`};if(t.pct>=A(e)){let r=t.pausePromptInjectedTo.length,o=r>0?`; pause prompt delivered to ${r} agent${r===1?"":"s"}`:"";return{status:"tripped",detail:`into the ${e.reserve}% reserve${o}`}}return{status:"armed",detail:`${E(t.pct)}% left, of which ${e.reserve}% is reserved`}}function j(t){let e=Math.abs(Math.round(t));if(e<60)return`${e}s`;let n=Math.floor(e/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function M(t){return new Date(t*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawn as Wt}from"node:child_process";import{closeSync as Yt,copyFileSync as Qt,mkdirSync as Xt,openSync as zt,readSync as Zt,readdirSync as gt,rmSync as te,statSync as ee,unlinkSync as ne,writeFileSync as ht}from"node:fs";import{homedir as bt}from"node:os";import{basename as re,join as S}from"node:path";import{fileURLToPath as se}from"node:url";import{readFileSync as Kt}from"node:fs";function H(t){return`'${t.replace(/'/g,"'\\''")}'`}function I(t){let e;try{e=JSON.parse(Kt(t,"utf8"))}catch{return{command:null,padding:void 0}}if(typeof e!="object"||e===null)return{command:null,padding:void 0};let n=e.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 ft(t){let e=o=>`${H(t.nodePath)} ${H(t.selfPath)} ${o} --run ${H(t.runDir)}`,n=o=>[{matcher:"",hooks:[{type:"command",command:e(o)}]}],r={type:"command",command:e("sensor"),refreshInterval:t.refresh};return t.padding!==void 0&&(r.padding=t.padding),{statusLine:r,hooks:{PreToolUse:n("gate")}}}var oe=7*24*60*60*1e3,G=()=>S(process.env.SPARE10_HOME??S(bt(),".spare10"),"runs"),V=()=>S(bt(),".claude","settings.json");function ie(t,e){let n;try{n=gt(t)}catch{return}for(let r of n){let o=S(t,r);try{e-ee(o).mtimeMs>oe&&te(o,{recursive:!0,force:!0})}catch{}}}function ue(t,e){let n;try{n=gt(t)}catch{return}let r=n.map(s=>S(t,s)).filter(s=>s!==e).map(m),o=ut(r,f());o!==null&&h(e,o)}function ae(t){let e=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=S(t,e);return Xt(n,{recursive:!0}),n}function St(t){let e;try{e=zt("/dev/tty","r")}catch{return null}process.stderr.write(t);let n=Buffer.alloc(1),r="";try{for(;Zt(e,n,0,1,null)>0;){let o=n.toString("utf8");if(o===`
22
- `||o==="\r")break;r+=o}}catch{return null}finally{Yt(e)}return r.trim()}var yt=t=>/^y(es)?$/i.test(t.trim());function le(t,e){return t?e===null?"proceeding":yt(e)?"consented":"declined":"clear"}function ce(t,e){let n=m(t);return et(n,e,f())?(process.stderr.write(`
23
- ${_(n,e)}
24
- `),le(!0,St("Start anyway? [y/N] "))):"clear"}function q(){return se(import.meta.url)}function de(t,e=q()){let n=S(t,"spare10.js");try{return Qt(e,n),n}catch{return e}}function pe(t,e=process.env){return!(t.includes("--no-session-persistence")||e.CLAUDE_CODE_CHILD_SESSION&&!e.CLAUDE_CODE_FORCE_SESSION_PERSISTENCE)}var fe=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"]),me=new Set(["--add-dir","--allowedTools","--allowed-tools","--betas","--disallowedTools","--disallowed-tools","--file","--mcp-config","--tools"]),ge=new Set(["-d","--debug","--prompt-suggestions","--remote-control"]),he=new Set(["-r","--resume","-w","--worktree","--from-pr","--teleport","--cloud"]),be=new Set(["--session-id"]),Se=new Set(["-c","--continue","--tmux","--bg","--background"]),ye="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 we(t,e){let n=[],r=s=>t[s],o=s=>r(s)!==void 0&&!r(s).startsWith("-");for(let s=0;s<t.length;s+=1){let i=r(s);if(i==="--")break;let u=i.startsWith("-")&&i.includes("="),a=u?i.split("=")[0]:i;if(!Se.has(a)){if(be.has(a)){u||(s+=1);continue}if(he.has(a)){!u&&o(s+1)&&(s+=1);continue}if(fe.has(a)){n.push(i),!u&&r(s+1)!==void 0&&n.push(r(++s));continue}if(ge.has(a)){n.push(i),!u&&o(s+1)&&n.push(r(++s));continue}if(me.has(a)){for(n.push(i);!u&&o(s+1);)n.push(r(++s));continue}if(i.startsWith("-")){n.push(i);continue}}}return["--resume",e,...n,ye]}function Ae(t,e,n,r){return new Promise(o=>{let s=Wt(t,e,{stdio:"inherit"});r&&s.pid!==void 0&&ht(R(n),String(s.pid));let i=u=>{try{ne(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 xe=t=>t.signal==="SIGTERM"||t.status===143;function mt(t){return t.signal?128+(t.signal==="SIGINT"?2:15):t.status??0}function Pe(t){return t===null?"unattended":yt(t)?"resume":"declined"}async function wt({config:t,command:e}){let[n,...r]=e;if(re(n)!=="claude")throw new c(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let o=G();ie(o,Date.now());let s=ae(o);ue(o,s);let i=ce(s,{...t,chain:null});if(i==="declined")return process.stderr.write(`Not started.
25
- `),0;if(i==="consented"){let d=m(s);h(s,{...d,disarmedUntil:F(d,f())}),process.stderr.write(`Continuing into the reserve for this window.
26
-
27
- `)}let u=I(V()),a={...t,chain:u.command};ht(O(s),JSON.stringify(a,null,2));let y=JSON.stringify(ft({nodePath:process.execPath,selfPath:de(s),runDir:s,refresh:t.refresh,padding:u.padding})),k=pe(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 Ae(n,["--settings",y,...g],s,k);if(d.error)return process.stderr.write(`spare10: could not start ${n}: ${d.error.message}
29
- `),127;let w=m(s);if(!xe(d)||w.halted===null)return mt(d);process.stderr.write(`
30
- ${_(w,a)}
31
- spare10 stopped the agent between operations \u2014 nothing is left half-written.
32
- `);let W=Pe(St("Resume anyway? [y/N] "));if(W!=="resume")return process.stderr.write(`Not resumed. To pick it up later: claude --resume ${w.halted}
33
- `),W==="declined"?0:mt(d);h(s,{...w,halted:null,disarmedUntil:F(w,f())}),process.stderr.write(`Resuming into the reserve for this window.
34
-
35
- `),g=we(r,w.halted)}}var _e="0.3.1",l={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function ve(t){try{return $e(t).map(n=>Ee(t,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 Ie(t){try{return At("command",["-v",t],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function Ce(){try{return At("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function Te(t){try{return ke(t,Re.X_OK),!0}catch{return!1}}function xt(){let t=(g="")=>process.stdout.write(`${g}
36
- `),e=f(),n=0;t(`spare10 ${_e}`),t(),t("Environment"),t(` ${l.info} node ${process.execPath} (${process.version})`),t(` ${l.info} spare10 ${q()}`);let r=Ie("claude"),o=r?Ce():null;r&&o?t(` ${l.ok} claude ${r} (${o})`):(n+=1,t(` ${l.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),t(),t("Status line");let s=I(V());if(s.command===null)t(` ${l.info} nothing to chain \u2014 spare10 will own the status line`);else{let g=s.command.split(/\s+/)[0]??"",d=Te(g);d||(n+=1),t(` ${d?l.ok:l.warn} chains into ${s.command}`),d||t(" that path is not executable; its output will be dropped")}t();let i=ve(G());if(i===null)return t("No runs yet. Start one with: spare10 claude"),n>0?1:0;let u=P(i),a=m(i),y=pt(a,u,e);if(t(`Latest run ${i}`),t(` ${l.info} reserve ${u.reserve}% of the 5-hour window`),t(` ${l.info} on trip ${u.pausePrompt===null?"stop the session and ask in the terminal":`inject: ${JSON.stringify(u.pausePrompt)}`}`),t(` ${l.info} refresh ${u.refresh}s`),a.pct!==null&&a.updatedAt!==null){t(` ${l.ok} quota ${a.pct}% used, ${100-a.pct}% left`);let g=e-a.updatedAt;t(` ${l.info} last reading ${j(g)} ago`+(g>u.refresh*3?" (no session running; still valid for this window)":""))}a.resetsAt!==null&&t(` ${l.info} resets ${M(a.resetsAt)} (in ${j(a.resetsAt-e)})`);let k=y.status==="blind";k&&(n+=1);let $=k?l.warn:y.status==="no-data"?l.info:l.ok;return t(` ${$} state ${y.status.toUpperCase()} \u2014 ${y.detail}`),n>0?1:0}import{spawnSync as Ne}from"node:child_process";import{readFileSync as Ue}from"node:fs";function Pt(t){return typeof t=="number"&&Number.isFinite(t)?t:null}function kt(t){let e;try{e=JSON.parse(t)}catch{return{sessionId:null,fiveHour:null}}if(typeof e!="object"||e===null)return{sessionId:null,fiveHour:null};let n=e,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=Pt(i.used_percentage);return u===null?{sessionId:r,fiveHour:null}:{sessionId:r,fiveHour:{usedPercentage:Math.min(100,Math.max(0,u)),resetsAt:Pt(i.resets_at)}}}function De(t,e,n){let r=e.fiveHour,o=t.tick+1;if(!r){let i=t.missingStreak+1;return{...t,tick:o,missingStreak:i,blind:i>=Q}}let s=r.resetsAt!==null&&t.resetsAt!==null&&r.resetsAt!==t.resetsAt;return{tick:o,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:s?null:t.disarmedUntil,pausePromptInjectedTo:s?[]:t.pausePromptInjectedTo,halted:t.halted}}var Le="\x1B[38;5;208m",Oe="\x1B[38;5;40m",Fe="\x1B[38;5;245m",K="\x1B[39m",J=t=>`${Le}${t}${K}`,je=t=>`${Oe}${t}${K}`,Me=t=>`${Fe}${t}${K}`,C=t=>t.reserve===N?"spare10":`spare10 (${t.reserve}%)`;function He(t,e,n){if(!e.badge)return"";if(t.blind)return"\u26A0 spare10 quota unavailable";if(t.pct===null)return Me(`\u29D7 ${C(e)}`);if(t.pct<A(e))return je(`\u25CF ${C(e)}`);if(t.disarmedUntil!==null&&n<t.disarmedUntil)return J(`\u2A2F ${C(e)}`);if(t.pausePromptInjectedTo.length>0)return J(`\u23F8 ${C(e)}`);let o=t.tick%2===0?"\u26A0":" ";return J(`${o} Pausing at next tool call`)}function Ge(t,e){if(!t||t.includes("spare10 sensor"))return"";try{return(Ne(t,{shell:!0,input:e,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function Ve(){try{return Ue(0,"utf8")}catch{return""}}function Rt(t){let e=Ve(),n=P(t),r=De(m(t),kt(e),f());h(t,r);let o=[He(r,n,f()),Ge(n.chain,e)].filter(Boolean);return o.length>0&&process.stdout.write(o.join(" ")),0}function $t(t,e){let n=t.indexOf(e);return n===-1?null:t[n+1]??null}function qe(t){let[e,...n]=t;switch(e){case"sensor":{let r=$t(n,"--run");return r?Rt(r):0}case"gate":{let r=$t(n,"--run");return r?dt(r):0}case"doctor":return xt();default:return wt(Z(t))}}function Et(t){throw t instanceof c&&(t.message&&(process.stderr.write(`spare10: ${t.message}
37
-
38
- ${U}
39
- `),process.exit(2)),process.stdout.write(`${U}
40
- `),process.exit(0)),t}try{Promise.resolve(qe(process.argv.slice(2))).then(t=>process.exit(t),Et)}catch(t){Et(t)}
19
+ spare10 --pause-prompt "Finish this block, commit, then stop." claude --resume`;function Ue(t){if(!/^\d+$/.test(t))throw new b(`--reserve must be a whole number: the API reports quota in integer percentages, so "${t}" cannot be honoured.`);let e=Number(t);if(e<1||e>99)throw new b(`--reserve must be between 1 and 99, got ${e}.`);return e}function De(t){if(!/^\d+$/.test(t)||Number(t)<1)throw new b(`--refresh must be a whole number of seconds >= 1, got "${t}".`);return Number(t)}function Et(t){let e={reserve:y.reserve,pausePrompt:y.pausePrompt,refresh:y.refresh,badge:y.badge},n=0,r=o=>{let i=t[n+1];if(i===void 0)throw new b(`${o} requires a value.`);return n+=1,i};for(;n<t.length;n+=1){let o=t[n];if(!o.startsWith("-"))break;switch(o){case"--reserve":e.reserve=Ue(r(o));break;case"--threshold":throw new b('--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":e.pausePrompt=r(o);break;case"--refresh":e.refresh=De(r(o));break;case"--no-badge":e.badge=!1;break;case"-h":case"--help":throw new b("");default:throw new b(`Unknown option "${o}".`)}}let s=t.slice(n);if(s.length===0)throw new b("No command given. Try: spare10 claude");return{config:e,command:s}}import{spawnSync as Xe}from"node:child_process";import{readFileSync as Kt}from"node:fs";import{spawnSync as vt}from"node:child_process";var Ct=1e4,D=t=>typeof t=="string"&&t?t:null;function je(t){let e;try{e=JSON.parse(t)}catch{return[]}if(!Array.isArray(e))return[];let n=[];for(let r of e){if(typeof r!="object"||r===null)continue;let s=r,o=D(s.sessionId);o!==null&&n.push({sessionId:o,backgroundId:D(s.id),kind:D(s.kind),name:D(s.name),cwd:D(s.cwd)})}return n}function _t(t,e){for(let n of t)if(n.sessionId===e)return n.kind!=="background"||n.backgroundId===null?null:{...n,backgroundId:n.backgroundId};return null}function B(t,e){let n=new Set(e.map(r=>r.sessionId));return t.filter(r=>!n.has(r.sessionId))}function Tt(t,e){try{return vt("claude",t,{encoding:"utf8",timeout:Ct,stdio:"ignore",...e===void 0?{}:{cwd:e}}).status===0}catch{return!1}}function T(){try{let t=vt("claude",["agents","--json"],{encoding:"utf8",timeout:Ct});return t.status!==0||typeof t.stdout!="string"?[]:je(t.stdout)}catch{return[]}}function Nt(t){return Tt(["stop",t])}function Fe(t){return["--bg","--resume",t]}function Mt(t,e){return Tt(Fe(t),e??void 0)}var N={kind:"pass"};function rt(t,e,n){return t.resetsAt!==null?n<t.resetsAt:t.updatedAt===null?!1:n-t.updatedAt<=e.refresh*Pt}function H(t){return t===null?"an unknown time":new Date(t*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function Ot(t,e){return`into your ${e.reserve}% reserve \xB7 ${_(t.pct??0)}% of quota left \xB7 resets ${H(t.resetsAt)}`}function Be(t,e){return`spare10 \u2014 ${Ot(t,e)}`}function He(t,e){return`spare10 budget guard. You have reached the safe usage limit for this session (${Ot(t,e)}). Immediately wrap up your work and stop. Immediately stop any subagent, unless the user instructs otherwise.
20
+
21
+ User instructions: ${e.pausePrompt??""}`}function Lt(t,e,n){return V({state:t,config:e,now:n,agent:null}).kind!=="pass"}function V({state:t,config:e,now:n,agent:r}){return t.pct===null||t.updatedAt===null||t.blind||!rt(t,e,n)||t.disarmedUntil!==null&&n<t.disarmedUntil||t.pct<C(e)?N:e.pausePrompt!==null?r!==null&&t.pausePromptInjectedTo.includes(r)?N:{kind:"inject",text:He(t,e)}:{kind:"halt",reason:`${Be(t,e)}. Stop now and wait for the user. Do not call any further tools.`}}var Ut={sessionId:null,toolName:null,agentId:null};function Dt(t){try{let e=JSON.parse(t);if(typeof e!="object"||e===null)return Ut;let n=e;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 Ut}}import{readFileSync as Ve,readdirSync as Ge,writeFileSync as We,renameSync as qe,mkdirSync as Je,unlinkSync as jt}from"node:fs";import{join as I}from"node:path";var Ft=t=>I(t,"state.json"),st=t=>I(t,"config.json"),j=t=>I(t,"claude.pid"),G=t=>I(t,"stopped");function S(){return Math.floor(Date.now()/1e3)}function ot(t){try{let e=JSON.parse(Ve(t,"utf8"));return typeof e=="object"&&e!==null?e:null}catch{return null}}var A=(t,e)=>typeof t=="number"&&Number.isFinite(t)?t:e,Bt=(t,e)=>typeof t=="boolean"?t:e,Ke=t=>Array.isArray(t)?t.filter(e=>typeof e=="string"):[],R=t=>typeof t=="string"&&t?t:null;function Ye(t){if(typeof t!="object"||t===null)return null;let e=t;return{...kt,version:R(e.version),model:R(e.model),effort:R(e.effort),cwd:R(e.cwd),fastMode:e.fastMode===!0}}function $(t){let e=ot(Ft(t));return e?{pct:A(e.pct,null),resetsAt:A(e.resetsAt,null),updatedAt:A(e.updatedAt,null),missingStreak:A(e.missingStreak,0)??0,blind:Bt(e.blind,!1),disarmedUntil:A(e.disarmedUntil,null),pausePromptInjectedTo:Ke(e.pausePromptInjectedTo),halted:typeof e.halted=="string"&&e.halted?e.halted:null,tick:A(e.tick,0)??0,session:Ye(e.session)}:{...tt}}function Ht(t,e,n){let r=`${e}.${process.pid}.tmp`;try{Je(t,{recursive:!0}),We(r,JSON.stringify(n),"utf8"),qe(r,e)}catch{try{jt(r)}catch{}}}function k(t,e){Ht(t,Ft(t),e)}function Vt(t,e){let n=G(t);Ht(n,I(n,`${e.sessionId}.json`),e)}function W(t){let e;try{e=Ge(G(t))}catch{return[]}let n=[];for(let r of e){if(!r.endsWith(".json"))continue;let s=ot(I(G(t),r));if(!s)continue;let o=R(s.sessionId),i=R(s.backgroundId);o===null||i===null||n.push({sessionId:o,backgroundId:i,name:R(s.name),cwd:R(s.cwd),at:A(s.at,0)??0})}return n.sort((r,s)=>r.at-s.at)}function it(t,e){for(let n of e)try{jt(I(G(t),`${n}.json`))}catch{}}function M(t){let e=ot(st(t));if(!e)return{...y};let n=A(e.reserve,y.reserve)??y.reserve,r=A(e.refresh,y.refresh)??y.refresh;return{reserve:Math.min(99,Math.max(1,Math.round(n))),pausePrompt:typeof e.pausePrompt=="string"&&e.pausePrompt?e.pausePrompt:null,refresh:Math.max(1,Math.round(r)),badge:Bt(e.badge,y.badge),chain:typeof e.chain=="string"&&e.chain?e.chain:null}}function q(t,e){return t.resetsAt??e+It}function Gt(t,e){let r=t.filter(s=>s.pct!==null&&s.updatedAt!==null&&s.resetsAt!==null&&s.resetsAt>e).reduce((s,o)=>s===null||o.updatedAt>s.updatedAt?o:s,null);return r===null?null:{...tt,pct:r.pct,resetsAt:r.resetsAt,updatedAt:r.updatedAt}}function Wt(){try{return Kt(0,"utf8")}catch{return""}}function Yt(t){process.stdout.write(JSON.stringify({hookSpecificOutput:t}))}var qt=2e3;function Qe(t){try{let e=Number(Kt(j(t),"utf8").trim());return Number.isInteger(e)&&e>1?e:null}catch{return null}}function ze(t){let e=Xe("ps",["-o","ppid=","-p",String(t)],{encoding:"utf8"}),n=Number(e.stdout.trim());return e.status===0&&Number.isInteger(n)&&n>0?n:null}function Ze(t,e=process.ppid){let n=e;for(let r=0;n!==null&&n>1&&r<32;r+=1){if(n===t)return!0;n=ze(n)}return!1}function Jt(t){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,t)}function tn(t,e,n,r){let s=n===null?null:Qe(t);if(s!==null&&Ze(s)){k(t,{...e,halted:n});try{process.kill(s,"SIGTERM"),Jt(qt)}catch{}}else if(n!==null){let o=_t(T(),n);o!==null&&(Vt(t,{sessionId:n,backgroundId:o.backgroundId,name:o.name,cwd:o.cwd,at:S()}),Nt(o.backgroundId),Jt(qt))}return Yt({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:r}),0}function Xt(t){let e=M(t),n=$(t),r=S();if(V({state:n,config:e,now:r,agent:null}).kind==="pass")return Wt(),0;let{sessionId:s,agentId:o}=Dt(Wt()),i=At(s,o),u=V({state:n,config:e,now:r,agent:i});switch(u.kind){case"pass":return 0;case"inject":return k(t,{...n,pausePromptInjectedTo:[...n.pausePromptInjectedTo,i]}),Yt({hookEventName:"PreToolUse",additionalContext:u.text}),0;case"halt":return tn(t,n,s,u.reason)}}import{execFileSync as Ae}from"node:child_process";import{accessSync as er,constants as nr,readdirSync as rr,statSync as bt}from"node:fs";import{join as sr}from"node:path";function Qt(t,e,n){if(t.pct===null||t.updatedAt===null)return{status:"no-data",detail:"no quota reading yet \u2014 start a session and wait a moment"};if(t.blind)return{status:"blind",detail:"Claude Code is not reporting rate_limits on this plan"};if(!rt(t,e,n))return{status:"stale",detail:"the window this reading described has already reset"};if(t.disarmedUntil!==null&&n<t.disarmedUntil)return{status:"disarmed",detail:`consent given; quiet until ${at(t.disarmedUntil)}`};if(t.pct>=C(e)){let r=t.pausePromptInjectedTo.length,s=r>0?`; pause prompt delivered to ${r} agent${r===1?"":"s"}`:"";return{status:"tripped",detail:`into the ${e.reserve}% reserve${s}`}}return{status:"armed",detail:`${_(t.pct)}% left, of which ${e.reserve}% is reserved`}}function ut(t){let e=Math.abs(Math.round(t));if(e<60)return`${e}s`;let n=Math.floor(e/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function at(t){return new Date(t*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawn as Pn}from"node:child_process";import{copyFileSync as In,mkdirSync as En,readdirSync as fe,rmSync as vn,statSync as Cn,unlinkSync as _n,writeFileSync as me}from"node:fs";import{homedir as mt}from"node:os";import{basename as Tn,join as E}from"node:path";import{fileURLToPath as Nn}from"node:url";var lt=46,en=78,ne=/\u001b\[[0-9;]*m/g,F=t=>t.replace(ne,"").length,O=[217,119,87],zt=173;var nn=236,rn=245;function re(t){if(!t.color){let r=s=>s;return{brand:r,dim:r,bold:r,chosen:s=>`[${s}]`}}let e=t.truecolor?`\x1B[38;2;${O[0]};${O[1]};${O[2]}m`:`\x1B[38;5;${zt}m`,n=t.truecolor?`\x1B[48;2;${O[0]};${O[1]};${O[2]}m`:`\x1B[48;5;${zt}m`;return{brand:r=>`${e}${r}\x1B[39m`,dim:r=>`\x1B[38;5;${rn}m${r}\x1B[39m`,bold:r=>`\x1B[1m${r}\x1B[22m`,chosen:r=>`${n}\x1B[38;5;${nn}m\x1B[1m${r}\x1B[0m`}}var se=[" \u2590\u259B\u2588\u2588\u2588\u259B\u2588","\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2580"," \u259D\u259D \u259D\u259D"],oe=Math.max(...se.map(t=>t.length));function sn(t){if(!t.unicode||!t.color)return[];let e=re(t);return se.map(n=>e.brand(n.padEnd(oe)))}function ct(t,e){return e&&(t===e||t.startsWith(`${e}/`))?`~${t.slice(e.length)}`:t}function on(t,e,n){let r=n.bold("Claude Code"),s=t?.version?` ${n.dim(`v${t.version}`)}`:"",o=[`${r}${s}`],i=t?.model??null;if(i!==null){let u=t?.effort?` with ${t.effort} effort`:"",a=t?.fastMode?" \xB7 fast mode":"";o.push(n.dim(`${i}${u}${a}`))}return t?.cwd&&o.push(n.dim(ct(t.cwd,e))),o}var Zt={unicode:{filled:"\u2588",empty:"\u2592"},ascii:{filled:"#",empty:"-"}};function un(t,e,n,r){let s=r?Zt.unicode:Zt.ascii,o=Math.max(4,e),i=Math.min(o,Math.max(0,Math.round(t/100*o)));return`${n.brand(s.filled.repeat(i))}${n.dim(s.empty.repeat(o-i))}`}function ie(t){return t.pct===null?`quota unknown \xB7 resets ${H(t.resetsAt)}`:`${t.pct}% used \xB7 ${_(t.pct)}% left \xB7 resets ${H(t.resetsAt)}`}function an(t,e){return F(t)<=e?t:`${t.replace(ne,"").slice(0,Math.max(1,e-1))}\u2026`}function te(t,e){let n=[],r="";for(let s of t.split(/\s+/).filter(Boolean))r===""?r=s:r.length+1+s.length<=e?r+=` ${s}`:(n.push(r),r=s);return r!==""&&n.push(r),n}var ee={unicode:{tl:"\u256D",tr:"\u256E",bl:"\u2570",br:"\u256F",h:"\u2500",v:"\u2502"},ascii:{tl:"+",tr:"+",bl:"+",br:"+",h:"-",v:"|"}};function ln(t){return Math.max(lt,Math.min(en,t-2))}function cn(t,e,n){return t.map((r,s)=>s===e?n.chosen(` ${r.label} `):n.dim(` ${r.label} `)).join(" ")}function ue({view:t,state:e,config:n,selected:r,caps:s,home:o}){let i=re(s),u=s.unicode?ee.unicode:ee.ascii,a=ln(s.columns),l=a-6,f=sn(s),w=on(t.session,o,i),g=f.length>0?oe:0,d=g>0?2:0,m=[];for(let c=0;c<Math.max(f.length,w.length);c+=1){let U=(f[c]??"").padEnd(g);m.push(`${U}${" ".repeat(d)}${an(w[c]??"",a-g-d)}`.trimEnd())}let P=[],h=(c="")=>P.push(c);h(i.bold(t.headline));let L=ie(e);if(e.pct!==null){let c=Math.max(6,l-F(L)-2);h(`${un(e.pct,c,i,s.unicode)} ${i.dim(L)}`)}h();for(let c of t.body)for(let U of te(c,l))h(i.dim(U));h(),h(cn(t.choices,r,i));let v=t.choices.map(c=>te(c.hint,l)),Q=Math.max(...v.map(c=>c.length)),x=v[r];for(let c=0;c<Q;c+=1)h(i.dim(x[c]??""));h(),h(i.dim(s.unicode?"\u2190/\u2192 choose \xB7 enter confirm \xB7 esc cancel":"left/right choose, enter confirm, esc cancel"));let z=` ${i.brand(i.bold("spare10"))} `,xt=`${n.reserve}% reserve`,Te=Math.max(1,a-6-F(z)-F(xt)),Ne=`${u.tl}${u.h}${z}${u.h.repeat(Te)} ${i.dim(xt)} ${u.h}${u.tr}`,Z=[...m,"",Ne];for(let c of P){let U=Math.max(0,a-4-F(c));Z.push(`${u.v} ${c}${" ".repeat(U)}${u.v}`)}return Z.push(`${u.bl}${u.h.repeat(a-2)}${u.br}`),Z}function ae(t,e){return`
22
+ spare10 \u2014 ${ie(e)}
23
+ ${t.headline}
24
+ ${t.choices[0].label}? [y/N] `}import{readFileSync as dn}from"node:fs";function dt(t){return`'${t.replace(/'/g,"'\\''")}'`}function J(t){let e;try{e=JSON.parse(dn(t,"utf8"))}catch{return{command:null,padding:void 0}}if(typeof e!="object"||e===null)return{command:null,padding:void 0};let n=e.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 le(t){let e=s=>`${dt(t.nodePath)} ${dt(t.selfPath)} ${s} --run ${dt(t.runDir)}`,n=s=>[{matcher:"",hooks:[{type:"command",command:e(s)}]}],r={type:"command",command:e("sensor"),refreshInterval:t.refresh};return t.padding!==void 0&&(r.padding=t.padding),{statusLine:r,hooks:{PreToolUse:n("gate")}}}import{closeSync as ce,openSync as pt,readSync as pn,writeSync as fn}from"node:fs";import{homedir as mn}from"node:os";import{ReadStream as gn,WriteStream as hn,isatty as bn}from"node:tty";function Sn(t){switch(t){case"\x1B[D":case"\x1B[A":case"\x1BOD":case"\x1BOA":return"prev";case"\x1B[C":case"\x1B[B":case"\x1BOC":case"\x1BOB":case" ":return"next";case"\r":case`
25
+ `:return"confirm";case"\x1B":case"":case"":return"cancel"}return/^[yY]$/.test(t)?"yes":/^[nNqQ]$/.test(t)?"no":t==="h"||t==="k"?"prev":t==="l"||t==="j"?"next":null}function wn(t){let e=[];for(let n=0;n<t.length;){let r=t[n+1];if(t[n]==="\x1B"&&(r==="["||r==="O")){let s=n+2;for(;s<t.length&&!/[@-~]/.test(t[s]);)s+=1;e.push(t.slice(n,s+1)),n=s+1;continue}e.push(t[n]),n+=1}return e}function yn(t,e){let n=t.TERM==="dumb"||t.TERM===void 0,r=!t.NO_COLOR&&!n,s=t.LC_ALL??t.LC_CTYPE??t.LANG??"";return{color:r,truecolor:r&&/truecolor|24bit/i.test(t.COLORTERM??""),unicode:!n&&(s===""||/utf-?8/i.test(s)),columns:e}}function $n(){let t;try{t=pt("/dev/tty","r")}catch{return null}let e=null,n=2;if(!process.stderr.isTTY)try{n=pt("/dev/tty","w"),e=new hn(n)}catch{n=2}return{fd:t,write:r=>{try{fn(n,r)}catch{}},columns:(e??process.stderr).columns||80,close:()=>{try{ce(t)}catch{}try{e?.destroy()}catch{}}}}var xn="\x1B[?25l",kn="\x1B[?25h",An=t=>`\r${"\x1B[K\x1B[A".repeat(t-1)}\x1B[K`;async function K({view:t,state:e,config:n,initial:r=1}){let s=$n();if(s===null)return null;let o=yn(process.env,s.columns),i=mn(),u=()=>{let m=Rn(s,ae(t,e));return s.close(),m};if(!bn(s.fd)||o.columns<lt+2)return u();let a;try{a=new gn(s.fd),a.setRawMode(!0)}catch{return u()}let l=r,f=0,w=()=>{let m=ue({view:t,state:e,config:n,selected:l,caps:o,home:i});s.write(`${f>0?An(f):""}${m.join(`
26
+ `)}
27
+ `),f=m.length+1},g=()=>{try{a.setRawMode(!1)}catch{}s.write(kn)};process.once("exit",g),s.write(xn),w();let d=await new Promise(m=>{let P=v=>{a.off("data",L),a.off("end",h),a.off("error",h),m(v)},h=()=>P(null),L=v=>{for(let Q of wn(v.toString("utf8"))){let x=Sn(Q);if(x==="prev"||x==="next"){l=((x==="next"?l+1:l-1)+t.choices.length)%t.choices.length,w();continue}if(x==="yes"||x==="no"){l=x==="yes"?0:1,w(),P(x==="yes"?"y":"n");return}if(x==="confirm"){P(l===0?"y":"n");return}if(x==="cancel"){l=1,w(),P("n");return}}};a.on("data",L),a.once("end",h),a.once("error",h),a.resume()});g(),process.off("exit",g);try{a.destroy()}catch{}return d}function Rn(t,e){let n;try{n=pt("/dev/tty","r")}catch{return null}t.write(e);let r=Buffer.alloc(1),s="";try{for(;pn(n,r,0,1,null)>0;){let o=r.toString("utf8");if(o===`
28
+ `||o==="\r")break;s+=o}}catch{return null}finally{ce(n)}return s.trim()}var Mn=7*24*60*60*1e3,Y=()=>E(process.env.SPARE10_HOME??E(mt(),".spare10"),"runs"),gt=()=>E(mt(),".claude","settings.json");function On(t,e){let n;try{n=fe(t)}catch{return}for(let r of n){let s=E(t,r);try{e-Cn(s).mtimeMs>Mn&&vn(s,{recursive:!0,force:!0})}catch{}}}function Ln(t,e){let n;try{n=fe(t)}catch{return}let r=n.map(o=>E(t,o)).filter(o=>o!==e).map($),s=Gt(r,S());s!==null&&k(e,s)}function Un(t){let e=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=E(t,e);return En(n,{recursive:!0}),n}var ge=t=>/^y(es)?$/i.test(t.trim());function Dn(t,e){return t?e===null?"proceeding":ge(e)?"consented":"declined":"clear"}function he(){return{version:null,model:null,effort:null,cwd:process.cwd(),fastMode:!1}}function jn(t){return{session:he(),headline:"This session would start inside the reserve.",body:[`You have less than the ${t.reserve}% reserve left for this session. If you decide to start anyway, spare10 will stay quiet until the limit resets.`],choices:[{label:"Start anyway",hint:"Runs now on your reserve."},{label:"Do not start",hint:"Back to the shell."}]}}function Fn(t){return{session:t.session,headline:"Reserve reached. The session is paused.",body:["spare10 stopped the agent between tool calls, so nothing is half-written and the whole conversation is saved."],choices:[{label:"Resume",hint:"Continue on the reserve. spare10 stays quiet until the limit resets."},{label:"Stop here",hint:`Back to the shell. You can still resume later with: claude --resume ${t.halted??""}`}]}}var Bn=5,ft=(t,e,n)=>t===1?e:`${t} ${n}`;function Hn(t,e=mt()){let n=t.slice(0,Bn),r=[`spare10 stopped ${ft(t.length,"a background session","background sessions")} at the reserve, between tool calls. Their conversations are kept.`,...n.map(s=>`${s.name??s.backgroundId} \xB7 ${ct(s.cwd??"",e)}`.trim())];return t.length>n.length&&r.push(`\u2026and ${t.length-n.length} more.`),{session:he(),headline:`Reserve reached. ${ft(t.length,"A background session is","background sessions are")} paused.`,body:r,choices:[{label:t.length===1?"Resume":"Resume all",hint:"Back to the background, on your reserve. spare10 stays quiet until the limit resets."},{label:"Leave stopped",hint:"Back to the shell. Each one can still be opened with: claude attach <id>"}]}}async function Vn(t,e){let n=W(t);if(n.length===0)return;let r=B(n,T());if(it(t,n.filter(i=>!r.includes(i)).map(i=>i.sessionId)),r.length===0)return;let s=$(t);if(xe(await K({view:Hn(r),state:s,config:e}))!=="resume"){process.stderr.write(`Left stopped. To pick ${r.length===1?"it":"them"} up later:
29
+ `);for(let i of r)process.stderr.write(` claude attach ${i.backgroundId}
30
+ `);return}k(t,{...s,disarmedUntil:q(s,S())});let o=[];for(let i of r)Mt(i.sessionId,i.cwd)?o.push(i.sessionId):process.stderr.write(`spare10: could not resume ${i.backgroundId}. Try: claude attach ${i.backgroundId}
31
+ `);it(t,o),o.length>0&&process.stderr.write(`Resumed ${ft(o.length,"one session","sessions")} into the reserve for this window.
32
+ `)}async function Gn(t,e){let n=$(t);if(!Lt(n,e,S()))return"clear";let r=await K({view:jn(e),state:n,config:e});return Dn(!0,r)}function ht(){return Nn(import.meta.url)}function Wn(t,e=ht()){let n=E(t,"spare10.js");try{return In(e,n),n}catch{return e}}function qn(t,e=process.env){return!(t.includes("--no-session-persistence")||e.CLAUDE_CODE_CHILD_SESSION&&!e.CLAUDE_CODE_FORCE_SESSION_PERSISTENCE)}var be=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"]),Se=new Set(["--add-dir","--allowedTools","--allowed-tools","--betas","--disallowedTools","--disallowed-tools","--file","--mcp-config","--tools"]),we=new Set(["-d","--debug","--prompt-suggestions","--remote-control"]),ye=new Set(["-r","--resume","-w","--worktree","--from-pr","--teleport","--cloud"]),$e=new Set(["--session-id"]),Jn=new Set(["-c","--continue","--tmux","--bg","--background"]),Kn=new Set(["agents","attach","auth","auto-mode","doctor","gateway","import","install","logs","mcp","plugin","plugins","project","respawn","rm","setup-token","stop","kill","ultrareview","update","upgrade"]);function Yn(t){for(let e=0;e<t.length;e+=1){let n=t[e];if(n==="--")return null;if(!n.startsWith("-"))return Kn.has(n)?n:null;if(n.includes("="))continue;let r=s=>t[s]!==void 0&&!t[s].startsWith("-");if(be.has(n)||$e.has(n))e+=1;else if(we.has(n)||ye.has(n))r(e+1)&&(e+=1);else if(Se.has(n))for(;r(e+1);)e+=1}return null}function de(t){return t.includes("--bg")||t.includes("--background")}var Xn="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 Qn(t,e){let n=[],r=o=>t[o],s=o=>r(o)!==void 0&&!r(o).startsWith("-");for(let o=0;o<t.length;o+=1){let i=r(o);if(i==="--")break;let u=i.startsWith("-")&&i.includes("="),a=u?i.split("=")[0]:i;if(!Jn.has(a)){if($e.has(a)){u||(o+=1);continue}if(ye.has(a)){!u&&s(o+1)&&(o+=1);continue}if(be.has(a)){n.push(i),!u&&r(o+1)!==void 0&&n.push(r(++o));continue}if(we.has(a)){n.push(i),!u&&s(o+1)&&n.push(r(++o));continue}if(Se.has(a)){for(n.push(i);!u&&s(o+1);)n.push(r(++o));continue}if(i.startsWith("-")){n.push(i);continue}}}return["--resume",e,...n,Xn]}function zn(t,e,n,r){return new Promise(s=>{let o=Pn(t,e,{stdio:"inherit"});r&&o.pid!==void 0&&me(j(n),String(o.pid));let i=u=>{try{_n(j(n))}catch{}s(u)};o.once("error",u=>i({status:null,signal:null,error:u})),o.once("exit",(u,a)=>i({status:u,signal:a,error:null}))})}var Zn=t=>t.signal==="SIGTERM"||t.status===143;function pe(t){return t.signal?128+(t.signal==="SIGINT"?2:15):t.status??0}function xe(t){return t===null?"unattended":ge(t)?"resume":"declined"}async function ke({config:t,command:e}){let[n,...r]=e;if(Tn(n)!=="claude")throw new b(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let s=Y();On(s,Date.now());let o=Un(s);Ln(s,o);let i=await Gn(o,{...t,chain:null});if(i==="declined")return process.stderr.write(`Not started.
33
+ `),0;if(i==="consented"){let m=$(o);k(o,{...m,disarmedUntil:q(m,S())}),process.stderr.write(`Continuing into the reserve for this window.
34
+
35
+ `)}let u=J(gt()),a={...t,chain:u.command};me(st(o),JSON.stringify(a,null,2));let l=JSON.stringify(le({nodePath:process.execPath,selfPath:Wn(o),runDir:o,refresh:t.refresh,padding:u.padding})),f=Yn(r)===null&&!de(r),w=f&&qn(r);de(r)?process.stderr.write(`spare10: --bg returns as soon as the session is dispatched, so spare10 will not be here to ask about resuming it. It still stops the session at the reserve; "spare10 doctor" lists what it stopped.
36
+ `):f&&!w&&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.
37
+ `);let g=()=>{};process.on("SIGINT",g),process.on("SIGTERM",g);let d=await tr({executable:n,rest:r,settings:l,runDir:o,runConfig:a,canResume:w});return await Vn(o,a),d}async function tr(t){let{executable:e,rest:n,settings:r,runDir:s,runConfig:o,canResume:i}=t,u=n;for(;;){let a=await zn(e,["--settings",r,...u],s,i);if(a.error)return process.stderr.write(`spare10: could not start ${e}: ${a.error.message}
38
+ `),127;let l=$(s);if(!Zn(a)||l.halted===null)return pe(a);let f=xe(await K({view:Fn(l),state:l,config:o}));if(f!=="resume")return process.stderr.write(`Not resumed. To pick it up later: claude --resume ${l.halted}
39
+ `),f==="declined"?0:pe(a);k(s,{...l,halted:null,disarmedUntil:q(l,S())}),process.stderr.write(`Resuming into the reserve for this window.
40
+
41
+ `),u=Qn(n,l.halted)}}var or="0.5.0",p={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function Re(t){try{return rr(t).map(e=>sr(t,e)).filter(e=>{try{return bt(e).isDirectory()}catch{return!1}}).sort((e,n)=>bt(n).mtimeMs-bt(e).mtimeMs)}catch{return[]}}var ir=t=>Re(t)[0]??null;function ur(t){try{return Ae("command",["-v",t],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function ar(){try{return Ae("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function lr(t){try{return er(t,nr.X_OK),!0}catch{return!1}}function Pe(){let t=(d="")=>process.stdout.write(`${d}
42
+ `),e=S(),n=0;t(`spare10 ${or}`),t(),t("Environment"),t(` ${p.info} node ${process.execPath} (${process.version})`),t(` ${p.info} spare10 ${ht()}`);let r=ur("claude"),s=r?ar():null;r&&s?t(` ${p.ok} claude ${r} (${s})`):(n+=1,t(` ${p.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),t(),t("Status line");let o=J(gt());if(o.command===null)t(` ${p.info} nothing to chain \u2014 spare10 will own the status line`);else{let d=o.command.split(/\s+/)[0]??"",m=lr(d);m||(n+=1),t(` ${m?p.ok:p.warn} chains into ${o.command}`),m||t(" that path is not executable; its output will be dropped")}t();let i=ir(Y());if(i===null)return t("No runs yet. Start one with: spare10 claude"),n>0?1:0;let u=M(i),a=$(i),l=Qt(a,u,e);if(t(`Latest run ${i}`),t(` ${p.info} reserve ${u.reserve}% of the 5-hour window`),t(` ${p.info} on trip ${u.pausePrompt===null?"stop the session and ask in the terminal":`inject: ${JSON.stringify(u.pausePrompt)}`}`),t(` ${p.info} refresh ${u.refresh}s`),a.pct!==null&&a.updatedAt!==null){t(` ${p.ok} quota ${a.pct}% used, ${100-a.pct}% left`);let d=e-a.updatedAt;t(` ${p.info} last reading ${ut(d)} ago`+(d>u.refresh*3?" (no session running; still valid for this window)":""))}a.resetsAt!==null&&t(` ${p.info} resets ${at(a.resetsAt)} (in ${ut(a.resetsAt-e)})`);let f=l.status==="blind";f&&(n+=1);let w=f?p.warn:l.status==="no-data"?p.info:p.ok;t(` ${w} state ${l.status.toUpperCase()} \u2014 ${l.detail}`);let g=B(Re(Y()).flatMap(W),T());if(g.length>0){t(),t(`Stopped in the background, waiting to be resumed (${g.length})`);for(let d of g)t(` ${p.info} ${d.name??d.backgroundId}`),t(` claude attach ${d.backgroundId}`)}return n>0?1:0}import{spawnSync as dr}from"node:child_process";import{readFileSync as pr}from"node:fs";function Ie(t){return typeof t=="number"&&Number.isFinite(t)?t:null}var wt=t=>typeof t=="string"&&t?t:null,St=(t,e,n)=>{let r=t[e];return typeof r=="object"&&r!==null?wt(r[n]):null};function cr(t){let e={version:wt(t.version),model:St(t,"model","display_name"),effort:St(t,"effort","level"),cwd:St(t,"workspace","current_dir")??wt(t.cwd),fastMode:t.fast_mode===!0};return e.version!==null||e.model!==null||e.cwd!==null||e.fastMode?e:null}function Ee(t){let e={sessionId:null,fiveHour:null,session:null},n;try{n=JSON.parse(t)}catch{return e}if(typeof n!="object"||n===null)return e;let r=n,s=typeof r.session_id=="string"?r.session_id:null,o=cr(r),i=r.rate_limits;if(typeof i!="object"||i===null)return{sessionId:s,fiveHour:null,session:o};let u=i.five_hour;if(typeof u!="object"||u===null)return{sessionId:s,fiveHour:null,session:o};let a=u,l=Ie(a.used_percentage);return l===null?{sessionId:s,fiveHour:null,session:o}:{sessionId:s,session:o,fiveHour:{usedPercentage:Math.min(100,Math.max(0,l)),resetsAt:Ie(a.resets_at)}}}function fr(t,e,n){let r=e.fiveHour,s=t.tick+1,o=e.session??t.session;if(!r){let u=t.missingStreak+1;return{...t,tick:s,session:o,missingStreak:u,blind:u>=Rt}}let i=r.resetsAt!==null&&t.resetsAt!==null&&r.resetsAt!==t.resetsAt;return{tick:s,session:o,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:i?null:t.disarmedUntil,pausePromptInjectedTo:i?[]:t.pausePromptInjectedTo,halted:t.halted}}var mr="\x1B[38;5;208m",gr="\x1B[38;5;40m",hr="\x1B[38;5;245m",$t="\x1B[39m",yt=t=>`${mr}${t}${$t}`,br=t=>`${gr}${t}${$t}`,Sr=t=>`${hr}${t}${$t}`,X=t=>t.reserve===et?"spare10":`spare10 (${t.reserve}%)`;function wr(t,e,n){if(!e.badge)return"";if(t.blind)return"\u26A0 spare10 quota unavailable";if(t.pct===null)return Sr(`\u29D7 ${X(e)}`);if(t.pct<C(e))return br(`\u25CF ${X(e)}`);if(t.disarmedUntil!==null&&n<t.disarmedUntil)return yt(`\u2A2F ${X(e)}`);if(t.pausePromptInjectedTo.length>0)return yt(`\u23F8 ${X(e)}`);let s=t.tick%2===0?"\u26A0":" ";return yt(`${s} Pausing at next tool call`)}function yr(t,e){if(!t||t.includes("spare10 sensor"))return"";try{return(dr(t,{shell:!0,input:e,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function $r(){try{return pr(0,"utf8")}catch{return""}}function ve(t){let e=$r(),n=M(t),r=fr($(t),Ee(e),S());k(t,r);let s=[wr(r,n,S()),yr(n.chain,e)].filter(Boolean);return s.length>0&&process.stdout.write(s.join(" ")),0}function Ce(t,e){let n=t.indexOf(e);return n===-1?null:t[n+1]??null}function xr(t){let[e,...n]=t;switch(e){case"sensor":{let r=Ce(n,"--run");return r?ve(r):0}case"gate":{let r=Ce(n,"--run");return r?Xt(r):0}case"doctor":return Pe();default:return ke(Et(t))}}function _e(t){throw t instanceof b&&(t.message&&(process.stderr.write(`spare10: ${t.message}
43
+
44
+ ${nt}
45
+ `),process.exit(2)),process.stdout.write(`${nt}
46
+ `),process.exit(0)),t}try{Promise.resolve(xr(process.argv.slice(2))).then(t=>process.exit(t),_e)}catch(t){_e(t)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spare10",
3
- "version": "0.3.1",
3
+ "version": "0.5.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",
@@ -23,6 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "build": "node scripts/build.mjs",
26
+ "demo": "node scripts/demo.mjs",
26
27
  "test": "vitest run",
27
28
  "test:watch": "vitest",
28
29
  "typecheck": "tsc --noEmit",