spare10 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,15 @@ spare10 --pause-prompt "Finish this block, commit, then stop." claude
41
41
  ```
42
42
 
43
43
  That text is injected into the running agent **without blocking it**, so the agent can
44
- actually carry out the wind-down you asked for.
44
+ actually carry out the wind-down you asked for. It arrives with the situation attached, since
45
+ an instruction turning up mid-turn otherwise has no context:
46
+
47
+ ```
48
+ spare10 budget guard. You have reached the safe usage limit for this session
49
+ (into your 10% reserve · 9% of quota left · resets 14:00). Wrap up your work and stop.
50
+
51
+ User instructions: Finish this block, commit, then stop.
52
+ ```
45
53
 
46
54
  ## Install
47
55
 
@@ -51,6 +59,7 @@ actually carry out the wind-down you asked for.
51
59
 
52
60
  ```bash
53
61
  npm install -g spare10 # or, with no install at all: npx spare10 claude
62
+ brew install alesdi/tap/spare10
54
63
  curl -fsSL https://raw.githubusercontent.com/alesdi/spare10/main/install.sh | sh
55
64
  ```
56
65
 
@@ -77,10 +86,11 @@ you'd expect.
77
86
  with a fifth of the window still in hand. It takes whole numbers only: Claude Code reports
78
87
  quota in integer percentages, so `--reserve 10.5` is rejected rather than silently rounded.
79
88
 
80
- The status line shows nothing until the reserve is reached, then an orange
81
- `⚠ Pausing at next tool call`, its icon pulsing once per refresh. Once you have consented it
82
- drops back to a quiet `▶ spare10`, with a non-default reserve spelled out as `▶ spare10 (20%)`
83
- the name already accounts for 10.
89
+ The status line shows a gray `⧗ spare10` until the first quota reading arrives, a green
90
+ `● spare10` while the reserve is untouched, then an orange
91
+ `⚠ Pausing at next tool call` once it is reached, its icon pulsing once per refresh. Once you
92
+ have consented it drops back to a quiet orange `⨯ spare10`. A non-default reserve is spelled
93
+ out either way, as `● spare10 (20%)` — the name already accounts for 10.
84
94
 
85
95
  The pulse is driven by spare10's own render cadence rather than the ANSI blink attribute,
86
96
  which most terminals ignore.
@@ -166,7 +176,7 @@ Not in this version, deliberately:
166
176
 
167
177
  ```bash
168
178
  npm install
169
- npm test # 122 tests: unit, plus the hooks as real subprocesses
179
+ npm test # 128 tests: unit, plus the hooks and CLI as real subprocesses
170
180
  npm run typecheck
171
181
  npm run build # single dependency-free bundle in dist/
