spare10 0.3.0 → 0.4.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 +24 -10
  2. package/dist/spare10.js +24 -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.
22
23
 
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.
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.
27
+
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
 
@@ -180,11 +187,18 @@ Not in this version, deliberately:
180
187
 
181
188
  ```bash
182
189
  npm install
183
- npm test # 151 tests: unit, plus the hooks and CLI as real subprocesses
190
+ npm test # 213 tests: unit, plus the hooks and CLI as real subprocesses
184
191
  npm run typecheck
185
192
  npm run build # single dependency-free bundle in dist/
193
+ npm run demo # the pause prompt at 91% usage, without burning a session
186
194
  ```
187
195
 
196
+ `npm run demo` draws the real panel from `src/`, so it cannot drift from what spare10 shows.
197
+ It is interactive by default; `-- --static` prints every state at once (both selections, the
198
+ no-colour fallback and the plain line prompt), `-- --preflight` shows the question asked
199
+ before launch rather than the one after a stop, and `-- --width 64` forces a narrower
200
+ terminal.
201
+
188
202
  Test fixtures are real payloads captured from a live Claude Code session, sanitized. When
189
203
  Anthropic changes the status line schema, the tolerant parser keeps spare10 failing open and
190
204
  `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},_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
2
+ var fe={version:null,model:null,effort:null,cwd:null,fastMode:!1},W={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjectedTo:[],halted:null,tick:0,session:null},it="main";function me(e){return e??it}var q=10,ut=2,w={reserve:q,pausePrompt:null,refresh:ut,badge:!0,chain:null},ge=3,he=3,be=3600;function v(e){return 100-e.reserve}function C(e){return 100-e}var h=class extends Error{},J=`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,26 @@ 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 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)}
19
+ spare10 --pause-prompt "Finish this block, commit, then stop." claude --resume`;function at(e){if(!/^\d+$/.test(e))throw new h(`--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 h(`--reserve must be between 1 and 99, got ${t}.`);return t}function lt(e){if(!/^\d+$/.test(e)||Number(e)<1)throw new h(`--refresh must be a whole number of seconds >= 1, got "${e}".`);return Number(e)}function Se(e){let t={reserve:w.reserve,pausePrompt:w.pausePrompt,refresh:w.refresh,badge:w.badge},n=0,r=o=>{let i=e[n+1];if(i===void 0)throw new h(`${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=at(r(o));break;case"--threshold":throw new h('--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=lt(r(o));break;case"--no-badge":t.badge=!1;break;case"-h":case"--help":throw new h("");default:throw new h(`Unknown option "${o}".`)}}let s=e.slice(n);if(s.length===0)throw new h("No command given. Try: spare10 claude");return{config:t,command:s}}import{spawnSync as wt}from"node:child_process";import{readFileSync as ve}from"node:fs";var I={kind:"pass"};function K(e,t,n){return e.resetsAt!==null?n<e.resetsAt:e.updatedAt===null?!1:n-e.updatedAt<=t.refresh*he}function D(e){return e===null?"an unknown time":new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function we(e,t){return`into your ${t.reserve}% reserve \xB7 ${C(e.pct??0)}% of quota left \xB7 resets ${D(e.resetsAt)}`}function ct(e,t){return`spare10 \u2014 ${we(e,t)}`}function dt(e,t){return`spare10 budget guard. You have reached the safe usage limit for this session (${we(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 ye(e,t,n){return U({state:e,config:t,now:n,agent:null}).kind!=="pass"}function U({state:e,config:t,now:n,agent:r}){return e.pct===null||e.updatedAt===null||e.blind||!K(e,t,n)||e.disarmedUntil!==null&&n<e.disarmedUntil||e.pct<v(t)?I:t.pausePrompt!==null?r!==null&&e.pausePromptInjectedTo.includes(r)?I:{kind:"inject",text:dt(e,t)}:{kind:"halt",reason:`${ct(e,t)}. Stop now and wait for the user. Do not call any further tools.`}}var $e={sessionId:null,toolName:null,agentId:null};function xe(e){try{let t=JSON.parse(e);if(typeof t!="object"||t===null)return $e;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 $e}}import{readFileSync as pt,writeFileSync as ft,renameSync as mt,mkdirSync as gt,unlinkSync as ht}from"node:fs";import{join as Y}from"node:path";var Pe=e=>Y(e,"state.json"),X=e=>Y(e,"config.json"),O=e=>Y(e,"claude.pid");function y(){return Math.floor(Date.now()/1e3)}function Re(e){try{let t=JSON.parse(pt(e,"utf8"));return typeof t=="object"&&t!==null?t:null}catch{return null}}var A=(e,t)=>typeof e=="number"&&Number.isFinite(e)?e:t,Ae=(e,t)=>typeof e=="boolean"?e:t,bt=e=>Array.isArray(e)?e.filter(t=>typeof t=="string"):[],F=e=>typeof e=="string"&&e?e:null;function St(e){if(typeof e!="object"||e===null)return null;let t=e;return{...fe,version:F(t.version),model:F(t.model),effort:F(t.effort),cwd:F(t.cwd),fastMode:t.fastMode===!0}}function x(e){let t=Re(Pe(e));return t?{pct:A(t.pct,null),resetsAt:A(t.resetsAt,null),updatedAt:A(t.updatedAt,null),missingStreak:A(t.missingStreak,0)??0,blind:Ae(t.blind,!1),disarmedUntil:A(t.disarmedUntil,null),pausePromptInjectedTo:bt(t.pausePromptInjectedTo),halted:typeof t.halted=="string"&&t.halted?t.halted:null,tick:A(t.tick,0)??0,session:St(t.session)}:{...W}}function R(e,t){let n=Pe(e),r=`${n}.${process.pid}.tmp`;try{gt(e,{recursive:!0}),ft(r,JSON.stringify(t),"utf8"),mt(r,n)}catch{try{ht(r)}catch{}}}function _(e){let t=Re(X(e));if(!t)return{...w};let n=A(t.reserve,w.reserve)??w.reserve,r=A(t.refresh,w.refresh)??w.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:Ae(t.badge,w.badge),chain:typeof t.chain=="string"&&t.chain?t.chain:null}}function Q(e,t){return e.resetsAt??t+be}function ke(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:{...W,pct:r.pct,resetsAt:r.resetsAt,updatedAt:r.updatedAt}}function Ee(){try{return ve(0,"utf8")}catch{return""}}function Ce(e){process.stdout.write(JSON.stringify({hookSpecificOutput:e}))}var yt=2e3;function $t(e){try{let t=Number(ve(O(e),"utf8").trim());return Number.isInteger(t)&&t>1?t:null}catch{return null}}function xt(e){let t=wt("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 Pt(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=xt(n)}return!1}function Rt(e){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e)}function At(e,t,n,r){let s=n===null?null:$t(e);if(s!==null&&Pt(s)){R(e,{...t,halted:n});try{process.kill(s,"SIGTERM"),Rt(yt)}catch{}}return Ce({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:r}),0}function Ie(e){let t=_(e),n=x(e),r=y();if(U({state:n,config:t,now:r,agent:null}).kind==="pass")return Ee(),0;let{sessionId:s,agentId:o}=xe(Ee()),i=me(o),u=U({state:n,config:t,now:r,agent:i});switch(u.kind){case"pass":return 0;case"inject":return R(e,{...n,pausePromptInjectedTo:[...n.pausePromptInjectedTo,i]}),Ce({hookEventName:"PreToolUse",additionalContext:u.text}),0;case"halt":return At(e,n,s,u.reason)}}import{execFileSync as Qe}from"node:child_process";import{accessSync as vn,constants as Cn,readdirSync as In,statSync as ue}from"node:fs";import{join as _n}from"node:path";function _e(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(!K(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 ${Z(e.disarmedUntil)}`};if(e.pct>=v(t)){let r=e.pausePromptInjectedTo.length,s=r>0?`; pause prompt delivered to ${r} agent${r===1?"":"s"}`:"";return{status:"tripped",detail:`into the ${t.reserve}% reserve${s}`}}return{status:"armed",detail:`${C(e.pct)}% left, of which ${t.reserve}% is reserved`}}function z(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 Z(e){return new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawn as Qt}from"node:child_process";import{copyFileSync as zt,mkdirSync as Zt,readdirSync as qe,rmSync as en,statSync as tn,unlinkSync as nn,writeFileSync as Je}from"node:fs";import{homedir as Ke}from"node:os";import{basename as rn,join as k}from"node:path";import{fileURLToPath as sn}from"node:url";import{readFileSync as kt}from"node:fs";function ee(e){return`'${e.replace(/'/g,"'\\''")}'`}function j(e){let t;try{t=JSON.parse(kt(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 Te(e){let t=s=>`${ee(e.nodePath)} ${ee(e.selfPath)} ${s} --run ${ee(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")}}}import{closeSync as Ge,openSync as ne,readSync as Dt,writeSync as Ut}from"node:fs";import{homedir as Ft}from"node:os";import{ReadStream as jt,WriteStream as Ht,isatty as Vt}from"node:tty";var te=46,Et=78,De=/\u001b\[[0-9;]*m/g,L=e=>e.replace(De,"").length,T=[217,119,87],Ne=173;var vt=236,Ct=245;function Ue(e){if(!e.color){let r=s=>s;return{brand:r,dim:r,bold:r,chosen:s=>`[${s}]`}}let t=e.truecolor?`\x1B[38;2;${T[0]};${T[1]};${T[2]}m`:`\x1B[38;5;${Ne}m`,n=e.truecolor?`\x1B[48;2;${T[0]};${T[1]};${T[2]}m`:`\x1B[48;5;${Ne}m`;return{brand:r=>`${t}${r}\x1B[39m`,dim:r=>`\x1B[38;5;${Ct}m${r}\x1B[39m`,bold:r=>`\x1B[1m${r}\x1B[22m`,chosen:r=>`${n}\x1B[38;5;${vt}m\x1B[1m${r}\x1B[0m`}}var Fe=[" \u2590\u259B\u2588\u2588\u2588\u259B\u2588","\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2580"," \u259D\u259D \u259D\u259D"],je=Math.max(...Fe.map(e=>e.length));function It(e){if(!e.unicode||!e.color)return[];let t=Ue(e);return Fe.map(n=>t.brand(n.padEnd(je)))}function _t(e,t){return t&&(e===t||e.startsWith(`${t}/`))?`~${e.slice(t.length)}`:e}function Tt(e,t,n){let r=n.bold("Claude Code"),s=e?.version?` ${n.dim(`v${e.version}`)}`:"",o=[`${r}${s}`],i=e?.model??null;if(i!==null){let u=e?.effort?` with ${e.effort} effort`:"",a=e?.fastMode?" \xB7 fast mode":"";o.push(n.dim(`${i}${u}${a}`))}return e?.cwd&&o.push(n.dim(_t(e.cwd,t))),o}var Me={unicode:{filled:"\u2588",empty:"\u2592"},ascii:{filled:"#",empty:"-"}};function Nt(e,t,n,r){let s=r?Me.unicode:Me.ascii,o=Math.max(4,t),i=Math.min(o,Math.max(0,Math.round(e/100*o)));return`${n.brand(s.filled.repeat(i))}${n.dim(s.empty.repeat(o-i))}`}function He(e){return e.pct===null?`quota unknown \xB7 resets ${D(e.resetsAt)}`:`${e.pct}% used \xB7 ${C(e.pct)}% left \xB7 resets ${D(e.resetsAt)}`}function Mt(e,t){return L(e)<=t?e:`${e.replace(De,"").slice(0,Math.max(1,t-1))}\u2026`}function Oe(e,t){let n=[],r="";for(let s of e.split(/\s+/).filter(Boolean))r===""?r=s:r.length+1+s.length<=t?r+=` ${s}`:(n.push(r),r=s);return r!==""&&n.push(r),n}var Le={unicode:{tl:"\u256D",tr:"\u256E",bl:"\u2570",br:"\u256F",h:"\u2500",v:"\u2502"},ascii:{tl:"+",tr:"+",bl:"+",br:"+",h:"-",v:"|"}};function Ot(e){return Math.max(te,Math.min(Et,e-2))}function Lt(e,t,n){return e.map((r,s)=>s===t?n.chosen(` ${r.label} `):n.dim(` ${r.label} `)).join(" ")}function Ve({view:e,state:t,config:n,selected:r,caps:s,home:o}){let i=Ue(s),u=s.unicode?Le.unicode:Le.ascii,a=Ot(s.columns),l=a-6,b=It(s),S=Tt(e.session,o,i),d=b.length>0?je:0,p=d>0?2:0,f=[];for(let c=0;c<Math.max(b.length,S.length);c+=1){let M=(b[c]??"").padEnd(d);f.push(`${M}${" ".repeat(p)}${Mt(S[c]??"",a-d-p)}`.trimEnd())}let P=[],g=(c="")=>P.push(c);g(i.bold(e.headline));let N=He(t);if(t.pct!==null){let c=Math.max(6,l-L(N)-2);g(`${Nt(t.pct,c,i,s.unicode)} ${i.dim(N)}`)}g();for(let c of e.body)for(let M of Oe(c,l))g(i.dim(M));g(),g(Lt(e.choices,r,i));let E=e.choices.map(c=>Oe(c.hint,l)),V=Math.max(...E.map(c=>c.length)),$=E[r];for(let c=0;c<V;c+=1)g(i.dim($[c]??""));g(),g(i.dim(s.unicode?"\u2190/\u2192 choose \xB7 enter confirm \xB7 esc cancel":"left/right choose, enter confirm, esc cancel"));let B=` ${i.brand(i.bold("spare10"))} `,pe=`${n.reserve}% reserve`,st=Math.max(1,a-6-L(B)-L(pe)),ot=`${u.tl}${u.h}${B}${u.h.repeat(st)} ${i.dim(pe)} ${u.h}${u.tr}`,G=[...f,"",ot];for(let c of P){let M=Math.max(0,a-4-L(c));G.push(`${u.v} ${c}${" ".repeat(M)}${u.v}`)}return G.push(`${u.bl}${u.h.repeat(a-2)}${u.br}`),G}function Be(e,t){return`
22
+ spare10 \u2014 ${He(t)}
23
+ ${e.headline}
24
+ ${e.choices[0].label}? [y/N] `}function Bt(e){switch(e){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(e)?"yes":/^[nNqQ]$/.test(e)?"no":e==="h"||e==="k"?"prev":e==="l"||e==="j"?"next":null}function Gt(e){let t=[];for(let n=0;n<e.length;){let r=e[n+1];if(e[n]==="\x1B"&&(r==="["||r==="O")){let s=n+2;for(;s<e.length&&!/[@-~]/.test(e[s]);)s+=1;t.push(e.slice(n,s+1)),n=s+1;continue}t.push(e[n]),n+=1}return t}function Wt(e,t){let n=e.TERM==="dumb"||e.TERM===void 0,r=!e.NO_COLOR&&!n,s=e.LC_ALL??e.LC_CTYPE??e.LANG??"";return{color:r,truecolor:r&&/truecolor|24bit/i.test(e.COLORTERM??""),unicode:!n&&(s===""||/utf-?8/i.test(s)),columns:t}}function qt(){let e;try{e=ne("/dev/tty","r")}catch{return null}let t=null,n=2;if(!process.stderr.isTTY)try{n=ne("/dev/tty","w"),t=new Ht(n)}catch{n=2}return{fd:e,write:r=>{try{Ut(n,r)}catch{}},columns:(t??process.stderr).columns||80,close:()=>{try{Ge(e)}catch{}try{t?.destroy()}catch{}}}}var Jt="\x1B[?25l",Kt="\x1B[?25h",Yt=e=>`\r${"\x1B[K\x1B[A".repeat(e-1)}\x1B[K`;async function re({view:e,state:t,config:n,initial:r=1}){let s=qt();if(s===null)return null;let o=Wt(process.env,s.columns),i=Ft(),u=()=>{let f=Xt(s,Be(e,t));return s.close(),f};if(!Vt(s.fd)||o.columns<te+2)return u();let a;try{a=new jt(s.fd),a.setRawMode(!0)}catch{return u()}let l=r,b=0,S=()=>{let f=Ve({view:e,state:t,config:n,selected:l,caps:o,home:i});s.write(`${b>0?Yt(b):""}${f.join(`
26
+ `)}
27
+ `),b=f.length+1},d=()=>{try{a.setRawMode(!1)}catch{}s.write(Kt)};process.once("exit",d),s.write(Jt),S();let p=await new Promise(f=>{let P=E=>{a.off("data",N),a.off("end",g),a.off("error",g),f(E)},g=()=>P(null),N=E=>{for(let V of Gt(E.toString("utf8"))){let $=Bt(V);if($==="prev"||$==="next"){l=(($==="next"?l+1:l-1)+e.choices.length)%e.choices.length,S();continue}if($==="yes"||$==="no"){l=$==="yes"?0:1,S(),P($==="yes"?"y":"n");return}if($==="confirm"){P(l===0?"y":"n");return}if($==="cancel"){l=1,S(),P("n");return}}};a.on("data",N),a.once("end",g),a.once("error",g),a.resume()});d(),process.off("exit",d);try{a.destroy()}catch{}return p}function Xt(e,t){let n;try{n=ne("/dev/tty","r")}catch{return null}e.write(t);let r=Buffer.alloc(1),s="";try{for(;Dt(n,r,0,1,null)>0;){let o=r.toString("utf8");if(o===`
28
+ `||o==="\r")break;s+=o}}catch{return null}finally{Ge(n)}return s.trim()}var on=7*24*60*60*1e3,se=()=>k(process.env.SPARE10_HOME??k(Ke(),".spare10"),"runs"),oe=()=>k(Ke(),".claude","settings.json");function un(e,t){let n;try{n=qe(e)}catch{return}for(let r of n){let s=k(e,r);try{t-tn(s).mtimeMs>on&&en(s,{recursive:!0,force:!0})}catch{}}}function an(e,t){let n;try{n=qe(e)}catch{return}let r=n.map(o=>k(e,o)).filter(o=>o!==t).map(x),s=ke(r,y());s!==null&&R(t,s)}function ln(e){let t=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=k(e,t);return Zt(n,{recursive:!0}),n}var Ye=e=>/^y(es)?$/i.test(e.trim());function cn(e,t){return e?t===null?"proceeding":Ye(t)?"consented":"declined":"clear"}function dn(){return{version:null,model:null,effort:null,cwd:process.cwd(),fastMode:!1}}function pn(e){return{session:dn(),headline:"This session would start inside the reserve.",body:[`You have less than the ${e.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(e){return{session:e.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 ${e.halted??""}`}]}}async function mn(e,t){let n=x(e);if(!ye(n,t,y()))return"clear";let r=await re({view:pn(t),state:n,config:t});return cn(!0,r)}function ie(){return sn(import.meta.url)}function gn(e,t=ie()){let n=k(e,"spare10.js");try{return zt(t,n),n}catch{return t}}function hn(e,t=process.env){return!(e.includes("--no-session-persistence")||t.CLAUDE_CODE_CHILD_SESSION&&!t.CLAUDE_CODE_FORCE_SESSION_PERSISTENCE)}var bn=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"]),Sn=new Set(["--add-dir","--allowedTools","--allowed-tools","--betas","--disallowedTools","--disallowed-tools","--file","--mcp-config","--tools"]),wn=new Set(["-d","--debug","--prompt-suggestions","--remote-control"]),yn=new Set(["-r","--resume","-w","--worktree","--from-pr","--teleport","--cloud"]),$n=new Set(["--session-id"]),xn=new Set(["-c","--continue","--tmux","--bg","--background"]),Pn="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 Rn(e,t){let n=[],r=o=>e[o],s=o=>r(o)!==void 0&&!r(o).startsWith("-");for(let o=0;o<e.length;o+=1){let i=r(o);if(i==="--")break;let u=i.startsWith("-")&&i.includes("="),a=u?i.split("=")[0]:i;if(!xn.has(a)){if($n.has(a)){u||(o+=1);continue}if(yn.has(a)){!u&&s(o+1)&&(o+=1);continue}if(bn.has(a)){n.push(i),!u&&r(o+1)!==void 0&&n.push(r(++o));continue}if(wn.has(a)){n.push(i),!u&&s(o+1)&&n.push(r(++o));continue}if(Sn.has(a)){for(n.push(i);!u&&s(o+1);)n.push(r(++o));continue}if(i.startsWith("-")){n.push(i);continue}}}return["--resume",t,...n,Pn]}function An(e,t,n,r){return new Promise(s=>{let o=Qt(e,t,{stdio:"inherit"});r&&o.pid!==void 0&&Je(O(n),String(o.pid));let i=u=>{try{nn(O(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 kn=e=>e.signal==="SIGTERM"||e.status===143;function We(e){return e.signal?128+(e.signal==="SIGINT"?2:15):e.status??0}function En(e){return e===null?"unattended":Ye(e)?"resume":"declined"}async function Xe({config:e,command:t}){let[n,...r]=t;if(rn(n)!=="claude")throw new h(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let s=se();un(s,Date.now());let o=ln(s);an(s,o);let i=await mn(o,{...e,chain:null});if(i==="declined")return process.stderr.write(`Not started.
29
+ `),0;if(i==="consented"){let p=x(o);R(o,{...p,disarmedUntil:Q(p,y())}),process.stderr.write(`Continuing into the reserve for this window.
30
+
31
+ `)}let u=j(oe()),a={...e,chain:u.command};Je(X(o),JSON.stringify(a,null,2));let l=JSON.stringify(Te({nodePath:process.execPath,selfPath:gn(o),runDir:o,refresh:e.refresh,padding:u.padding})),b=hn(r);b||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.
32
+ `);let S=()=>{};process.on("SIGINT",S),process.on("SIGTERM",S);let d=r;for(;;){let p=await An(n,["--settings",l,...d],o,b);if(p.error)return process.stderr.write(`spare10: could not start ${n}: ${p.error.message}
33
+ `),127;let f=x(o);if(!kn(p)||f.halted===null)return We(p);let P=En(await re({view:fn(f),state:f,config:a}));if(P!=="resume")return process.stderr.write(`Not resumed. To pick it up later: claude --resume ${f.halted}
34
+ `),P==="declined"?0:We(p);R(o,{...f,halted:null,disarmedUntil:Q(f,y())}),process.stderr.write(`Resuming into the reserve for this window.
35
+
36
+ `),d=Rn(r,f.halted)}}var Tn="0.4.0",m={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function Nn(e){try{return In(e).map(n=>_n(e,n)).filter(n=>{try{return ue(n).isDirectory()}catch{return!1}}).sort((n,r)=>ue(r).mtimeMs-ue(n).mtimeMs)[0]??null}catch{return null}}function Mn(e){try{return Qe("command",["-v",e],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function On(){try{return Qe("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function Ln(e){try{return vn(e,Cn.X_OK),!0}catch{return!1}}function ze(){let e=(d="")=>process.stdout.write(`${d}
37
+ `),t=y(),n=0;e(`spare10 ${Tn}`),e(),e("Environment"),e(` ${m.info} node ${process.execPath} (${process.version})`),e(` ${m.info} spare10 ${ie()}`);let r=Mn("claude"),s=r?On():null;r&&s?e(` ${m.ok} claude ${r} (${s})`):(n+=1,e(` ${m.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),e(),e("Status line");let o=j(oe());if(o.command===null)e(` ${m.info} nothing to chain \u2014 spare10 will own the status line`);else{let d=o.command.split(/\s+/)[0]??"",p=Ln(d);p||(n+=1),e(` ${p?m.ok:m.warn} chains into ${o.command}`),p||e(" that path is not executable; its output will be dropped")}e();let i=Nn(se());if(i===null)return e("No runs yet. Start one with: spare10 claude"),n>0?1:0;let u=_(i),a=x(i),l=_e(a,u,t);if(e(`Latest run ${i}`),e(` ${m.info} reserve ${u.reserve}% of the 5-hour window`),e(` ${m.info} on trip ${u.pausePrompt===null?"stop the session and ask in the terminal":`inject: ${JSON.stringify(u.pausePrompt)}`}`),e(` ${m.info} refresh ${u.refresh}s`),a.pct!==null&&a.updatedAt!==null){e(` ${m.ok} quota ${a.pct}% used, ${100-a.pct}% left`);let d=t-a.updatedAt;e(` ${m.info} last reading ${z(d)} ago`+(d>u.refresh*3?" (no session running; still valid for this window)":""))}a.resetsAt!==null&&e(` ${m.info} resets ${Z(a.resetsAt)} (in ${z(a.resetsAt-t)})`);let b=l.status==="blind";b&&(n+=1);let S=b?m.warn:l.status==="no-data"?m.info:m.ok;return e(` ${S} state ${l.status.toUpperCase()} \u2014 ${l.detail}`),n>0?1:0}import{spawnSync as Un}from"node:child_process";import{readFileSync as Fn}from"node:fs";function Ze(e){return typeof e=="number"&&Number.isFinite(e)?e:null}var le=e=>typeof e=="string"&&e?e:null,ae=(e,t,n)=>{let r=e[t];return typeof r=="object"&&r!==null?le(r[n]):null};function Dn(e){let t={version:le(e.version),model:ae(e,"model","display_name"),effort:ae(e,"effort","level"),cwd:ae(e,"workspace","current_dir")??le(e.cwd),fastMode:e.fast_mode===!0};return t.version!==null||t.model!==null||t.cwd!==null||t.fastMode?t:null}function et(e){let t={sessionId:null,fiveHour:null,session:null},n;try{n=JSON.parse(e)}catch{return t}if(typeof n!="object"||n===null)return t;let r=n,s=typeof r.session_id=="string"?r.session_id:null,o=Dn(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=Ze(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:Ze(a.resets_at)}}}function jn(e,t,n){let r=t.fiveHour,s=e.tick+1,o=t.session??e.session;if(!r){let u=e.missingStreak+1;return{...e,tick:s,session:o,missingStreak:u,blind:u>=ge}}let i=r.resetsAt!==null&&e.resetsAt!==null&&r.resetsAt!==e.resetsAt;return{tick:s,session:o,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:i?null:e.disarmedUntil,pausePromptInjectedTo:i?[]:e.pausePromptInjectedTo,halted:e.halted}}var Hn="\x1B[38;5;208m",Vn="\x1B[38;5;40m",Bn="\x1B[38;5;245m",de="\x1B[39m",ce=e=>`${Hn}${e}${de}`,Gn=e=>`${Vn}${e}${de}`,Wn=e=>`${Bn}${e}${de}`,H=e=>e.reserve===q?"spare10":`spare10 (${e.reserve}%)`;function qn(e,t,n){if(!t.badge)return"";if(e.blind)return"\u26A0 spare10 quota unavailable";if(e.pct===null)return Wn(`\u29D7 ${H(t)}`);if(e.pct<v(t))return Gn(`\u25CF ${H(t)}`);if(e.disarmedUntil!==null&&n<e.disarmedUntil)return ce(`\u2A2F ${H(t)}`);if(e.pausePromptInjectedTo.length>0)return ce(`\u23F8 ${H(t)}`);let s=e.tick%2===0?"\u26A0":" ";return ce(`${s} Pausing at next tool call`)}function Jn(e,t){if(!e||e.includes("spare10 sensor"))return"";try{return(Un(e,{shell:!0,input:t,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function Kn(){try{return Fn(0,"utf8")}catch{return""}}function tt(e){let t=Kn(),n=_(e),r=jn(x(e),et(t),y());R(e,r);let s=[qn(r,n,y()),Jn(n.chain,t)].filter(Boolean);return s.length>0&&process.stdout.write(s.join(" ")),0}function nt(e,t){let n=e.indexOf(t);return n===-1?null:e[n+1]??null}function Yn(e){let[t,...n]=e;switch(t){case"sensor":{let r=nt(n,"--run");return r?tt(r):0}case"gate":{let r=nt(n,"--run");return r?Ie(r):0}case"doctor":return ze();default:return Xe(Se(e))}}function rt(e){throw e instanceof h&&(e.message&&(process.stderr.write(`spare10: ${e.message}
38
+
39
+ ${J}
40
+ `),process.exit(2)),process.stdout.write(`${J}
41
+ `),process.exit(0)),e}try{Promise.resolve(Yn(process.argv.slice(2))).then(e=>process.exit(e),rt)}catch(e){rt(e)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spare10",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",