spare10 0.1.0 → 0.1.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.
- package/dist/spare10.js +7 -7
- package/package.json +1 -1
package/dist/spare10.js
CHANGED
|
@@ -19,16 +19,16 @@ Examples:
|
|
|
19
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
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)}
|
|
21
21
|
|
|
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
|
|
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=
|
|
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
24
|
${Y(n,t)}
|
|
25
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=
|
|
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.
|
|
27
27
|
|
|
28
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.
|
|
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=
|
|
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}
|
|
31
31
|
|
|
32
32
|
${T}
|
|
33
|
-
|
|
34
|
-
`),process.exit(
|
|
33
|
+
`),process.exit(2)),process.stdout.write(`${T}
|
|
34
|
+
`),process.exit(0)),e}
|