spare10 0.3.0 → 0.3.1

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 (2) hide show
  1. package/dist/spare10.js +18 -18
  2. package/package.json +1 -1
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 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
3
3
 
4
4
  spare10 [options] <command> [args...]
5
5
 
@@ -16,25 +16,25 @@ Commands:
16
16
  Examples:
17
17
  spare10 claude
18
18
  spare10 --reserve 15 claude
19
- spare10 --pause-prompt "Finish this block, commit, then stop." claude --resume`;function 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)}
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
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.
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
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}
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
37
 
38
38
  ${U}
39
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)}
40
+ `),process.exit(0)),t}try{Promise.resolve(qe(process.argv.slice(2))).then(t=>process.exit(t),Et)}catch(t){Et(t)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spare10",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",