172
182
  ```
package/dist/spare10.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var N={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjected:!1,awaitingApproval:!1,tick:0},C=10,he=2,c={reserve:C,pausePrompt:null,refresh:he,badge:!0,chain:null},V=3,K=3,W=3600;function w(e){return 100-e.reserve}function $(e){return 100-e}var u=class extends Error{},T=`spare10 \u2014 pause Claude Code before the 5-hour quota runs out
2
+ var C={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjected:!1,awaitingApproval:!1,tick:0},N=10,ye=2,c={reserve:N,pausePrompt:null,refresh:ye,badge:!0,chain:null},K=3,W=3,Y=3600;function w(e){return 100-e.reserve}function P(e){return 100-e}var u=class extends Error{},T=`spare10 \u2014 pause Claude Code before the 5-hour quota runs out
3
3
 
4
4
  spare10 [options] <command> [args...]
5
5
 
@@ -16,18 +16,18 @@ 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 be(e){if(!/^\d+$/.test(e))throw new u(`--reserve must be a whole number: the API reports quota in integer percentages, so "${e}" cannot be honoured.`);let t=Number(e);if(t<1||t>99)throw new u(`--reserve must be between 1 and 99, got ${t}.`);return t}function Se(e){if(!/^\d+$/.test(e)||Number(e)<1)throw new u(`--refresh must be a whole number of seconds >= 1, got "${e}".`);return Number(e)}function Q(e){let t={reserve:c.reserve,pausePrompt:c.pausePrompt,refresh:c.refresh,badge:c.badge},n=0,r=o=>{let i=e[n+1];if(i===void 0)throw new u(`${o} requires a value.`);return n+=1,i};for(;n<e.length;n+=1){let o=e[n];if(!o.startsWith("-"))break;switch(o){case"--reserve":t.reserve=be(r(o));break;case"--threshold":throw new u('--threshold was replaced by --reserve, which is the quota kept back rather than the level that trips. "--threshold 90" is now "--reserve 10".');case"--pause-prompt":t.pausePrompt=r(o);break;case"--refresh":t.refresh=Se(r(o));break;case"--no-badge":t.badge=!1;break;case"-h":case"--help":throw new u("");default:throw new u(`Unknown option "${o}".`)}}let s=e.slice(n);if(s.length===0)throw new u("No command given. Try: spare10 claude");return{config:t,command:s}}import{readFileSync as Re}from"node:fs";var we=new Set(["bypassPermissions","dontAsk"]),k={kind:"pass"};function I(e,t,n){return e.resetsAt!==null?n<e.resetsAt:e.updatedAt===null?!1:n-e.updatedAt<=t.refresh*K}function ye(e){return e===null?"an unknown time":new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function X(e,t){return`spare10 \u2014 into your ${t.reserve}% reserve \xB7 ${$(e.pct??0)}% of quota left \xB7 resets ${ye(e.resetsAt)}`}var U=X;function z(e,t,n){return R({state:e,config:t,now:n,permissionMode:null}).kind!=="pass"}function Y(e,t){return`${X(e,t)}
20
- The agent will pause safely at its first tool call \u2014 between operations, with nothing left half-written.`}function R({state:e,config:t,now:n,permissionMode:r}){return e.pct===null||e.updatedAt===null||e.blind||!I(e,t,n)||e.disarmedUntil!==null&&n<e.disarmedUntil||e.pct<w(t)?k:t.pausePrompt!==null&&!e.pausePromptInjected?{kind:"inject",text:`${U(e,t)}
19
+ spare10 --pause-prompt "Finish this block, commit, then stop." claude --resume`;function ke(e){if(!/^\d+$/.test(e))throw new u(`--reserve must be a whole number: the API reports quota in integer percentages, so "${e}" cannot be honoured.`);let t=Number(e);if(t<1||t>99)throw new u(`--reserve must be between 1 and 99, got ${t}.`);return t}function Ae(e){if(!/^\d+$/.test(e)||Number(e)<1)throw new u(`--refresh must be a whole number of seconds >= 1, got "${e}".`);return Number(e)}function Q(e){let t={reserve:c.reserve,pausePrompt:c.pausePrompt,refresh:c.refresh,badge:c.badge},n=0,r=o=>{let i=e[n+1];if(i===void 0)throw new u(`${o} requires a value.`);return n+=1,i};for(;n<e.length;n+=1){let o=e[n];if(!o.startsWith("-"))break;switch(o){case"--reserve":t.reserve=ke(r(o));break;case"--threshold":throw new u('--threshold was replaced by --reserve, which is the quota kept back rather than the level that trips. "--threshold 90" is now "--reserve 10".');case"--pause-prompt":t.pausePrompt=r(o);break;case"--refresh":t.refresh=Ae(r(o));break;case"--no-badge":t.badge=!1;break;case"-h":case"--help":throw new u("");default:throw new u(`Unknown option "${o}".`)}}let s=e.slice(n);if(s.length===0)throw new u("No command given. Try: spare10 claude");return{config:t,command:s}}import{readFileSync as Ne}from"node:fs";var $e=new Set(["bypassPermissions","dontAsk"]),k={kind:"pass"};function U(e,t,n){return e.resetsAt!==null?n<e.resetsAt:e.updatedAt===null?!1:n-e.updatedAt<=t.refresh*W}function xe(e){return e===null?"an unknown time":new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function z(e,t){return`into your ${t.reserve}% reserve \xB7 ${P(e.pct??0)}% of quota left \xB7 resets ${xe(e.resetsAt)}`}function Z(e,t){return`spare10 \u2014 ${z(e,t)}`}function Pe(e,t){return`spare10 budget guard. You have reached the safe usage limit for this session (${z(e,t)}). Wrap up your work and stop.
21
20
 
