spare10 0.1.0 → 0.1.2
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 +11 -2
- package/dist/spare10.js +13 -13
- package/package.json +1 -1
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
|
|
|
@@ -166,7 +175,7 @@ Not in this version, deliberately:
|
|
|
166
175
|
|
|
167
176
|
```bash
|
|
168
177
|
npm install
|
|
169
|
-
npm test #
|
|
178
|
+
npm test # 128 tests: unit, plus the hooks and CLI as real subprocesses
|
|
170
179
|
npm run typecheck
|
|
171
180
|
npm run build # single dependency-free bundle in dist/
|
|
172
181
|
```
|
package/dist/spare10.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var C={pct:null,resetsAt:null,updatedAt:null,missingStreak:0,blind:!1,disarmedUntil:null,pausePromptInjected:!1,awaitingApproval:!1,tick:0},N=10,be=2,c={reserve:N,pausePrompt:null,refresh:be,badge:!0,chain:null},J=3,V=3,K=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
|
|
3
3
|
|
|
4
4
|
spare10 [options] <command> [args...]
|
|
5
5
|
|
|
@@ -16,19 +16,19 @@ 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
|
|
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 Se(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 we(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 W(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=Se(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=we(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 Ee}from"node:fs";var ye=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*V}function ke(e){return e===null?"an unknown time":new Date(e*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function X(e,t){return`into your ${t.reserve}% reserve \xB7 ${$(e.pct??0)}% of quota left \xB7 resets ${ke(e.resetsAt)}`}function Y(e,t){return`spare10 \u2014 ${X(e,t)}`}function xe(e,t){return`spare10 budget guard. You have reached the safe usage limit for this session (${X(e,t)}). Wrap up your work and stop.
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`),
|
|
21
|
+
User instructions: ${t.pausePrompt??""}`}var Q=Y;function z(e,t,n){return R({state:e,config:t,now:n,permissionMode:null}).kind!=="pass"}function Z(e,t){return`${Y(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:xe(e,t)}:r!==null&&ye.has(r)?{kind:"deny",reason:`${Q(e,t)}. Stop now and wait for the user. Do not call any further tools.`}:{kind:"ask",reason:Q(e,t)}}function ee(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 Ae,writeFileSync as Pe,renameSync as $e,mkdirSync as Re,unlinkSync as ve}from"node:fs";import{join as te}from"node:path";var ne=e=>te(e,"state.json"),I=e=>te(e,"config.json");function d(){return Math.floor(Date.now()/1e3)}function re(e){try{let t=JSON.parse(Ae(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=re(ne(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=ne(e),r=`${n}.${process.pid}.tmp`;try{Re(e,{recursive:!0}),Pe(r,JSON.stringify(t),"utf8"),$e(r,n)}catch{try{ve(r)}catch{}}}function y(e){let t=re(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 x(e,t){return e.resetsAt??t+K}function se(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 Ee(0,"utf8")}catch{return""}}function D(e){process.stdout.write(JSON.stringify({hookSpecificOutput:e}))}function oe(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}=ee(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:x(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 ie(e){F();let t=f(e);return t.awaitingApproval&&m(e,{...t,awaitingApproval:!1,disarmedUntil:x(t,d())}),0}import{execFileSync as pe}from"node:child_process";import{accessSync as Ke,constants as We,readdirSync as Qe,statSync as G}from"node:fs";import{join as Xe}from"node:path";function ae(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:`${$(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 Ce}from"node:child_process";import{closeSync as Ne,mkdirSync as Te,openSync as Ue,readSync as Ie,readdirSync as le,rmSync as De,statSync as Fe,writeFileSync as je}from"node:fs";import{homedir as ce}from"node:os";import{basename as Me,join as A}from"node:path";import{fileURLToPath as Oe}from"node:url";import{readFileSync as _e}from"node:fs";function O(e){return`'${e.replace(/'/g,"'\\''")}'`}function E(e){let t;try{t=JSON.parse(_e(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 ue(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 Le=7*24*60*60*1e3,L=()=>A(ce(),".spare10","runs"),H=()=>A(ce(),".claude","settings.json");function He(e,t){let n;try{n=le(e)}catch{return}for(let r of n){let s=A(e,r);try{t-Fe(s).mtimeMs>Le&&De(s,{recursive:!0,force:!0})}catch{}}}function qe(e,t){let n;try{n=le(e)}catch{return}let r=n.map(o=>A(e,o)).filter(o=>o!==t).map(f),s=se(r,d());s!==null&&m(t,s)}function Ge(e){let t=`${Date.now().toString(36)}-${process.pid.toString(36)}`,n=A(e,t);return Te(n,{recursive:!0}),n}function Be(e){let t;try{t=Ue("/dev/tty","r")}catch{return null}process.stderr.write(e);let n=Buffer.alloc(1),r="";try{for(;Ie(t,n,0,1,null)>0;){let s=n.toString("utf8");if(s===`
|
|
23
|
+
`||s==="\r")break;r+=s}}catch{return null}finally{Ne(t)}return r.trim()}function Je(e,t,n){return e?t||n===null?"proceeding":/^y(es)?$/i.test(n.trim())?"consented":"declined":"clear"}function Ve(e,t){let n=f(e);if(!z(n,t,d()))return"clear";process.stderr.write(`
|
|
24
|
+
${Z(n,t)}
|
|
25
|
+
`);let r=t.pausePrompt!==null?null:Be("Start anyway? [y/N] ");return Je(!0,t.pausePrompt!==null,r)}function q(){return Oe(import.meta.url)}function de({config:e,command:t}){let[n,...r]=t;if(Me(n)!=="claude")throw new u(`spare10 wraps the "claude" command, but got "${n}". Run it as: ${["spare10",...r,"claude"].join(" ")}`);let s=L();He(s,Date.now());let o=Ge(s);qe(s,o);let i=Ve(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:x(P,d())}),process.stderr.write(`Continuing into the reserve for this window.
|
|
27
27
|
|
|
28
|
-
`)}let l=E(
|
|
29
|
-
`),127):g.signal?128+(g.signal==="SIGINT"?2:15):g.status??0}var
|
|
30
|
-
`),t=d(),n=0;e(`spare10 ${
|
|
28
|
+
`)}let l=E(H());je(I(o),JSON.stringify({...e,chain:l.command},null,2));let p=ue({nodePath:process.execPath,selfPath:q(),runDir:o,refresh:e.refresh,padding:l.padding}),b=()=>{};process.on("SIGINT",b),process.on("SIGTERM",b);let g=Ce(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 Ye="0.1.2",a={ok:"\u2713",warn:"\u26A0",info:"\xB7"};function ze(e){try{return Qe(e).map(n=>Xe(e,n)).filter(n=>{try{return G(n).isDirectory()}catch{return!1}}).sort((n,r)=>G(r).mtimeMs-G(n).mtimeMs)[0]??null}catch{return null}}function Ze(e){try{return pe("command",["-v",e],{shell:!0,encoding:"utf8"}).trim()||null}catch{return null}}function et(){try{return pe("claude",["--version"],{encoding:"utf8",timeout:1e4}).trim()}catch{return null}}function tt(e){try{return Ke(e,We.X_OK),!0}catch{return!1}}function fe(){let e=(S="")=>process.stdout.write(`${S}
|
|
30
|
+
`),t=d(),n=0;e(`spare10 ${Ye}`),e(),e("Environment"),e(` ${a.info} node ${process.execPath} (${process.version})`),e(` ${a.info} spare10 ${q()}`);let r=Ze("claude"),s=r?et():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]??"",_=tt(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=ze(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=ae(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 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 nt}from"node:child_process";import{readFileSync as rt}from"node:fs";function me(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function ge(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=me(i.used_percentage);return l===null?{sessionId:r,fiveHour:null}:{sessionId:r,fiveHour:{usedPercentage:Math.min(100,Math.max(0,l)),resetsAt:me(i.resets_at)}}}function st(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>=J}}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 ot="\x1B[38;5;208m",it="\x1B[39m",at=e=>`${ot}${e}${it}`;function ut(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===N?"":` (${t.reserve}%)`}`;let s=e.tick%2===0?"\u26A0":" ";return at(`${s} Pausing at next tool call`)}function lt(e,t){if(!e||e.includes("spare10 sensor"))return"";try{return(nt(e,{shell:!0,input:t,encoding:"utf8",timeout:5e3,maxBuffer:1e6}).stdout??"").replace(/\n+$/,"")}catch{return""}}function ct(){try{return rt(0,"utf8")}catch{return""}}function he(e){let t=ct(),n=y(e),r=st(f(e),ge(t),d());m(e,r);let s=[ut(r,n,d()),lt(n.chain,t)].filter(Boolean);return s.length>0&&process.stdout.write(s.join(" ")),0}function B(e,t){let n=e.indexOf(t);return n===-1?null:e[n+1]??null}function dt(e){let[t,...n]=e;switch(t){case"sensor":{let r=B(n,"--run");return r?he(r):0}case"gate":{let r=B(n,"--run");return r?oe(r):0}case"post":{let r=B(n,"--run");return r?ie(r):0}case"doctor":return fe();default:return de(W(e))}}try{process.exit(dt(process.argv.slice(2)))}catch(e){throw e instanceof u&&(e.message&&(process.stderr.write(`spare10: ${e.message}
|
|
31
31
|
|
|
32
32
|
${T}
|
|
33
|
-
|
|
34
|
-
`),process.exit(
|
|
33
|
+
`),process.exit(2)),process.stdout.write(`${T}
|
|
34
|
+
`),process.exit(0)),e}
|