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