22
- ${t.pausePrompt}`}:r!==null&&we.has(r)?{kind:"deny",reason:`${U(e,t)}. Stop now and wait for the user. Do not call any further tools.`}:{kind:"ask",reason:U(e,t)}}function Z(e){try{let t=JSON.parse(e);if(typeof t!="object"||t===null)return{permissionMode:null,toolName:null};let n=t;return{permissionMode:typeof n.permission_mode=="string"?n.permission_mode:null,toolName:typeof n.tool_name=="string"?n.tool_name:null}}catch{return{permissionMode:null,toolName:null}}}import{readFileSync as ke,writeFileSync as Ae,renameSync as xe,mkdirSync as Pe,unlinkSync as $e}from"node:fs";import{join as ee}from"node:path";var te=e=>ee(e,"state.json"),D=e=>ee(e,"config.json");function d(){return Math.floor(Date.now()/1e3)}function ne(e){try{let t=JSON.parse(ke(e,"utf8"));return typeof t=="object"&&t!==null?t:null}catch{return null}}var h=(e,t)=>typeof e=="number"&&Number.isFinite(e)?e:t,v=(e,t)=>typeof e=="boolean"?e:t;function f(e){let t=ne(te(e));return t?{pct:h(t.pct,null),resetsAt:h(t.resetsAt,null),updatedAt:h(t.updatedAt,null),missingStreak:h(t.missingStreak,0)??0,blind:v(t.blind,!1),disarmedUntil:h(t.disarmedUntil,null),pausePromptInjected:v(t.pausePromptInjected,!1),awaitingApproval:v(t.awaitingApproval,!1),tick:h(t.tick,0)??0}:{...N}}function m(e,t){let n=te(e),r=`${n}.${process.pid}.tmp`;try{Pe(e,{recursive:!0}),Ae(r,JSON.stringify(t),"utf8"),xe(r,n)}catch{try{$e(r)}catch{}}}function y(e){let t=ne(D(e));if(!t)return{...c};let n=h(t.reserve,c.reserve)??c.reserve,r=h(t.refresh,c.refresh)??c.refresh;return{reserve:Math.min(99,Math.max(1,Math.round(n))),pausePrompt:typeof t.pausePrompt=="string"&&t.pausePrompt?t.pausePrompt:null,refresh:Math.max(1,Math.round(r)),badge:v(t.badge,c.badge),chain:typeof t.chain=="string"&&t.chain?t.chain:null}}function A(e,t){return e.resetsAt??t+W}function re(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:{...N,pct:r.pct,resetsAt:r.resetsAt,updatedAt:r.updatedAt}}function F(){try{return Re(0,"utf8")}catch{return""}}function j(e){process.stdout.write(JSON.stringify({hookSpecificOutput:e}))}function se(e){let t=y(e),n=f(e),r=d();if(R({state:n,config:t,now:r,permissionMode:null}).kind==="pass")return F(),0;let{permissionMode:s}=Z(F()),o=R({state:n,config:t,now:r,permissionMode:s});switch(o.kind){case"pass":return 0;case"inject":return m(e,{...n,pausePromptInjected:!0,disarmedUntil:A(n,r)}),j({hookEventName:"PreToolUse",additionalContext:o.text}),0;case"ask":return m(e,{...n,awaitingApproval:!0}),j({hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:o.reason}),0;case"deny":return j({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o.reason}),0}}function oe(e){F();let t=f(e);return t.awaitingApproval&&m(e,{...t,awaitingApproval:!1,disarmedUntil:A(t,d())}),0}import{execFileSync as de}from"node:child_process";import{accessSync as Je,constants as Ve,readdirSync as Ke,statSync as B}from"node:fs";import{join as We}from"node:path";function ie(e,t,n){return e.pct===null||e.updatedAt===null?{status:"no-data",detail:"no quota reading yet \u2014 start a session and wait a moment"}:e.blind?{status:"blind",detail:"Claude Code is not reporting rate_limits on this plan"}:I(e,t,n)?e.disarmedUntil!==null&&n<e.disarmedUntil?{status:"disarmed",detail:`consent given; quiet until ${O(e.disarmedUntil)}`}:e.pct>=w(t)?{status:"tripped",detail:`into the ${t.reserve}% reserve`}:{status:"armed",detail:`${$(e.pct)}% left, of which ${t.reserve}% is reserved`}:{status:"stale",detail:"the window this reading described has already reset"}}function M(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 O(e){return new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawnSync as Ee}from"node:child_process";import{closeSync as _e,mkdirSync as Ne,openSync as Ce,readSync as Te,readdirSync as ue,rmSync as Ue,statSync as Ie,writeFileSync as De}from"node:fs";import{homedir as le}from"node:os";import{basename as je,join as x}from"node:path";import{fileURLToPath as Fe}from"node:url";import{readFileSync as ve}from"node:fs";function L(e){return`'${e.replace(/'/g,"'\\''")}'`}function E(e){let t;try{t=JSON.parse(ve(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 ae(e){let t=s=>`${L(e.nodePath)} ${L(e.selfPath)} ${s} --run ${L(e.runDir)}`,n=s=>[{matcher:"",hooks:[{type:"command",command:t(s)}]}],r={type:"command",command:t("sensor"),refreshInterval:e.refresh};return e.padding!==void 0&&(r.padding=e.padding),{statusLine:r,hooks:{PreToolUse:n("gate"),PostToolUse:n("post")}}}var Me=7*24*60*60*1e3,H=()=>x(le(),".spare10","runs"),G=()=>x(le(),".claude","settings.json");function Oe(e,t){let n;try{n=ue(e)}catch{return}for(let r of n){let s=x(e,r);try{t-Ie(s).mtimeMs>Me&&Ue(s,{recursive:!0,force:!0})}catch{}}}function Le(e,t){let n;try{n=ue(e)}catch{return}let r=n.map(o=>x(e,o)).filter(o=>o!==t).map(f),s=re(r,d());s!==null&&m(t,s)}function He(e){let t=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=x(e,t);return Ne(n,{recursive:!0}),n}function Ge(e){let t;try{t=Ce("/dev/tty","r")}catch{return null}process.stderr.write(e);let n=Buffer.alloc(1),r="";try{for(;Te(t,n,0,1,null)>0;){let s=n.toString("utf8");if(s===`
23
- `||s==="\r")break;r+=s}}catch{return null}finally{_e(t)}return r.trim()}function qe(e,t,n){return e?t||n===null?"proceeding":/^y(es)?$/i.test(n.trim())?"consented":"declined":"clear"}function Be(e,t){let n=f(e);if(!z(n,t,d()))return"clear";process.stderr.write(`
24
- ${Y(n,t)}
25
- `);let r=t.pausePrompt!==null?null:Ge("Start anyway? [y/N] ");return qe(!0,t.pausePrompt!==null,r)}function q(){return Fe(import.meta.url)}function ce({config:e,command:t}){let[n,...r]=t;if(je(n)!=="claude")throw new u(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let s=H();Oe(s,Date.now());let o=He(s);Le(s,o);let i=Be(o,{...e,chain:null});if(i==="declined")return process.stderr.write(`Not started.
26
- `),0;if(i==="consented"){let P=f(o);m(o,{...P,disarmedUntil:A(P,d())}),process.stderr.write(`Continuing into the reserve for this window.
21
+ User instructions: ${t.pausePrompt??""}`}var X=Z;function ee(e,t,n){return R({state:e,config:t,now:n,permissionMode:null}).kind!=="pass"}function te(e,t){return`${Z(e,t)}
22
+ The agent will pause safely at its first tool call \u2014 between operations, with nothing left half-written.`}function R({state:e,config:t,now:n,permissionMode:r}){return e.pct===null||e.updatedAt===null||e.blind||!U(e,t,n)||e.disarmedUntil!==null&&n<e.disarmedUntil||e.pct<w(t)?k:t.pausePrompt!==null&&!e.pausePromptInjected?{kind:"inject",text:Pe(e,t)}:r!==null&&$e.has(r)?{kind:"deny",reason:`${X(e,t)}. Stop now and wait for the user. Do not call any further tools.`}:{kind:"ask",reason:X(e,t)}}function ne(e){try{let t=JSON.parse(e);if(typeof t!="object"||t===null)return{permissionMode:null,toolName:null};let n=t;return{permissionMode:typeof n.permission_mode=="string"?n.permission_mode:null,toolName:typeof n.tool_name=="string"?n.tool_name:null}}catch{return{permissionMode:null,toolName:null}}}import{readFileSync as Re,writeFileSync as ve,renameSync as Ee,mkdirSync as _e,unlinkSync as Ce}from"node:fs";import{join as re}from"node:path";var se=e=>re(e,"state.json"),I=e=>re(e,"config.json");function d(){return Math.floor(Date.now()/1e3)}function oe(e){try{let t=JSON.parse(Re(e,"utf8"));return typeof t=="object"&&t!==null?t:null}catch{return null}}var h=(e,t)=>typeof e=="number"&&Number.isFinite(e)?e:t,v=(e,t)=>typeof e=="boolean"?e:t;function f(e){let t=oe(se(e));return t?{pct:h(t.pct,null),resetsAt:h(t.resetsAt,null),updatedAt:h(t.updatedAt,null),missingStreak:h(t.missingStreak,0)??0,blind:v(t.blind,!1),disarmedUntil:h(t.disarmedUntil,null),pausePromptInjected:v(t.pausePromptInjected,!1),awaitingApproval:v(t.awaitingApproval,!1),tick:h(t.tick,0)??0}:{...C}}function m(e,t){let n=se(e),r=`${n}.${process.pid}.tmp`;try{_e(e,{recursive:!0}),ve(r,JSON.stringify(t),"utf8"),Ee(r,n)}catch{try{Ce(r)}catch{}}}function y(e){let t=oe(I(e));if(!t)return{...c};let n=h(t.reserve,c.reserve)??c.reserve,r=h(t.refresh,c.refresh)??c.refresh;return{reserve:Math.min(99,Math.max(1,Math.round(n))),pausePrompt:typeof t.pausePrompt=="string"&&t.pausePrompt?t.pausePrompt:null,refresh:Math.max(1,Math.round(r)),badge:v(t.badge,c.badge),chain:typeof t.chain=="string"&&t.chain?t.chain:null}}function A(e,t){return e.resetsAt??t+Y}function ie(e,t){let r=e.filter(s=>s.pct!==null&&s.updatedAt!==null&&s.resetsAt!==null&&s.resetsAt>t).reduce((s,o)=>s===null||o.updatedAt>s.updatedAt?o:s,null);return r===null?null:{...C,pct:r.pct,resetsAt:r.resetsAt,updatedAt:r.updatedAt}}function F(){try{return Ne(0,"utf8")}catch{return""}}function D(e){process.stdout.write(JSON.stringify({hookSpecificOutput:e}))}function ae(e){let t=y(e),n=f(e),r=d();if(R({state:n,config:t,now:r,permissionMode:null}).kind==="pass")return F(),0;let{permissionMode:s}=ne(F()),o=R({state:n,config:t,now:r,permissionMode:s});switch(o.kind){case"pass":return 0;case"inject":return m(e,{...n,pausePromptInjected:!0,disarmedUntil:A(n,r)}),D({hookEventName:"PreToolUse",additionalContext:o.text}),0;case"ask":return m(e,{...n,awaitingApproval:!0}),D({hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:o.reason}),0;case"deny":return D({hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o.reason}),0}}function ue(e){F();let t=f(e);return t.awaitingApproval&&m(e,{...t,awaitingApproval:!1,disarmedUntil:A(t,d())}),0}import{execFileSync as me}from"node:child_process";import{accessSync as Qe,constants as Xe,readdirSync as ze,statSync as q}from"node:fs";import{join as Ze}from"node:path";function le(e,t,n){return e.pct===null||e.updatedAt===null?{status:"no-data",detail:"no quota reading yet \u2014 start a session and wait a moment"}:e.blind?{status:"blind",detail:"Claude Code is not reporting rate_limits on this plan"}:U(e,t,n)?e.disarmedUntil!==null&&n<e.disarmedUntil?{status:"disarmed",detail:`consent given; quiet until ${M(e.disarmedUntil)}`}:e.pct>=w(t)?{status:"tripped",detail:`into the ${t.reserve}% reserve`}:{status:"armed",detail:`${P(e.pct)}% left, of which ${t.reserve}% is reserved`}:{status:"stale",detail:"the window this reading described has already reset"}}function j(e){let t=Math.abs(Math.round(e));if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function M(e){return new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}import{spawnSync as Ue}from"node:child_process";import{closeSync as Ie,mkdirSync as De,openSync as Fe,readSync as je,readdirSync as de,rmSync as Me,statSync as Oe,writeFileSync as Le}from"node:fs";import{homedir as pe}from"node:os";import{basename as He,join as $}from"node:path";import{fileURLToPath as Ge}from"node:url";import{readFileSync as Te}from"node:fs";function O(e){return`'${e.replace(/'/g,"'\\''")}'`}function E(e){let t;try{t=JSON.parse(Te(e,"utf8"))}catch{return{command:null,padding:void 0}}if(typeof t!="object"||t===null)return{command:null,padding:void 0};let n=t.statusLine;if(typeof n!="object"||n===null)return{command:null,padding:void 0};let r=n,s=typeof r.command=="string"?r.command:null,o=typeof r.padding=="number"?r.padding:void 0;return s!==null&&s.includes("spare10")?{command:null,padding:o}:{command:s,padding:o}}function ce(e){let t=s=>`${O(e.nodePath)} ${O(e.selfPath)} ${s} --run ${O(e.runDir)}`,n=s=>[{matcher:"",hooks:[{type:"command",command:t(s)}]}],r={type:"command",command:t("sensor"),refreshInterval:e.refresh};return e.padding!==void 0&&(r.padding=e.padding),{statusLine:r,hooks:{PreToolUse:n("gate"),PostToolUse:n("post")}}}var qe=7*24*60*60*1e3,L=()=>$(pe(),".spare10","runs"),H=()=>$(pe(),".claude","settings.json");function Be(e,t){let n;try{n=de(e)}catch{return}for(let r of n){let s=$(e,r);try{t-Oe(s).mtimeMs>qe&&Me(s,{recursive:!0,force:!0})}catch{}}}function Je(e,t){let n;try{n=de(e)}catch{return}let r=n.map(o=>$(e,o)).filter(o=>o!==t).map(f),s=ie(r,d());s!==null&&m(t,s)}function Ve(e){let t=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=$(e,t);return De(n,{recursive:!0}),n}function Ke(e){let t;try{t=Fe("/dev/tty","r")}catch{return null}process.stderr.write(e);let n=Buffer.alloc(1),r="";try{for(;je(t,n,0,1,null)>0;){let s=n.toString("utf8");if(s===`
23
+ `||s==="\r")break;r+=s}}catch{return null}finally{Ie(t)}return r.trim()}function We(e,t,n){return e?t||n===null?"proceeding":/^y(es)?$/i.test(n.trim())?"consented":"declined":"clear"}function Ye(e,t){let n=f(e);if(!ee(n,t,d()))return"clear";process.stderr.write(`
24
+ ${te(n,t)}
25
+ `);let r=t.pausePrompt!==null?null:Ke("Start anyway? [y/N] ");return We(!0,t.pausePrompt!==null,r)}function G(){return Ge(import.meta.url)}function fe({config:e,command:t}){let[n,...r]=t;if(He(n)!=="claude")throw new u(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let s=L();Be(s,Date.now());let o=Ve(s);Je(s,o);let i=Ye(o,{...e,chain:null});if(i==="declined")return process.stderr.write(`Not started.
26
+ `),0;if(i==="consented"){let x=f(o);m(o,{...x,disarmedUntil:A(x,d())}),process.stderr.write(`Continuing into the reserve for this window.
27
27
 
28
- `)}let l=E(G());De(D(o),JSON.stringify({...e,chain:l.command},null,2));let p=ae({nodePath:process.execPath,selfPath:q(),runDir:o,refresh:e.refresh,padding:l.padding}),b=()=>{};process.on("SIGINT",b),process.on("SIGTERM",b);let g=Ee(n,["--settings",JSON.stringify(p),...r],{stdio:"inherit"});return g.error?(process.stderr.write(`spare10: could not start ${n}: ${g.error.message}
29
- `),127):g.signal?128+(g.signal==="SIGINT"?2:15):g.status??0}var Qe="0.1.1",a={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function Xe(e){try{return Ke(e).map(n=>We(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 ze(e){try{return de("command",["-v",e],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function Ye(){try{return de("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function Ze(e){try{return Je(e,Ve.X_OK),!0}catch{return!1}}function pe(){let e=(S="")=>process.stdout.write(`${S}
30
- `),t=d(),n=0;e(`spare10 ${Qe}`),e(),e("Environment"),e(` ${a.info} node ${process.execPath} (${process.version})`),e(` ${a.info} spare10 ${q()}`);let r=ze("claude"),s=r?Ye():null;r&&s?e(` ${a.ok} claude ${r} (${s})`):(n+=1,e(` ${a.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),e(),e("Status line");let o=E(G());if(o.command===null)e(` ${a.info} nothing to chain \u2014 spare10 will own the status line`);else{let S=o.command.split(/\s+/)[0]??"",_=Ze(S);_||(n+=1),e(` ${_?a.ok:a.warn} chains into ${o.command}`),_||e(" that path is not executable; its output will be dropped")}e();let i=Xe(H());if(i===null)return e("No runs yet. Start one with: spare10 claude"),n>0?1:0;let l=y(i),p=f(i),b=ie(p,l,t);if(e(`Latest run ${i}`),e(` ${a.info} reserve ${l.reserve}% of the 5-hour window`),e(` ${a.info} on trip ${l.pausePrompt===null?"ask for confirmation":`inject: ${JSON.stringify(l.pausePrompt)}`}`),e(` ${a.info} refresh ${l.refresh}s`),p.pct!==null&&p.updatedAt!==null){e(` ${a.ok} quota ${p.pct}% used, ${100-p.pct}% left`);let S=t-p.updatedAt;e(` ${a.info} last reading ${M(S)} ago`+(S>l.refresh*3?" (no session running; still valid for this window)":""))}p.resetsAt!==null&&e(` ${a.info} resets ${O(p.resetsAt)} (in ${M(p.resetsAt-t)})`);let g=b.status==="blind";g&&(n+=1);let P=g?a.warn:b.status==="no-data"?a.info:a.ok;return e(` ${P} state ${b.status.toUpperCase()} \u2014 ${b.detail}`),n>0?1:0}import{spawnSync as et}from"node:child_process";import{readFileSync as tt}from"node:fs";function fe(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function me(e){let t;try{t=JSON.parse(e)}catch{return{sessionId:null,fiveHour:null}}if(typeof t!="object"||t===null)return{sessionId:null,fiveHour:null};let n=t,r=typeof n.session_id=="string"?n.session_id:null,s=n.rate_limits;if(typeof s!="object"||s===null)return{sessionId:r,fiveHour:null};let o=s.five_hour;if(typeof o!="object"||o===null)return{sessionId:r,fiveHour:null};let i=o,l=fe(i.used_percentage);return l===null?{sessionId:r,fiveHour:null}:{sessionId:r,fiveHour:{usedPercentage:Math.min(100,Math.max(0,l)),resetsAt:fe(i.resets_at)}}}function nt(e,t,n){let r=t.fiveHour,s=e.tick+1;if(!r){let i=e.missingStreak+1;return{...e,tick:s,missingStreak:i,blind:i>=V}}let o=r.resetsAt!==null&&e.resetsAt!==null&&r.resetsAt!==e.resetsAt;return{tick:s,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:o?null:e.disarmedUntil,pausePromptInjected:o?!1:e.pausePromptInjected,awaitingApproval:o?!1:e.awaitingApproval}}var rt="\x1B[38;5;208m",st="\x1B[39m",ot=e=>`${rt}${e}${st}`;function it(e,t,n){if(!t.badge)return"";if(e.blind)return"\u26A0 spare10 quota unavailable";if(e.pct===null||e.pct<w(t))return"";if(e.disarmedUntil!==null&&n<e.disarmedUntil)return`\u25B6 spare10${t.reserve===C?"":` (${t.reserve}%)`}`;let s=e.tick%2===0?"\u26A0":" ";return ot(`${s} Pausing at next tool call`)}function at(e,t){if(!e||e.includes("spare10 sensor"))return"";try{return(et(e,{shell:!0,input:t,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function ut(){try{return tt(0,"utf8")}catch{return""}}function ge(e){let t=ut(),n=y(e),r=nt(f(e),me(t),d());m(e,r);let s=[it(r,n,d()),at(n.chain,t)].filter(Boolean);return s.length>0&&process.stdout.write(s.join(" ")),0}function J(e,t){let n=e.indexOf(t);return n===-1?null:e[n+1]??null}function lt(e){let[t,...n]=e;switch(t){case"sensor":{let r=J(n,"--run");return r?ge(r):0}case"gate":{let r=J(n,"--run");return r?se(r):0}case"post":{let r=J(n,"--run");return r?oe(r):0}case"doctor":return pe();default:return ce(Q(e))}}try{process.exit(lt(process.argv.slice(2)))}catch(e){throw e instanceof u&&(e.message&&(process.stderr.write(`spare10: ${e.message}
28
+ `)}let l=E(H());Le(I(o),JSON.stringify({...e,chain:l.command},null,2));let p=ce({nodePath:process.execPath,selfPath:G(),runDir:o,refresh:e.refresh,padding:l.padding}),b=()=>{};process.on("SIGINT",b),process.on("SIGTERM",b);let g=Ue(n,["--settings",JSON.stringify(p),...r],{stdio:"inherit"});return g.error?(process.stderr.write(`spare10: could not start ${n}: ${g.error.message}
29
+ `),127):g.signal?128+(g.signal==="SIGINT"?2:15):g.status??0}var et="0.2.0",a={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function tt(e){try{return ze(e).map(n=>Ze(e,n)).filter(n=>{try{return q(n).isDirectory()}catch{return!1}}).sort((n,r)=>q(r).mtimeMs-q(n).mtimeMs)[0]??null}catch{return null}}function nt(e){try{return me("command",["-v",e],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function rt(){try{return me("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function st(e){try{return Qe(e,Xe.X_OK),!0}catch{return!1}}function ge(){let e=(S="")=>process.stdout.write(`${S}
30
+ `),t=d(),n=0;e(`spare10 ${et}`),e(),e("Environment"),e(` ${a.info} node ${process.execPath} (${process.version})`),e(` ${a.info} spare10 ${G()}`);let r=nt("claude"),s=r?rt():null;r&&s?e(` ${a.ok} claude ${r} (${s})`):(n+=1,e(` ${a.warn} claude not found on PATH \u2014 spare10 has nothing to wrap`)),e(),e("Status line");let o=E(H());if(o.command===null)e(` ${a.info} nothing to chain \u2014 spare10 will own the status line`);else{let S=o.command.split(/\s+/)[0]??"",_=st(S);_||(n+=1),e(` ${_?a.ok:a.warn} chains into ${o.command}`),_||e(" that path is not executable; its output will be dropped")}e();let i=tt(L());if(i===null)return e("No runs yet. Start one with: spare10 claude"),n>0?1:0;let l=y(i),p=f(i),b=le(p,l,t);if(e(`Latest run ${i}`),e(` ${a.info} reserve ${l.reserve}% of the 5-hour window`),e(` ${a.info} on trip ${l.pausePrompt===null?"ask for confirmation":`inject: ${JSON.stringify(l.pausePrompt)}`}`),e(` ${a.info} refresh ${l.refresh}s`),p.pct!==null&&p.updatedAt!==null){e(` ${a.ok} quota ${p.pct}% used, ${100-p.pct}% left`);let S=t-p.updatedAt;e(` ${a.info} last reading ${j(S)} ago`+(S>l.refresh*3?" (no session running; still valid for this window)":""))}p.resetsAt!==null&&e(` ${a.info} resets ${M(p.resetsAt)} (in ${j(p.resetsAt-t)})`);let g=b.status==="blind";g&&(n+=1);let x=g?a.warn:b.status==="no-data"?a.info:a.ok;return e(` ${x} state ${b.status.toUpperCase()} \u2014 ${b.detail}`),n>0?1:0}import{spawnSync as ot}from"node:child_process";import{readFileSync as it}from"node:fs";function he(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function be(e){let t;try{t=JSON.parse(e)}catch{return{sessionId:null,fiveHour:null}}if(typeof t!="object"||t===null)return{sessionId:null,fiveHour:null};let n=t,r=typeof n.session_id=="string"?n.session_id:null,s=n.rate_limits;if(typeof s!="object"||s===null)return{sessionId:r,fiveHour:null};let o=s.five_hour;if(typeof o!="object"||o===null)return{sessionId:r,fiveHour:null};let i=o,l=he(i.used_percentage);return l===null?{sessionId:r,fiveHour:null}:{sessionId:r,fiveHour:{usedPercentage:Math.min(100,Math.max(0,l)),resetsAt:he(i.resets_at)}}}function at(e,t,n){let r=t.fiveHour,s=e.tick+1;if(!r){let i=e.missingStreak+1;return{...e,tick:s,missingStreak:i,blind:i>=K}}let o=r.resetsAt!==null&&e.resetsAt!==null&&r.resetsAt!==e.resetsAt;return{tick:s,pct:r.usedPercentage,resetsAt:r.resetsAt,updatedAt:n,missingStreak:0,blind:!1,disarmedUntil:o?null:e.disarmedUntil,pausePromptInjected:o?!1:e.pausePromptInjected,awaitingApproval:o?!1:e.awaitingApproval}}var ut="\x1B[38;5;208m",lt="\x1B[38;5;40m",ct="\x1B[38;5;245m",J="\x1B[39m",Se=e=>`${ut}${e}${J}`,dt=e=>`${lt}${e}${J}`,pt=e=>`${ct}${e}${J}`,B=e=>e.reserve===N?"spare10":`spare10 (${e.reserve}%)`;function ft(e,t,n){if(!t.badge)return"";if(e.blind)return"\u26A0 spare10 quota unavailable";if(e.pct===null)return pt(`\u29D7 ${B(t)}`);if(e.pct<w(t))return dt(`\u25CF ${B(t)}`);if(e.disarmedUntil!==null&&n<e.disarmedUntil)return Se(`\u2A2F ${B(t)}`);let s=e.tick%2===0?"\u26A0":" ";return Se(`${s} Pausing at next tool call`)}function mt(e,t){if(!e||e.includes("spare10 sensor"))return"";try{return(ot(e,{shell:!0,input:t,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function gt(){try{return it(0,"utf8")}catch{return""}}function we(e){let t=gt(),n=y(e),r=at(f(e),be(t),d());m(e,r);let s=[ft(r,n,d()),mt(n.chain,t)].filter(Boolean);return s.length>0&&process.stdout.write(s.join(" ")),0}function V(e,t){let n=e.indexOf(t);return n===-1?null:e[n+1]??null}function ht(e){let[t,...n]=e;switch(t){case"sensor":{let r=V(n,"--run");return r?we(r):0}case"gate":{let r=V(n,"--run");return r?ae(r):0}case"post":{let r=V(n,"--run");return r?ue(r):0}case"doctor":return ge();default:return fe(Q(e))}}try{process.exit(ht(process.argv.slice(2)))}catch(e){throw e instanceof u&&(e.message&&(process.stderr.write(`spare10: ${e.message}
31
31
 
32
32
  ${T}
33
33
  `),process.exit(2)),process.stdout.write(`${T}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spare10",
3
- "version": "0.1.1",
3
+ "version": "0.2.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",