@hydra-acp/budgeter 0.1.13 → 0.1.15

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.
@@ -1 +1 @@
1
- import{relative as j,resolve as A}from"node:path";import{homedir as O}from"node:os";import{realpathSync as N}from"node:fs";function Q(g){const c=g.match(/^(\d+)\s*([dhmwy])$/i);if(c!==null){const T=c[1],a=c[2];if(T===void 0||a===void 0)throw new Error(`invalid since spec: ${g}`);const k=parseInt(T,10),l=a.toLowerCase(),i=new Date;return l==="d"?i.setDate(i.getDate()-k):l==="h"?i.setHours(i.getHours()-k):l==="w"?i.setDate(i.getDate()-k*7):l==="m"?i.setMonth(i.getMonth()-k):l==="y"&&i.setFullYear(i.getFullYear()-k),i}const t=new Date(g);if(Number.isNaN(t.getTime()))throw new Error(`invalid since spec: ${g}`);return t}function H(g){const c=g.locByFiletype;if(c===void 0)return 0;let t=0;for(const T of Object.keys(c)){const a=c[T];a!==void 0&&(t+=a.added-a.removed)}return t}const B=new Map;function I(g){if(B.has(g))return B.get(g);try{const c=N(g);return B.set(g,c),c}catch{return B.set(g,g),g}}function $(g,c){let t=g;if(c.since!==void 0){const k=c.since.getTime(),l=[];for(const i of t)new Date(i.updatedAt).getTime()<k||l.push(i);t=l}if(c.dir!==void 0){const k=I(A(c.dir)),l=[];for(const i of t)i.cwd===void 0||i.cwd===""||(i.cwd.startsWith(k+"/")||i.cwd===k)&&l.push(i);t=l}if(c.host!==void 0&&c.host!=="all"){const k=c.host,l=[];for(const i of t)k==="local"?(!i.importedFromMachine||i.upstreamSessionId)&&l.push(i):i.importedFromMachine===k&&!i.upstreamSessionId&&l.push(i);t=l}if(c.interactive!==void 0){const k=c.interactive,l=[];for(const i of t)i.interactive===k&&l.push(i);t=l}const T=c.min??0,a=[];for(const k of t){let l;c.minMetric==="tokens"?l=k.contextTokens:c.minMetric==="loc"?l=H(k):l=k.costAmount,l>T&&a.push(k)}return a}function q(g){return g.dir!==void 0?A(g.dir):A(O())}function P(g,c,t){if(g===void 0||g==="")return"<unknown>";const T=I(A(g));if(c===void 0){const y=I(A(O()));return T===y?"~":T.startsWith(y+"/")?"~/"+T.slice(y.length+1):T}const a=I(A(t));let k=j(a,T);if(k===""||k===".")return"<root>";if(k.startsWith(".."))return"<unknown>";const l=k.split("/");if(l.length===0)return"<root>";const i=Math.min(c,l.length),C=l.slice(0,i);return C.length===0?"<root>":C.join("/")}function U(g,c,t={},T){let a=t.since;if(a===void 0&&t.bucket!==void 0){const e=new Date;t.bucket==="hour"?(e.setHours(e.getHours()-24),a=e):t.bucket==="day"?(e.setDate(e.getDate()-30),a=e):t.bucket==="week"?(e.setMonth(e.getMonth()-6),a=e):t.bucket==="month"&&(e.setFullYear(e.getFullYear()-2),a=e)}if(!(a!==void 0||t.bucket!==void 0||t.tokens===!0||t.loc===!0||t.by!==void 0||t.dir!==void 0||t.interactive!==void 0)){let e=0;for(const r of g)e+=r.costAmount;let o="";for(const r of g){o=r.costCurrency;break}return{kind:"total",row:{label:"All sessions",costAmount:e,sessionCount:g.length},currency:o}}const l={since:a,dir:t.dir,interactive:t.interactive,min:t.min,minMetric:t.loc===!0?"loc":t.tokens===!0?"tokens":"cost"},i=$(g,l),C=new Map;if(c!==void 0)for(const e of c){const o=C.get(e.sessionId);o===void 0?C.set(e.sessionId,[e]):o.push(e)}let y="";if(c!==void 0&&c.length>0)for(const e of c){y=e.currency;break}else if(i.length>0)for(const e of i){y=e.costCurrency;break}if(t.by==="filetype"&&t.bucket===void 0){const e=new Map,o=new Set;for(const u of i){const n=u.locByFiletype;if(n===void 0)continue;let f=!1;for(const d of Object.keys(n)){const w=n[d];if(w===void 0)continue;let h=e.get(d);h===void 0&&(h={added:0,removed:0,sessions:new Set},e.set(d,h)),h.added+=w.added,h.removed+=w.removed,(w.added>0||w.removed>0)&&(h.sessions.add(u.sessionId),f=!0)}f&&o.add(u.sessionId)}const r=[];for(const[u,n]of e){const f={label:u,costAmount:0,sessionCount:n.sessions.size,linesAdded:n.added,linesRemoved:n.removed};r.push({label:u,items:[f],sessionCount:n.sessions.size})}return{kind:"grouped",groups:r,currency:y,totalSessions:o.size}}const D=e=>t.by==="dir"?P(e.cwd,t.depth,q(t)):t.by==="session"?e.sessionId.startsWith("hydra_session_")?e.sessionId.slice(14):e.sessionId:t.by==="model"?e.model===""?"<unknown>":e.model:t.by==="agent"?e.agentId===""?"<unknown>":e.agentId:"<all>",M=e=>{const o=new Date(e);if(Number.isNaN(o.getTime()))return"<invalid>";if(t.bucket==="hour"){const r=o.toLocaleDateString(void 0,{month:"2-digit",day:"2-digit"}),u=String(o.getHours()).padStart(2,"0");return`${r} ${u}:00`}if(t.bucket==="day")return o.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"});if(t.bucket==="week"){const r=new Date(o),u=r.getDay(),n=r.getDate()-u+(u===0?-6:1);return r.setDate(n),r.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"})}return t.bucket==="month"?o.toLocaleDateString(void 0,{year:"numeric",month:"2-digit"}):o.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"})};if(t.by===void 0&&t.bucket===void 0){let e=0,o=0,r=0,u=0,n=0,f=0;for(const w of i){e+=w.costAmount;const h=C.get(w.sessionId);if(h!==void 0)for(const s of h)o+=s.deltaCost,t.tokens===!0&&(s.inputTokens!==void 0&&(r+=s.inputTokens),s.outputTokens!==void 0&&(u+=s.outputTokens),s.cacheReadTokens!==void 0&&(n+=s.cacheReadTokens),s.cacheWriteTokens!==void 0&&(f+=s.cacheWriteTokens))}const d={label:"All sessions",costAmount:e,deltaCost:o,sessionCount:i.length};if(t.tokens===!0&&(d.inputTokens=r,d.outputTokens=u,d.cacheReadTokens=n,d.cacheWriteTokens=f),t.loc===!0){let w=0,h=0;for(const s of i){const m=s.locByFiletype;if(m!==void 0)for(const p of Object.keys(m)){const b=m[p];b!==void 0&&(w+=b.added,h+=b.removed)}}d.linesAdded=w,d.linesRemoved=h}return{kind:"total",row:d,currency:y}}if(t.by!==void 0&&t.bucket===void 0){const e=new Map;for(const r of i){const u=D(r);let n=e.get(u);if(n===void 0&&(n={label:u,rows:{label:u,costAmount:0,deltaCost:0,sessionCount:0}},e.set(u,n)),n.rows.costAmount+=r.costAmount,n.rows.sessionCount=(n.rows.sessionCount??0)+1,t.tokens===!0&&(n.rows.inputTokens===void 0&&(n.rows.inputTokens=0),n.rows.inputTokens+=r.contextTokens),t.loc===!0){const d=r.locByFiletype;if(d!==void 0){n.rows.linesAdded===void 0&&(n.rows.linesAdded=0),n.rows.linesRemoved===void 0&&(n.rows.linesRemoved=0);for(const w of Object.keys(d)){const h=d[w];h!==void 0&&(n.rows.linesAdded+=h.added,n.rows.linesRemoved+=h.removed)}}}const f=C.get(r.sessionId);if(f!==void 0)for(const d of f)n.rows.deltaCost+=d.deltaCost,t.tokens===!0&&(n.rows.inputTokens===void 0&&(n.rows.inputTokens=0),n.rows.outputTokens===void 0&&(n.rows.outputTokens=0),n.rows.cacheReadTokens===void 0&&(n.rows.cacheReadTokens=0),n.rows.cacheWriteTokens===void 0&&(n.rows.cacheWriteTokens=0),d.inputTokens!==void 0&&(n.rows.inputTokens+=d.inputTokens),d.outputTokens!==void 0&&(n.rows.outputTokens+=d.outputTokens),d.cacheReadTokens!==void 0&&(n.rows.cacheReadTokens+=d.cacheReadTokens),d.cacheWriteTokens!==void 0&&(n.rows.cacheWriteTokens+=d.cacheWriteTokens))}const o=[];for(const r of e.values())o.push({label:r.label,items:[r.rows],sessionCount:r.rows.sessionCount});return{kind:"grouped",groups:o,currency:y}}const _=new Map;for(const e of i)_.set(e.sessionId,e);const R=new Map;if(c!==void 0){for(const e of c){if(!_.has(e.sessionId))continue;const o=R.get(e.sessionId);o===void 0?R.set(e.sessionId,[e]):o.push(e)}for(const e of R.values())e.sort((o,r)=>o.ts.localeCompare(r.ts))}const L=(e,o)=>{e._sessions===void 0&&(e._sessions=new Set),e._sessions.add(o),e.sessionCount=e._sessions.size},F=e=>{delete e._sessions},G=(e,o,r)=>{const u=M(o.ts);let n=e.get(u);n===void 0&&(n={bucket:u,costAmount:0,deltaCost:0,sessionCount:0},e.set(u,n)),n.costAmount+=r,n.deltaCost+=r,L(n,o.sessionId),t.tokens===!0&&(n.inputTokens===void 0||(o.inputTokens??0)>n.inputTokens)&&(n.inputTokens=o.inputTokens??n.inputTokens??0)};if(t.loc===!0&&t.bucket!==void 0){const e=new Set;for(const s of i)e.add(s.sessionId);const o=new Map;for(const s of i)o.set(s.sessionId,s);const r=(s,m)=>{let p=s.get(m);return p===void 0&&(p={bucket:m,costAmount:0,deltaCost:0,sessionCount:0,linesAdded:0,linesRemoved:0},s.set(m,p)),(p.linesAdded??void 0)===void 0&&(p.linesAdded=0),(p.linesRemoved??void 0)===void 0&&(p.linesRemoved=0),p},u=(s,m)=>{s.linesAdded=(s.linesAdded??0)+m.linesAdded,s.linesRemoved=(s.linesRemoved??0)+m.linesRemoved,s._sessions===void 0&&(s._sessions=new Set),s._sessions.add(m.sessionId),s.sessionCount=s._sessions.size},n=s=>{delete s._sessions},f=T??[];if(t.by==="filetype"){const s=new Map,m=new Set;for(const b of f){if(!e.has(b.sessionId)||a!==void 0&&new Date(b.ts)<a)continue;let v=s.get(b.filetype);v===void 0&&(v={label:b.filetype,buckets:new Map,sessions:new Set},s.set(b.filetype,v));const S=r(v.buckets,M(b.ts));u(S,b),v.sessions.add(b.sessionId),m.add(b.sessionId)}const p=[];for(const b of s.values()){const v=Array.from(b.buckets.values());for(const S of v)n(S);v.sort((S,E)=>S.bucket.localeCompare(E.bucket)),v.length>0&&p.push({label:b.label,items:v,sessionCount:b.sessions.size})}return{kind:"timeSeriesGrouped",groups:p,currency:y,totalSessions:m.size}}if(t.by!==void 0){const s=new Map;for(const p of f){if(!e.has(p.sessionId)||a!==void 0&&new Date(p.ts)<a)continue;const b=o.get(p.sessionId);if(b===void 0)continue;const v=D(b);let S=s.get(v);S===void 0&&(S={label:v,buckets:new Map,sessions:new Set},s.set(v,S));const E=r(S.buckets,M(p.ts));u(E,p),S.sessions.add(p.sessionId)}const m=[];for(const p of s.values()){const b=Array.from(p.buckets.values());for(const v of b)n(v);b.sort((v,S)=>v.bucket.localeCompare(S.bucket)),b.length>0&&m.push({label:p.label,items:b,sessionCount:p.sessions.size})}return{kind:"timeSeriesGrouped",groups:m,currency:y}}const d=new Map,w=new Set;for(const s of f){if(!e.has(s.sessionId)||a!==void 0&&new Date(s.ts)<a)continue;const m=r(d,M(s.ts));u(m,s),w.add(s.sessionId)}const h=Array.from(d.values());for(const s of h)n(s);return h.sort((s,m)=>s.bucket.localeCompare(m.bucket)),{kind:"timeSeries",timeSeries:h,currency:y,totalSessions:w.size}}if(t.by!==void 0&&t.bucket!==void 0){const e=new Map,o=u=>{const n=D(u);let f=e.get(n);return f===void 0&&(f={label:n,buckets:new Map,sessions:new Set},e.set(n,f)),f};for(const u of i){const n=R.get(u.sessionId);if(n===void 0||n.length===0)continue;const f=o(u),d=f.buckets;f.sessions.add(u.sessionId);const w=n[0];if(w===void 0)continue;let h=w.cumulativeCost;for(let s=1;s<n.length;s++){const m=n[s];if(m===void 0)continue;const p=Math.max(0,m.cumulativeCost-h);h=m.cumulativeCost,!(a!==void 0&&new Date(m.ts)<a)&&G(d,m,p)}}const r=[];for(const u of e.values()){const n=Array.from(u.buckets.values());for(const f of n)F(f);n.sort((f,d)=>f.bucket.localeCompare(d.bucket)),n.length>0&&r.push({label:u.label,items:n,sessionCount:u.sessions.size})}return{kind:"timeSeriesGrouped",groups:r,currency:y}}const W=new Map,z=new Set;for(const e of i){const o=R.get(e.sessionId);if(o===void 0||o.length===0)continue;const r=o[0];if(r===void 0)continue;let u=r.cumulativeCost,n=!1;for(let f=1;f<o.length;f++){const d=o[f];if(d===void 0)continue;const w=Math.max(0,d.cumulativeCost-u);u=d.cumulativeCost,!(a!==void 0&&new Date(d.ts)<a)&&(G(W,d,w),n=!0)}n&&z.add(e.sessionId)}const x=Array.from(W.values());for(const e of x)F(e);return x.sort((e,o)=>e.bucket.localeCompare(o.bucket)),{kind:"timeSeries",timeSeries:x,currency:y,totalSessions:z.size}}export{U as aggregate,$ as applyFilters,Q as parseSince};
1
+ import{relative as H,resolve as A}from"node:path";import{homedir as L}from"node:os";import{realpathSync as $}from"node:fs";function V(l){const c=l.match(/^(\d+)\s*([dhmwy])$/i);if(c!==null){const T=c[1],f=c[2];if(T===void 0||f===void 0)throw new Error(`invalid since spec: ${l}`);const m=parseInt(T,10),a=f.toLowerCase(),r=new Date;return a==="d"?r.setDate(r.getDate()-m):a==="h"?r.setHours(r.getHours()-m):a==="w"?r.setDate(r.getDate()-m*7):a==="m"?r.setMonth(r.getMonth()-m):a==="y"&&r.setFullYear(r.getFullYear()-m),r}const t=new Date(l);if(Number.isNaN(t.getTime()))throw new Error(`invalid since spec: ${l}`);return t}function q(l){const c=l.locByFiletype;if(c===void 0)return 0;let t=0;for(const T of Object.keys(c)){const f=c[T];f!==void 0&&(t+=f.added-f.removed)}return t}const I=new Map;function D(l){if(I.has(l))return I.get(l);try{const c=$(l);return I.set(l,c),c}catch{return I.set(l,l),l}}function P(l,c){let t=l;if(c.since!==void 0){const m=c.since.getTime(),a=[];for(const r of t)new Date(r.updatedAt).getTime()<m||a.push(r);t=a}if(c.dir!==void 0){const m=D(A(c.dir)),a=[];for(const r of t)r.cwd===void 0||r.cwd===""||(r.cwd.startsWith(m+"/")||r.cwd===m)&&a.push(r);t=a}if(c.host!==void 0&&c.host!=="all"){const m=c.host,a=[];for(const r of t)m==="local"?(!r.importedFromMachine||r.upstreamSessionId)&&a.push(r):r.importedFromMachine===m&&!r.upstreamSessionId&&a.push(r);t=a}if(c.interactive!==void 0){const m=c.interactive,a=[];for(const r of t)r.interactive===m&&a.push(r);t=a}const T=c.min??0,f=[];for(const m of t){let a;c.minMetric==="tokens"?a=m.contextTokens:c.minMetric==="loc"?a=q(m):a=m.costAmount,a>T&&f.push(m)}return f}function Y(l){return l.dir!==void 0?A(l.dir):A(L())}function K(l,c,t){if(l===void 0||l==="")return"<unknown>";const T=D(A(l));if(c===void 0){const v=D(A(L()));return T===v?"~":T.startsWith(v+"/")?"~/"+T.slice(v.length+1):T}const f=D(A(t));let m=H(f,T);if(m===""||m===".")return"<root>";if(m.startsWith(".."))return"<unknown>";const a=m.split("/");if(a.length===0)return"<root>";const r=Math.min(c,a.length),S=a.slice(0,r);return S.length===0?"<root>":S.join("/")}function X(l,c,t={},T){let f=t.since;if(f===void 0&&t.bucket!==void 0){const e=new Date;t.bucket==="hour"?(e.setHours(e.getHours()-24),f=e):t.bucket==="day"||t.bucket==="weekday"?(e.setDate(e.getDate()-30),f=e):t.bucket==="week"?(e.setMonth(e.getMonth()-6),f=e):t.bucket==="month"&&(e.setFullYear(e.getFullYear()-2),f=e)}if(!(f!==void 0||t.bucket!==void 0||t.tokens===!0||t.loc===!0||t.by!==void 0||t.dir!==void 0||t.interactive!==void 0)){let e=0;for(const o of l)e+=o.costAmount;let s="";for(const o of l){s=o.costCurrency;break}return{kind:"total",row:{label:"All sessions",costAmount:e,sessionCount:l.length},currency:s}}const a={since:f,dir:t.dir,interactive:t.interactive,min:t.min,minMetric:t.loc===!0?"loc":t.tokens===!0?"tokens":"cost"},r=P(l,a),S=new Map;if(c!==void 0)for(const e of c){const s=S.get(e.sessionId);s===void 0?S.set(e.sessionId,[e]):s.push(e)}let v="";if(c!==void 0&&c.length>0)for(const e of c){v=e.currency;break}else if(r.length>0)for(const e of r){v=e.costCurrency;break}if(t.by==="filetype"&&t.bucket===void 0){const e=new Map,s=new Set;for(const u of r){const n=u.locByFiletype;if(n===void 0)continue;let g=!1;for(const d of Object.keys(n)){const h=n[d];if(h===void 0)continue;let w=e.get(d);w===void 0&&(w={added:0,removed:0,sessions:new Set},e.set(d,w)),w.added+=h.added,w.removed+=h.removed,(h.added>0||h.removed>0)&&(w.sessions.add(u.sessionId),g=!0)}g&&s.add(u.sessionId)}const o=[];for(const[u,n]of e){const g={label:u,costAmount:0,sessionCount:n.sessions.size,linesAdded:n.added,linesRemoved:n.removed};o.push({label:u,items:[g],sessionCount:n.sessions.size})}return{kind:"grouped",groups:o,currency:v,totalSessions:s.size}}const x=e=>t.by==="dir"?K(e.cwd,t.depth,Y(t)):t.by==="session"?e.sessionId.startsWith("hydra_session_")?e.sessionId.slice(14):e.sessionId:t.by==="model"?e.model===""?"<unknown>":e.model:t.by==="agent"?e.agentId===""?"<unknown>":e.agentId:"<all>",B=e=>{const s=new Date(e);if(Number.isNaN(s.getTime()))return"<invalid>";if(t.bucket==="hour"){const o=s.toLocaleDateString(void 0,{month:"2-digit",day:"2-digit"}),u=String(s.getHours()).padStart(2,"0");return`${o} ${u}:00`}if(t.bucket==="day")return s.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"});if(t.bucket==="weekday")return s.toLocaleDateString(void 0,{weekday:"short"});if(t.bucket==="week"){const o=new Date(s),u=o.getDay(),n=o.getDate()-u+(u===0?-6:1);return o.setDate(n),o.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"})}return t.bucket==="month"?s.toLocaleDateString(void 0,{year:"numeric",month:"2-digit"}):s.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"})},E=new Map;if(t.bucket==="weekday"){const e=new Date(2024,0,1);for(let s=0;s<7;s++){const o=new Date(e);o.setDate(e.getDate()+s),E.set(o.toLocaleDateString(void 0,{weekday:"short"}),s)}}const R=(e,s)=>{if(t.bucket==="weekday"){const o=E.get(e.bucket)??7,u=E.get(s.bucket)??7;if(o!==u)return o-u}return e.bucket.localeCompare(s.bucket)};if(t.by===void 0&&t.bucket===void 0){let e=0,s=0,o=0,u=0,n=0,g=0;for(const h of r){e+=h.costAmount;const w=S.get(h.sessionId);if(w!==void 0)for(const i of w)s+=i.deltaCost,t.tokens===!0&&(i.inputTokens!==void 0&&(o+=i.inputTokens),i.outputTokens!==void 0&&(u+=i.outputTokens),i.cacheReadTokens!==void 0&&(n+=i.cacheReadTokens),i.cacheWriteTokens!==void 0&&(g+=i.cacheWriteTokens))}const d={label:"All sessions",costAmount:e,deltaCost:s,sessionCount:r.length};if(t.tokens===!0&&(d.inputTokens=o,d.outputTokens=u,d.cacheReadTokens=n,d.cacheWriteTokens=g),t.loc===!0){let h=0,w=0;for(const i of r){const b=i.locByFiletype;if(b!==void 0)for(const k of Object.keys(b)){const p=b[k];p!==void 0&&(h+=p.added,w+=p.removed)}}d.linesAdded=h,d.linesRemoved=w}return{kind:"total",row:d,currency:v}}if(t.by!==void 0&&t.bucket===void 0){const e=new Map;for(const o of r){const u=x(o);let n=e.get(u);if(n===void 0&&(n={label:u,rows:{label:u,costAmount:0,deltaCost:0,sessionCount:0}},e.set(u,n)),n.rows.costAmount+=o.costAmount,n.rows.sessionCount=(n.rows.sessionCount??0)+1,t.tokens===!0&&(n.rows.inputTokens===void 0&&(n.rows.inputTokens=0),n.rows.inputTokens+=o.contextTokens),t.loc===!0){const d=o.locByFiletype;if(d!==void 0){n.rows.linesAdded===void 0&&(n.rows.linesAdded=0),n.rows.linesRemoved===void 0&&(n.rows.linesRemoved=0);for(const h of Object.keys(d)){const w=d[h];w!==void 0&&(n.rows.linesAdded+=w.added,n.rows.linesRemoved+=w.removed)}}}const g=S.get(o.sessionId);if(g!==void 0)for(const d of g)n.rows.deltaCost+=d.deltaCost,t.tokens===!0&&(n.rows.inputTokens===void 0&&(n.rows.inputTokens=0),n.rows.outputTokens===void 0&&(n.rows.outputTokens=0),n.rows.cacheReadTokens===void 0&&(n.rows.cacheReadTokens=0),n.rows.cacheWriteTokens===void 0&&(n.rows.cacheWriteTokens=0),d.inputTokens!==void 0&&(n.rows.inputTokens+=d.inputTokens),d.outputTokens!==void 0&&(n.rows.outputTokens+=d.outputTokens),d.cacheReadTokens!==void 0&&(n.rows.cacheReadTokens+=d.cacheReadTokens),d.cacheWriteTokens!==void 0&&(n.rows.cacheWriteTokens+=d.cacheWriteTokens))}const s=[];for(const o of e.values())s.push({label:o.label,items:[o.rows],sessionCount:o.rows.sessionCount});return{kind:"grouped",groups:s,currency:v}}const F=new Map;for(const e of r)F.set(e.sessionId,e);const M=new Map;if(c!==void 0){for(const e of c){if(!F.has(e.sessionId))continue;const s=M.get(e.sessionId);s===void 0?M.set(e.sessionId,[e]):s.push(e)}for(const e of M.values())e.sort((s,o)=>s.ts.localeCompare(o.ts))}const j=(e,s)=>{e._sessions===void 0&&(e._sessions=new Set),e._sessions.add(s),e.sessionCount=e._sessions.size},G=e=>{delete e._sessions},W=(e,s,o)=>{const u=B(s.ts);let n=e.get(u);n===void 0&&(n={bucket:u,costAmount:0,deltaCost:0,sessionCount:0},e.set(u,n)),n.costAmount+=o,n.deltaCost+=o,j(n,s.sessionId),t.tokens===!0&&(n.inputTokens===void 0||(s.inputTokens??0)>n.inputTokens)&&(n.inputTokens=s.inputTokens??n.inputTokens??0)};if(t.loc===!0&&t.bucket!==void 0){const e=new Set;for(const i of r)e.add(i.sessionId);const s=new Map;for(const i of r)s.set(i.sessionId,i);const o=(i,b)=>{let k=i.get(b);return k===void 0&&(k={bucket:b,costAmount:0,deltaCost:0,sessionCount:0,linesAdded:0,linesRemoved:0},i.set(b,k)),(k.linesAdded??void 0)===void 0&&(k.linesAdded=0),(k.linesRemoved??void 0)===void 0&&(k.linesRemoved=0),k},u=(i,b)=>{i.linesAdded=(i.linesAdded??0)+b.linesAdded,i.linesRemoved=(i.linesRemoved??0)+b.linesRemoved,i._sessions===void 0&&(i._sessions=new Set),i._sessions.add(b.sessionId),i.sessionCount=i._sessions.size},n=i=>{delete i._sessions},g=T??[];if(t.by==="filetype"){const i=new Map,b=new Set;for(const p of g){if(!e.has(p.sessionId)||f!==void 0&&new Date(p.ts)<f)continue;let y=i.get(p.filetype);y===void 0&&(y={label:p.filetype,buckets:new Map,sessions:new Set},i.set(p.filetype,y));const C=o(y.buckets,B(p.ts));u(C,p),y.sessions.add(p.sessionId),b.add(p.sessionId)}const k=[];for(const p of i.values()){const y=Array.from(p.buckets.values());for(const C of y)n(C);y.sort(R),y.length>0&&k.push({label:p.label,items:y,sessionCount:p.sessions.size})}return{kind:"timeSeriesGrouped",groups:k,currency:v,totalSessions:b.size}}if(t.by!==void 0){const i=new Map;for(const k of g){if(!e.has(k.sessionId)||f!==void 0&&new Date(k.ts)<f)continue;const p=s.get(k.sessionId);if(p===void 0)continue;const y=x(p);let C=i.get(y);C===void 0&&(C={label:y,buckets:new Map,sessions:new Set},i.set(y,C));const N=o(C.buckets,B(k.ts));u(N,k),C.sessions.add(k.sessionId)}const b=[];for(const k of i.values()){const p=Array.from(k.buckets.values());for(const y of p)n(y);p.sort(R),p.length>0&&b.push({label:k.label,items:p,sessionCount:k.sessions.size})}return{kind:"timeSeriesGrouped",groups:b,currency:v}}const d=new Map,h=new Set;for(const i of g){if(!e.has(i.sessionId)||f!==void 0&&new Date(i.ts)<f)continue;const b=o(d,B(i.ts));u(b,i),h.add(i.sessionId)}const w=Array.from(d.values());for(const i of w)n(i);return w.sort(R),{kind:"timeSeries",timeSeries:w,currency:v,totalSessions:h.size}}if(t.by!==void 0&&t.bucket!==void 0){const e=new Map,s=u=>{const n=x(u);let g=e.get(n);return g===void 0&&(g={label:n,buckets:new Map,sessions:new Set},e.set(n,g)),g};for(const u of r){const n=M.get(u.sessionId);if(n===void 0||n.length===0)continue;const g=s(u),d=g.buckets;g.sessions.add(u.sessionId);const h=n[0];if(h===void 0)continue;let w=h.cumulativeCost;for(let i=1;i<n.length;i++){const b=n[i];if(b===void 0)continue;const k=Math.max(0,b.cumulativeCost-w);w=b.cumulativeCost,!(f!==void 0&&new Date(b.ts)<f)&&W(d,b,k)}}const o=[];for(const u of e.values()){const n=Array.from(u.buckets.values());for(const g of n)G(g);n.sort(R),n.length>0&&o.push({label:u.label,items:n,sessionCount:u.sessions.size})}return{kind:"timeSeriesGrouped",groups:o,currency:v}}const O=new Map,z=new Set;for(const e of r){const s=M.get(e.sessionId);if(s===void 0||s.length===0)continue;const o=s[0];if(o===void 0)continue;let u=o.cumulativeCost,n=!1;for(let g=1;g<s.length;g++){const d=s[g];if(d===void 0)continue;const h=Math.max(0,d.cumulativeCost-u);u=d.cumulativeCost,!(f!==void 0&&new Date(d.ts)<f)&&(W(O,d,h),n=!0)}n&&z.add(e.sessionId)}const _=Array.from(O.values());for(const e of _)G(e);return _.sort(R),{kind:"timeSeries",timeSeries:_,currency:v,totalSessions:z.size}}export{X as aggregate,P as applyFilters,V as parseSince};
@@ -0,0 +1 @@
1
+ import{createReadStream as p,existsSync as l,readdirSync as m}from"node:fs";import{createGunzip as y}from"node:zlib";import{createInterface as g}from"node:readline";import{resolve as f}from"node:path";const u=/^history\.jsonl\.(\d+)(\.gz)?$/;function h(e){let r;try{r=m(e)}catch{return[]}const n=new Map;for(const t of r){const i=u.exec(t);if(!i)continue;const c=Number(i[1]),d=i[2]!==void 0,a=n.get(c);(a===void 0||d&&!a.endsWith(".gz"))&&n.set(c,t)}const s=[...n.keys()].sort((t,i)=>t-i).map(t=>f(e,n.get(t))),o=f(e,"history.jsonl");return l(o)&&s.push(o),s}async function*v(e){for(const r of h(e)){const n=p(r),s=r.endsWith(".gz")?n.pipe(y()):n,o=g({input:s,crlfDelay:1/0});try{for await(const t of o)t.length>0&&(yield t)}catch{}finally{o.close(),n.destroy()}}}export{h as transcriptFiles,v as transcriptLines};
@@ -1 +1 @@
1
- import{createInterface as j}from"node:readline";import{createReadStream as S,statSync as x}from"node:fs";import{resolve as N}from"node:path";import{logger as J}from"../util/log.js";import{sessionsDir as _}from"./session-store.js";import{filetypeForPath as M}from"./language.js";import{fetchToolBlob as P}from"./daemon-client.js";const I=J("cost/history-stream");function B(t){if(!t||typeof t!="object"||Array.isArray(t))return;const n=t["hydra-acp"];if(!n||typeof n!="object"||Array.isArray(n))return;const e=n.cumulativeCost;return typeof e=="number"?e:void 0}function L(t){const n=B(t._meta),e=t.cost??void 0,i=n!==void 0?n:typeof e?.amount=="number"?e.amount:void 0;if(i===void 0)return;const a=typeof e?.currency=="string"?e.currency:void 0;return{amount:i,currency:a}}function $(t){const n=t.recordedAt;return typeof n=="number"?new Date(n).toISOString():typeof n=="string"?n:""}async function*K(t){const n=Array.isArray(t)?t:[t];for(const e of n){const i=N(_(),e.sessionId,"history.jsonl");try{x(i)}catch(f){const o=f;if(o.code==="ENOENT"){I.debug(`no history for ${e.sessionId}`);continue}I.debug(`stat failed for ${i}: ${o.message}`);continue}const a=S(i,{encoding:"utf8"}),b=j({input:a,crlfDelay:1/0}),m=new Map;try{for await(const f of b){if(f.length===0)continue;let o;try{o=JSON.parse(f)}catch{I.debug(`malformed JSON in history.jsonl for ${e.sessionId}`);continue}if(!o||typeof o!="object"||Array.isArray(o))continue;const r=o;if(r.method!=="session/update")continue;const d=r.params??void 0;if(!d||typeof d!="object"||Array.isArray(d))continue;const c=d.update??void 0;if(!c||typeof c!="object"||Array.isArray(c)||c.sessionUpdate!=="usage_update")continue;const u=$(r),y=L(c);if(y===void 0)continue;const w=y.currency??"",h=y.amount,g=m.get(e.sessionId)??0,p=Math.max(0,h-g);m.set(e.sessionId,h);const s=c.usage??void 0;let A,k,l,R;if(s&&typeof s=="object"&&!Array.isArray(s)){const T=s.inputTokens;typeof T=="number"&&(A=T);const v=s.outputTokens;typeof v=="number"&&(k=v);const E=s.cacheReadInputTokens;typeof E=="number"&&(l=E);const C=s.cacheCreationInputTokens;typeof C=="number"&&(R=C)}yield{sessionId:e.sessionId,ts:u,deltaCost:p,cumulativeCost:h,currency:w,...A!==void 0&&{inputTokens:A},...k!==void 0&&{outputTokens:k},...l!==void 0&&{cacheReadTokens:l},...R!==void 0&&{cacheWriteTokens:R}}}}finally{b.close(),a.destroy()}}}async function O(t,n){if(typeof n=="string")return n;if(n&&typeof n=="object"&&!Array.isArray(n)){const e=n.__hydraBlob;if(typeof e=="string")return await P(t,e)??""}return""}function D(t){if(t.length===0)return 0;let n=1;for(let e=0;e<t.length;e++)t.charCodeAt(e)===10&&n++;return t.charCodeAt(t.length-1)===10&&n--,n}async function*Q(t){const n=Array.isArray(t)?t:[t];for(const e of n){const i=N(_(),e.sessionId,"history.jsonl");try{x(i)}catch(o){const r=o;if(r.code==="ENOENT")continue;I.debug(`stat failed for ${i}: ${r.message}`);continue}const a=S(i,{encoding:"utf8"}),b=j({input:a,crlfDelay:1/0}),m=new Map,f=[];try{for await(const o of b){if(o.length===0)continue;let r;try{r=JSON.parse(o)}catch{continue}if(!r||typeof r!="object"||Array.isArray(r))continue;const d=r;if(d.method!=="session/update")continue;const c=d.params??void 0;if(!c||typeof c!="object"||Array.isArray(c))continue;const u=c.update??void 0;if(!u||typeof u!="object"||Array.isArray(u)||u.sessionUpdate!=="tool_call"&&u.sessionUpdate!=="tool_call_update")continue;const y=u.content;if(!Array.isArray(y)||y.length===0)continue;const w=typeof u.toolCallId=="string"?u.toolCallId:"";if(w==="")continue;const h=$(d);for(const g of y){if(!g||typeof g!="object"||Array.isArray(g))continue;const p=g;if(p.type!=="diff")continue;const s=typeof p.path=="string"?p.path:"";if(s==="")continue;const A=await O(e.sessionId,p.oldText),k=await O(e.sessionId,p.newText),l=`${w}|${s}`;m.has(l)||f.push(l),m.set(l,{sessionId:e.sessionId,ts:h,path:s,filetype:M(s),linesAdded:D(k),linesRemoved:D(A)})}}}finally{b.close(),a.destroy()}for(const o of f){const r=m.get(o);r!==void 0&&(yield r)}}}export{Q as streamHistoryEditEvents,K as streamHistoryEvents};
1
+ import{createInterface as D}from"node:readline";import{createReadStream as L,statSync as J}from"node:fs";import{resolve as j}from"node:path";import{transcriptLines as B}from"./history-files.js";import{logger as F}from"../util/log.js";import{sessionsDir as _}from"./session-store.js";import{filetypeForPath as M}from"./language.js";import{fetchToolBlob as P}from"./daemon-client.js";const v=F("cost/history-stream");function U(t){const n=t.cost??void 0;if(typeof n?.amount!="number")return;const e=typeof n.currency=="string"?n.currency:void 0;return{amount:n.amount,currency:e}}function $(t){const n=t.recordedAt;return typeof n=="number"?new Date(n).toISOString():typeof n=="string"?n:""}async function*X(t){const n=Array.isArray(t)?t:[t];for(const e of n){const y=j(_(),e.sessionId),h=new Map,p=e.costAmount>0?e.costAmount:void 0,A=1.5;let m=0,i=!1;for await(const o of B(y)){let c;try{c=JSON.parse(o)}catch{v.debug(`malformed JSON in history.jsonl for ${e.sessionId}`);continue}if(!c||typeof c!="object"||Array.isArray(c))continue;const a=c;if(a.method!=="session/update")continue;const s=a.params??void 0;if(!s||typeof s!="object"||Array.isArray(s))continue;const r=s.update??void 0;if(!r||typeof r!="object"||Array.isArray(r)||r.sessionUpdate!=="usage_update")continue;const w=$(a),k=U(r);if(k===void 0)continue;const l=k.currency??"",d=k.amount,g=h.get(e.sessionId)??0,b=Math.max(0,d-g);if(h.set(e.sessionId,d),p!==void 0&&m+b>p*A+1){v.debug(`${e.sessionId}: transcript series overshoots recorded lifetime (${(m+b).toFixed(2)} > ${p.toFixed(2)}); abandoning archive-derived events`),i=!0;break}m+=b;const u=r.usage??void 0;let f,I,R,T;if(u&&typeof u=="object"&&!Array.isArray(u)){const E=u.inputTokens;typeof E=="number"&&(f=E);const C=u.outputTokens;typeof C=="number"&&(I=C);const x=u.cacheReadInputTokens;typeof x=="number"&&(R=x);const S=u.cacheCreationInputTokens;typeof S=="number"&&(T=S)}yield{sessionId:e.sessionId,ts:w,deltaCost:b,cumulativeCost:d,currency:l,...f!==void 0&&{inputTokens:f},...I!==void 0&&{outputTokens:I},...R!==void 0&&{cacheReadTokens:R},...T!==void 0&&{cacheWriteTokens:T}}}}}async function N(t,n){if(typeof n=="string")return n;if(n&&typeof n=="object"&&!Array.isArray(n)){const e=n.__hydraBlob;if(typeof e=="string")return await P(t,e)??""}return""}function O(t){if(t.length===0)return 0;let n=1;for(let e=0;e<t.length;e++)t.charCodeAt(e)===10&&n++;return t.charCodeAt(t.length-1)===10&&n--,n}async function*Y(t){const n=Array.isArray(t)?t:[t];for(const e of n){const y=j(_(),e.sessionId,"history.jsonl");try{J(y)}catch(i){const o=i;if(o.code==="ENOENT")continue;v.debug(`stat failed for ${y}: ${o.message}`);continue}const h=L(y,{encoding:"utf8"}),p=D({input:h,crlfDelay:1/0}),A=new Map,m=[];try{for await(const i of p){if(i.length===0)continue;let o;try{o=JSON.parse(i)}catch{continue}if(!o||typeof o!="object"||Array.isArray(o))continue;const c=o;if(c.method!=="session/update")continue;const a=c.params??void 0;if(!a||typeof a!="object"||Array.isArray(a))continue;const s=a.update??void 0;if(!s||typeof s!="object"||Array.isArray(s)||s.sessionUpdate!=="tool_call"&&s.sessionUpdate!=="tool_call_update")continue;const r=s.content;if(!Array.isArray(r)||r.length===0)continue;const w=typeof s.toolCallId=="string"?s.toolCallId:"";if(w==="")continue;const k=$(c);for(const l of r){if(!l||typeof l!="object"||Array.isArray(l))continue;const d=l;if(d.type!=="diff")continue;const g=typeof d.path=="string"?d.path:"";if(g==="")continue;const b=await N(e.sessionId,d.oldText),u=await N(e.sessionId,d.newText),f=`${w}|${g}`;A.has(f)||m.push(f),A.set(f,{sessionId:e.sessionId,ts:k,path:g,filetype:M(g),linesAdded:O(u),linesRemoved:O(b)})}}}finally{p.close(),h.destroy()}for(const i of m){const o=A.get(i);o!==void 0&&(yield o)}}}export{Y as streamHistoryEditEvents,X as streamHistoryEvents};
@@ -0,0 +1,61 @@
1
+ import{writeFileSync as T,readFileSync as y,existsSync as x}from"node:fs";import{basename as S,resolve as c}from"node:path";import{homedir as O}from"node:os";import{buildOpencodeIndex as I,defaultOpencodeDbs as A,listSessionDirs as E,readGate as P,scanSession as R}from"./repair-io.js";import{planSession as j}from"./repair-core.js";import{applyPlan as N,revertSession as D,ALGO_VERSION as W}from"./repair-write.js";const C=`Usage: hydra-acp-budgeter repair [OPTIONS]
2
+
3
+ Reconcile recorded session cost against the agent's own ledger.
4
+
5
+ Sessions resurrected before the ledger-probe fix have inflated cost: the daemon
6
+ banked its displayed total, then a session-scoped agent re-reported the same
7
+ spend and the two were summed. This recomputes each recorded usage_update from
8
+ the agent's ledger, attributing work via tool-call ids.
9
+
10
+ Dry-run by default \u2014 prints what would change and writes nothing.
11
+
12
+ OPTIONS:
13
+ --apply Write the repair (requires the daemon to be stopped)
14
+ --revert Undo a previous repair, restoring the original values
15
+ --session <id> Limit to one session (repeatable)
16
+ --limit <n> Process at most n sessions, largest inflation first
17
+ --report <path> Write a JSON report of the plan
18
+ --json Emit the report to stdout instead of a table
19
+ --skip-warm Allow running with the daemon up, skipping live sessions
20
+ --force Skip the daemon-stopped check (not recommended)
21
+ -h, --help Show this help
22
+
23
+ SAFETY
24
+ * Original values are stamped into each rewritten row, write-once, so
25
+ --revert restores byte-for-byte.
26
+ * A repaired session is gated in meta.json and will not be repaired twice;
27
+ to redo it, --revert first.
28
+ * Sessions that cannot be proven are refused, never guessed at.
29
+ `;function J(i){const r={apply:!1,revert:!1,sessions:[],limit:void 0,report:void 0,json:!1,force:!1,skipWarm:!1};for(let s=0;s<i.length;s+=1){const n=i[s];if(n==="--apply")r.apply=!0;else if(n==="--revert")r.revert=!0;else if(n==="--json")r.json=!0;else if(n==="--force")r.force=!0;else if(n==="--skip-warm")r.skipWarm=!0;else if(n==="--session")r.sessions.push(i[++s]??"");else if(n==="--limit")r.limit=Number(i[++s]);else if(n==="--report")r.report=i[++s];else{if(n==="-h"||n==="--help")return;process.stderr.write(`repair: unknown option ${n}
30
+ `),process.exit(2)}}return r.apply&&r.revert&&(process.stderr.write(`repair: --apply and --revert are mutually exclusive
31
+ `),process.exit(2)),r}function _(i){const r=i??c(process.env.HYDRA_ACP_HOME??c(O(),".hydra-acp"),"daemon.pid");if(!x(r))return"down";let s;try{s=JSON.parse(y(r,"utf8")).pid}catch{return"unknown"}if(typeof s!="number")return"unknown";try{return process.kill(s,0),"up"}catch(n){return n.code==="ESRCH"?"down":"unknown"}}async function F(){const i=process.env.HYDRA_ACP_HOME??c(O(),".hydra-acp");let r,s;try{r=JSON.parse(y(c(i,"daemon.pid"),"utf8")).loopbackPort}catch{return}try{s=y(c(i,"auth-token"),"utf8").trim()}catch{return}if(!(typeof r!="number"||!s))try{const n=new AbortController,f=setTimeout(()=>n.abort(),3e3),m=await fetch(`http://127.0.0.1:${r}/v1/sessions?status=warm`,{headers:{Authorization:`Bearer ${s}`},signal:n.signal});if(clearTimeout(f),!m.ok)return;const u=await m.json();return Array.isArray(u.sessions)?new Set(u.sessions.map(d=>d.sessionId).filter(d=>!!d)):void 0}catch{return}}function l(i){return i.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2})}async function V(i){const r=J(i);if(r===void 0){process.stdout.write(C);return}let s=new Set;if(r.skipWarm){const e=await F();e===void 0&&(process.stderr.write(`repair: --skip-warm needs the daemon's warm-session list and could not get it.
32
+ Refusing rather than guessing which sessions are live.
33
+ `),process.exit(2)),s=e,process.stderr.write(`skipping ${s.size} warm session(s) held by the running daemon
34
+ `)}if(r.apply||r.revert){const e=_();e==="up"&&r.skipWarm&&!r.force||e!=="down"&&!r.force&&(process.stderr.write(e==="up"?`repair: the hydra daemon is running.
35
+ Stop it first (\`hydra daemon stop\`) \u2014 a live daemon overwrites meta.json
36
+ from stale in-memory state and undoes half of the repair.
37
+ Use --force only if you are certain no session is live.
38
+ `:`repair: cannot determine whether the hydra daemon is running.
39
+ Refusing rather than risking a write against a live daemon.
40
+ Stop the daemon and retry, or pass --force.
41
+ `),process.exit(2))}let n=E();if(r.sessions.length>0){const e=new Set(r.sessions);n=n.filter(t=>e.has(S(t))||[...e].some(o=>S(t).endsWith(o)))}if(r.revert){let e=0,t=0;for(const o of n){if(s.size>0&&s.has(S(o)))continue;const a=D(o);a&&(e+=1,t+=a.rowsReverted,process.stdout.write(`reverted ${S(o)} (${a.rowsReverted} rows)
42
+ `))}process.stdout.write(`
43
+ ${e} session(s) reverted, ${t} rows restored.
44
+ `);return}const f=A();f.length===0&&(process.stderr.write(`repair: no OpenCode database found; nothing to reconcile against.
45
+ `),process.exit(1)),process.stderr.write(`indexing ${f.length} OpenCode database(s)\u2026
46
+ `);const m=I(f),u=[],d=new Map;let w=0;for(const e of n){const t=R(e);if(t===void 0||s.has(t.input.sessionId))continue;if(P(t.meta)){w+=1;continue}const o=j(t.input,m.callIndex,m.ledger);if(!o.ok){const a=d.get(o.refusal.reason)??{n:0,reported:0};a.n+=1,a.reported+=t.input.reportedTotal,d.set(o.refusal.reason,a);continue}u.push({sessionId:t.input.sessionId,dir:e,reported:o.plan.reportedTotal,trueTotal:o.plan.trueTotal,rows:o.plan.rows.length,upstreams:o.plan.attribution.upstreams.length,plan:o.plan})}u.sort((e,t)=>t.reported-t.trueTotal-(e.reported-e.trueTotal));const p=r.limit===void 0?u:u.slice(0,r.limit),h=p.reduce((e,t)=>e+t.reported,0),g=p.reduce((e,t)=>e+t.trueTotal,0),$={tool:"hydra-acp-budgeter",algo:W,at:new Date().toISOString(),mode:r.apply?"apply":"dry-run",sessions:p.map(e=>({sessionId:e.sessionId,reported:e.reported,trueTotal:e.trueTotal,rows:e.rows,upstreams:e.plan.attribution.upstreams,forwardedSkipped:e.plan.attribution.forwardedSkipped})),totals:{reported:h,trueTotal:g,phantom:h-g},refused:Object.fromEntries([...d].map(([e,t])=>[e,t])),alreadyRepaired:w,warmSkipped:s.size};if(r.report!==void 0&&T(r.report,`${JSON.stringify($,null,2)}
47
+ `),r.json)process.stdout.write(`${JSON.stringify($,null,2)}
48
+ `);else{process.stdout.write(`
49
+ ${"session".padEnd(34)}${"rows".padStart(6)}${"ups".padStart(5)}${"reported".padStart(13)}${"true".padStart(12)}${"phantom".padStart(13)}
50
+ `);for(const e of p.slice(0,25))process.stdout.write(`${e.sessionId.slice(-32).padEnd(34)}${String(e.rows).padStart(6)}${String(e.upstreams).padStart(5)}${l(e.reported).padStart(13)}${l(e.trueTotal).padStart(12)}${l(e.reported-e.trueTotal).padStart(13)}
51
+ `);p.length>25&&process.stdout.write(`\u2026 and ${p.length-25} more
52
+ `),process.stdout.write(`
53
+ ${p.length} session(s) repairable: $${l(h)} recorded -> $${l(g)} actual ($${l(h-g)} phantom)
54
+ `),w>0&&process.stdout.write(`${w} session(s) already repaired (use --revert to redo)
55
+ `);for(const[e,t]of[...d].sort((o,a)=>a[1].reported-o[1].reported))process.stdout.write(`refused: ${e.padEnd(22)} ${String(t.n).padStart(4)} sessions $${l(t.reported)}
56
+ `)}if(!r.apply){const e=`
57
+ Dry run \u2014 nothing written. Re-run with --apply to commit.
58
+ `;r.json?process.stderr.write(e):process.stdout.write(e);return}let k=0,v=0;const b=[];for(const e of p){const t=R(e.dir);if(t!==void 0)try{const o=N(t,e.plan,H());k+=1,v+=o.rowsRewritten}catch(o){b.push(`${e.sessionId}: ${o.message}`)}}process.stdout.write(`
59
+ applied to ${k} session(s), ${v} rows rewritten.
60
+ `);for(const e of b)process.stderr.write(`FAILED ${e}
61
+ `);b.length>0&&(process.exitCode=1)}function H(){try{const i=c(import.meta.dirname,"../../package.json");return JSON.parse(y(i,"utf8")).version??"unknown"}catch{return"unknown"}}export{C as REPAIR_HELP,_ as daemonState,V as runRepair};
@@ -0,0 +1 @@
1
+ function p(e,d){const t=new Set;let n=0,o=0,r=0;for(const a of e){if(a.forwarded){r+=1;continue}const l=d.get(a.toolCallId);if(l===void 0){o+=1;continue}t.add(l),n+=1}return{upstreams:[...t].sort(),ownResolved:n,ownUnresolved:o,forwardedSkipped:r}}function y(e,d,t){let n=0;for(const o of d)for(const[r,a]of e.get(o)??[]){if(r>t)break;n+=a}return n}function c(e,d,t){const n=[];for(const l of d)for(const s of e.get(l)??[])n.push(s);n.sort((l,s)=>l[0]-s[0]);const o=[];let r=0,a=0;for(const l of t){for(;r<n.length&&n[r][0]<=l;)a+=n[r][1],r+=1;o.push(a)}return o}function g(e,d){const t=[];for(const r of e)if(r.repaired<-1e-6){t.push(`negative repaired value ${r.repaired} at ${r.file}:${r.line}`);break}for(let r=1;r<e.length;r+=1)if(e[r].repaired<e[r-1].repaired-1e-6){t.push(`repaired series decreases at ${e[r].file}:${e[r].line} (${e[r-1].repaired} -> ${e[r].repaired})`);break}for(let r=0;r<e.length;r+=1){const a=Math.max(e[r].amount,e[r+1]?.amount??e[r].amount);if(e[r].repaired>a+1e-6){t.push(`repaired ${e[r].repaired} exceeds recorded ${e[r].amount} at ${e[r].file}:${e[r].line}`);break}}const o=e[e.length-1];return o!==void 0&&o.repaired>d+1e-6&&t.push(`repaired final ${o.repaired} exceeds meta.json total ${d}`),{ok:t.length===0,violations:t}}function m(e,d,t){const n=(i,u,f)=>({ok:!1,refusal:{sessionId:e.sessionId,reason:i,detail:u,...f?{attribution:f}:{}}});if(e.reportedTotal<=0)return n("zero-cost","nothing to repair");if(e.forkedFromSessionId)return n("fork",`forked from ${e.forkedFromSessionId}`);const o=p(e.toolCalls,d);if(o.upstreams.length===0)return n("no-own-upstream",`no tool call resolves to an agent session (${o.ownUnresolved} unresolved, ${o.forwardedSkipped} forwarded)`,o);if(o.ownUnresolved>o.ownResolved)return n("majority-unresolved",`${o.ownUnresolved} of ${o.ownUnresolved+o.ownResolved} own tool ids unresolved \u2014 ledger partially purged, total would under-report`,o);const r=c(t,o.upstreams,e.usageRows.map(i=>i.recordedAt)),a=e.usageRows.map((i,u)=>({...i,repaired:r[u]??0})),l=g(a,e.reportedTotal);if(!l.ok)return n("guard-violation",l.violations.join("; "),o);const s=y(t,o.upstreams,Number.MAX_SAFE_INTEGER);return{ok:!0,plan:{sessionId:e.sessionId,attribution:o,rows:a,reportedTotal:e.reportedTotal,trueTotal:s}}}export{p as attribute,g as checkGuards,m as planSession,y as truthAt,c as truthSeries};
@@ -0,0 +1,2 @@
1
+ import{existsSync as f,readdirSync as S,readFileSync as x}from"node:fs";import{gunzipSync as w}from"node:zlib";import{homedir as b}from"node:os";import{resolve as d}from"node:path";import{DatabaseSync as R}from"node:sqlite";import{logger as A}from"../util/log.js";const u=A("cost/repair-io"),_="hydra-acp-budgeter",I="costRepair",p=/^history\.jsonl(\.(\d+)(\.gz)?)?$/;function z(){const t=process.env.OPENCODE_DATA_DIR??d(b(),".local/share/opencode");return["opencode.db","opencode-local.db"].map(e=>d(t,e)).filter(e=>f(e))}function j(t){const e=new Map,r=new Map;for(const o of t){let s;try{s=new R(o,{readOnly:!0})}catch(n){u.warn(`cannot open ${o}: ${n.message}`);continue}try{for(const n of s.prepare("select session_id, data from part where data like '%callID%'").all()){const a=n.data.indexOf('"callID":"');if(a<0)continue;const i=n.data.indexOf('"',a+10);i<0||e.set(n.data.slice(a+10,i),n.session_id)}for(const n of s.prepare("select session_id, data from message").all()){let a;try{a=JSON.parse(n.data)}catch{continue}if(a.role!=="assistant")continue;const i=a.time?.created;if(typeof i!="number")continue;const c=r.get(n.session_id)??[];c.push([i,a.cost??0]),r.set(n.session_id,c)}}catch(n){u.warn(`reading ${o} failed: ${n.message}`)}finally{s.close()}}for(const o of r.values())o.sort((s,n)=>s[0]-n[0]);return u.debug(`indexed ${e.size} tool calls, ${r.size} agent sessions`),{callIndex:e,ledger:r}}function E(t){const e=S(t).filter(o=>p.test(o));e.sort((o,s)=>{const n=o==="history.jsonl"?Number.MAX_SAFE_INTEGER:Number(p.exec(o)?.[2]??0),a=s==="history.jsonl"?Number.MAX_SAFE_INTEGER:Number(p.exec(s)?.[2]??0);return n-a});const r=[];for(const o of e)try{const s=x(d(t,o)),n=o.endsWith(".gz")?w(s).toString("utf8"):s.toString("utf8");r.push({name:o,gz:o.endsWith(".gz"),lines:n.split(`
2
+ `)})}catch(s){u.warn(`reading ${o} failed: ${s.message}`)}return r}function k(t){try{return JSON.parse(x(d(t,"meta.json"),"utf8"))}catch{return}}function C(t){const e=t.currentUsage??{},r=typeof e.costAmount=="number"?e.costAmount:0,o=typeof e.cumulativeCost=="number"?e.cumulativeCost:0;return r+o}function v(t){const r=t.extensionState?.[_]?.[I];return r&&typeof r=="object"?r:void 0}function U(t){const e=k(t);if(e===void 0)return;const r=E(t),o=[],s=[];for(const n of r)for(let a=0;a<n.lines.length;a+=1){const i=n.lines[a];if(i.length===0||!i.includes('"usage_update"')&&!i.includes('"toolCallId"'))continue;let c;try{c=JSON.parse(i)}catch{continue}const l=c.params?.update;if(!l)continue;const g=l._meta?.["hydra-acp"];if(l.sessionUpdate==="usage_update"){const h=l.cost,m=g?.[I]?.original,y=typeof m=="number"?m:h?.amount;typeof y=="number"&&typeof c.recordedAt=="number"&&o.push({file:n.name,line:a,recordedAt:c.recordedAt,amount:y});continue}typeof l.toolCallId=="string"&&s.push({toolCallId:l.toolCallId,forwarded:g?.planner!==void 0})}return o.sort((n,a)=>n.recordedAt-a.recordedAt),{dir:t,meta:e,files:r,input:{sessionId:e.sessionId,agentId:e.agentId,forkedFromSessionId:e.forkedFromSessionId,reportedTotal:C(e),usageRows:o,toolCalls:s}}}function O(){const t=process.env.HYDRA_ACP_HOME??d(b(),".hydra-acp");return d(t,"sessions")}function G(){const t=O();return f(t)?S(t).map(e=>d(t,e)).filter(e=>f(d(e,"meta.json"))):[]}export{p as ARCHIVE_RE,_ as BUDGETER_NS,I as REPAIR_KEY,j as buildOpencodeIndex,z as defaultOpencodeDbs,O as hydraSessionsDir,G as listSessionDirs,v as readGate,k as readMeta,E as readTranscriptFiles,C as reportedTotalOf,U as scanSession};
@@ -0,0 +1,4 @@
1
+ import{renameSync as x,writeFileSync as v,readFileSync as y,statSync as O,utimesSync as N}from"node:fs";import{gzipSync as E}from"node:zlib";import{resolve as p}from"node:path";import{logger as j}from"../util/log.js";import{BUDGETER_NS as c,REPAIR_KEY as l,readMeta as U,readTranscriptFiles as T}from"./repair-io.js";const $=j("cost/repair-write"),b=1,k=1;function J(e){return`${c}@${e}`}function _(e,i){const u=JSON.parse(e),n=u.params.update,o=n._meta??{},t=o["hydra-acp"]??{},r=n.cost??{},s=t[l],a=s&&typeof s.original=="number"?s.original:r.amount;return t[l]={v:k,algo:b,original:a},o["hydra-acp"]=t,n._meta=o,n.cost={...r,amount:i},JSON.stringify(u)}function A(e){const i=JSON.parse(e),u=i.params.update,n=u._meta,o=n?.["hydra-acp"],t=o?.[l];if(t)return typeof t.original=="number"&&(u.cost={...u.cost??{},amount:t.original}),delete o[l],Object.keys(o).length===0&&delete n["hydra-acp"],Object.keys(n).length===0&&delete u._meta,JSON.stringify(i)}function w(e,i){const u=`${e}.repair-tmp`;let n;try{const o=O(e);n={atime:o.atime,mtime:o.mtime}}catch{n=void 0}v(u,i,{mode:384}),x(u,e),n&&N(e,n.atime,n.mtime)}function h(e){const i=e.lines.join(`
2
+ `);return e.gz?E(Buffer.from(i,"utf8")):i}function B(e,i,u){const n=new Map;for(const r of i.rows){if(Math.abs(r.repaired-r.amount)<1e-9)continue;const s=n.get(r.file)??new Map;s.set(r.line,r.repaired),n.set(r.file,s)}const o=[];let t=0;for(const r of e.files){const s=n.get(r.name);if(!s||s.size===0)continue;const a=[...r.lines];for(const[d,f]of s){const g=a[d];if(g===void 0||g.length===0)throw new Error(`${r.name}:${d} missing while applying repair`);a[d]=_(g,f),t+=1}w(p(e.dir,r.name),h({...r,lines:a})),o.push(r.name)}return M(e,i,t,u),{filesRewritten:o,rowsRewritten:t}}function M(e,i,u,n){const o=p(e.dir,"meta.json"),t=JSON.parse(y(o,"utf8")),r=t.currentUsage??{},s={v:k,algo:b,tool:J(n),source:"opencode-ledger",method:"tool-call-join",upstreams:[...i.attribution.upstreams],rowsRewritten:u,originalCurrentUsage:t.currentUsage,hadExtensionState:t.extensionState!==void 0,reportedTotal:i.reportedTotal,trueTotal:i.trueTotal,at:new Date().toISOString()};t.currentUsage={...r,costAmount:i.trueTotal,cumulativeCost:void 0};const a=t.extensionState??{};a[c]={...a[c]??{},[l]:s},t.extensionState=a,w(o,`${JSON.stringify(t,null,2)}
3
+ `)}function I(e){const i=U(e);if(i===void 0)return;const n=i.extensionState?.[c]?.[l];if(!n)return;const o=[];let t=0;for(const d of T(e)){const f=[...d.lines];let g=!1;for(let m=0;m<f.length;m+=1){const R=f[m];if(R.length===0||!R.includes(l))continue;let S;try{S=A(R)}catch{continue}S!==void 0&&(f[m]=S,g=!0,t+=1)}g&&(w(p(e,d.name),h({...d,lines:f})),o.push(d.name))}const r=p(e,"meta.json"),s=JSON.parse(y(r,"utf8"));n.originalCurrentUsage===void 0?delete s.currentUsage:s.currentUsage=n.originalCurrentUsage;const a=s.extensionState??{};return a[c]&&(delete a[c][l],Object.keys(a[c]).length===0&&delete a[c]),Object.keys(a).length===0&&!n.hadExtensionState?delete s.extensionState:s.extensionState=a,w(r,`${JSON.stringify(s,null,2)}
4
+ `),$.debug(`reverted ${e}: ${t} rows`),{filesRewritten:o,rowsReverted:t}}export{b as ALGO_VERSION,k as MARKER_VERSION,B as applyPlan,A as revertLine,I as revertSession,_ as rewriteLine};
@@ -1 +1 @@
1
- import{readdirSync as k,readFileSync as M,realpathSync as C,statSync as E}from"node:fs";import{isAbsolute as x,resolve as a}from"node:path";import{homedir as F}from"node:os";import{logger as j}from"../util/log.js";const c=j("cost/session-store"),f=new Map;function $(n){if(f.has(n))return f.get(n);try{const s=C(n);return f.set(n,s),s}catch{f.set(n,void 0);return}}function N(){const n=process.env.HYDRA_ACP_HOME??a(F(),".hydra-acp");return a(n,"sessions")}function J(n){const s=a(n,"meta.json");let o;try{o=M(s,"utf8")}catch(p){const g=p;if(g.code==="ENOENT")return;c.debug(`skipping ${n}: read meta.json failed: ${g.message}`);return}let t;try{t=JSON.parse(o)}catch{c.debug(`skipping ${n}: meta.json is not valid JSON`);return}if(!t||typeof t!="object"||Array.isArray(t)){c.debug(`skipping ${n}: meta.json is not an object`);return}const e=t,r=typeof e.sessionId=="string"?e.sessionId:void 0;if(r===void 0){c.debug(`skipping ${n}: missing sessionId`);return}let i;const u=typeof e.cwd=="string"?e.cwd:void 0;u!==void 0&&(x(u)?i=$(u):i=u);const m=typeof e.agentId=="string"?e.agentId:"",y=typeof e.currentModel=="string"?e.currentModel:"",l=e.interactive===!0,d=e.currentUsage??void 0,S=typeof d?.costAmount=="number"?d.costAmount:0,A=typeof d?.costCurrency=="string"?d.costCurrency:"",b=typeof d?.used=="number"?d.used:0,w=typeof e.title=="string"?e.title:"",h=typeof e.createdAt=="string"?e.createdAt:"",v=typeof e.updatedAt=="string"?e.updatedAt:"",I=typeof e.importedFromMachine=="string"&&e.importedFromMachine.length>0?e.importedFromMachine:void 0,R=typeof e.upstreamSessionId=="string"&&e.upstreamSessionId.length>0?e.upstreamSessionId:void 0;return{sessionId:r,cwd:i,agentId:m,model:y,interactive:l,costAmount:S,costCurrency:A,contextTokens:b,title:w,createdAt:h,updatedAt:v,importedFromMachine:I,upstreamSessionId:R}}async function U(n){const{streamHistoryEditEvents:s}=await import("./history-stream.js");for(const o of n){const t={};for await(const e of s(o)){const r=t[e.filetype]??{added:0,removed:0};r.added+=e.linesAdded,r.removed+=e.linesRemoved,t[e.filetype]=r}o.locByFiletype=t}return n}function _(){const n=N();let s;try{s=k(n)}catch(t){const e=t;return c.debug(`session store not found at ${n}: ${e.message}`),[]}const o=[];for(const t of s){const e=a(n,t);let r;try{r=E(e)}catch{continue}if(!r.isDirectory())continue;const i=J(e);i!==void 0&&o.push(i)}return o}export{U as enrichSessionsWithLoc,_ as scanSessions,N as sessionsDir};
1
+ import{readdirSync as k,readFileSync as C,realpathSync as M,statSync as E}from"node:fs";import{isAbsolute as x,resolve as a}from"node:path";import{homedir as F}from"node:os";import{logger as j}from"../util/log.js";const c=j("cost/session-store"),f=new Map;function $(n){if(f.has(n))return f.get(n);try{const s=M(n);return f.set(n,s),s}catch{f.set(n,void 0);return}}function N(){const n=process.env.HYDRA_ACP_HOME??a(F(),".hydra-acp");return a(n,"sessions")}function J(n){const s=a(n,"meta.json");let r;try{r=C(s,"utf8")}catch(m){const p=m;if(p.code==="ENOENT")return;c.debug(`skipping ${n}: read meta.json failed: ${p.message}`);return}let t;try{t=JSON.parse(r)}catch{c.debug(`skipping ${n}: meta.json is not valid JSON`);return}if(!t||typeof t!="object"||Array.isArray(t)){c.debug(`skipping ${n}: meta.json is not an object`);return}const e=t,o=typeof e.sessionId=="string"?e.sessionId:void 0;if(o===void 0){c.debug(`skipping ${n}: missing sessionId`);return}let d;const u=typeof e.cwd=="string"?e.cwd:void 0;u!==void 0&&(x(u)?d=$(u):d=u);const g=typeof e.agentId=="string"?e.agentId:"",y=typeof e.currentModel=="string"?e.currentModel:"",l=e.interactive===!0,i=e.currentUsage??void 0,S=(typeof i?.costAmount=="number"?i.costAmount:0)+(typeof i?.cumulativeCost=="number"?i.cumulativeCost:0),v=typeof i?.costCurrency=="string"?i.costCurrency:"",b=typeof i?.used=="number"?i.used:0,w=typeof e.title=="string"?e.title:"",A=typeof e.createdAt=="string"?e.createdAt:"",h=typeof e.updatedAt=="string"?e.updatedAt:"",I=typeof e.importedFromMachine=="string"&&e.importedFromMachine.length>0?e.importedFromMachine:void 0,R=typeof e.upstreamSessionId=="string"&&e.upstreamSessionId.length>0?e.upstreamSessionId:void 0;return{sessionId:o,cwd:d,agentId:g,model:y,interactive:l,costAmount:S,costCurrency:v,contextTokens:b,title:w,createdAt:A,updatedAt:h,importedFromMachine:I,upstreamSessionId:R}}async function U(n){const{streamHistoryEditEvents:s}=await import("./history-stream.js");for(const r of n){const t={};for await(const e of s(r)){const o=t[e.filetype]??{added:0,removed:0};o.added+=e.linesAdded,o.removed+=e.linesRemoved,t[e.filetype]=o}r.locByFiletype=t}return n}function _(){const n=N();let s;try{s=k(n)}catch(t){const e=t;return c.debug(`session store not found at ${n}: ${e.message}`),[]}const r=[];for(const t of s){const e=a(n,t);let o;try{o=E(e)}catch{continue}if(!o.isDirectory())continue;const d=J(e);d!==void 0&&r.push(d)}return r}export{U as enrichSessionsWithLoc,_ as scanSessions,N as sessionsDir};
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as U}from"node:fs";import{dirname as F,resolve as N}from"node:path";import{fileURLToPath as P}from"node:url";import{loadConfig as H}from"./config.js";import{BudgeterBridge as M}from"./bridge.js";import{DEFAULT_RULE as W}from"./rule.js";import{logger as j,setDebug as G}from"./util/log.js";import{scanSessions as _,enrichSessionsWithLoc as B}from"./cost/session-store.js";import{listSessionsFromDaemon as A,fetchUsageEventsFromDaemon as J}from"./cost/daemon-client.js";import{streamHistoryEditEvents as Y}from"./cost/history-stream.js";import{aggregate as q,applyFilters as V,parseSince as K}from"./cost/aggregate.js";import{renderText as Z,renderJson as z}from"./cost/format.js";const D=j("main");function L(){try{const e=F(P(import.meta.url));return JSON.parse(U(N(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}function Q(){process.stderr.write(`hydra-acp-budgeter: 'reset' as a CLI subcommand is no longer supported.
2
+ import{readFileSync as U}from"node:fs";import{dirname as F,resolve as N}from"node:path";import{fileURLToPath as M}from"node:url";import{loadConfig as P}from"./config.js";import{BudgeterBridge as _}from"./bridge.js";import{DEFAULT_RULE as H}from"./rule.js";import{logger as W,setDebug as j}from"./util/log.js";import{scanSessions as B,enrichSessionsWithLoc as G}from"./cost/session-store.js";import{listSessionsFromDaemon as A,fetchUsageEventsFromDaemon as J}from"./cost/daemon-client.js";import{streamHistoryEditEvents as Y}from"./cost/history-stream.js";import{aggregate as q,applyFilters as V,parseSince as z}from"./cost/aggregate.js";import{renderText as K,renderJson as Z}from"./cost/format.js";const L=W("main");function C(){try{const e=F(M(import.meta.url));return JSON.parse(U(N(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}function Q(){process.stderr.write(`hydra-acp-budgeter: 'reset' as a CLI subcommand is no longer supported.
3
3
  Run \`/hydra hydra-acp-budgeter reset\` in a live hydra session instead.
4
4
  Cost state is now persisted per-session in meta.json (extension_state).
5
5
  `),process.exit(2)}const X=`Usage: hydra budgeter usage [OPTIONS]
6
6
 
7
7
  Options:
8
8
  --since <date|duration> Only include sessions updated after this date (e.g. 7d, 2024-01-01)
9
- --bucket <hour|day|week|month> Group results into time buckets (implies --since 24h/30d/6m/2y)
9
+ --bucket <hour|day|week|month|weekday|dayofweek> Group results into time buckets (implies
10
+ --since 24h/30d/6m/2y; weekday collapses the last 30 days
11
+ into Mon-Sun)
10
12
  --by <dir|session|model|agent|filetype> Group by dimension (filetype requires --metric loc)
11
13
  --depth <N> Depth for --by dir grouping (default: 1)
12
14
  --dir <path> Only include sessions under this directory prefix
@@ -21,14 +23,14 @@ Options:
21
23
  --metric <cost|tokens|loc> Display metric (default: cost). "loc" counts net lines of code
22
24
  from Edit/Write tool diffs in session history.
23
25
  --json Output as JSON
24
- --help Show this help message`;async function T(e){if(e.includes("--help")){process.stdout.write(X+`
25
- `);return}let r,n="hour",i,d,l,w=!1,f,u=!0,o,b=!1,h,m=!1,p=!1;for(let t=0;t<e.length;t++){const s=e[t];if(s!==void 0){if(s==="--since")t+=1,r=e[t];else if(s==="--bucket")t+=1,n=e[t],m=!0;else if(s==="--by")t+=1,i=e[t];else if(s==="--depth")t+=1,d=e[t];else if(s==="--dir")t+=1,l=e[t];else if(s==="--interactive")w=!0;else if(s==="--min")t+=1,f=e[t];else if(s==="--histogram")u=!0,p=!0;else if(s==="--no-histogram")u=!1,p=!0;else if(s==="--metric")t+=1,o=e[t];else if(s==="--json")b=!0;else if(s==="--host")t+=1,h=e[t];else if(s.startsWith("--host="))h=s.slice(7);else if(s.startsWith("--"))throw new Error(`Unknown option: ${s}
26
+ --help Show this help message`;async function D(e){if(e.includes("--help")){process.stdout.write(X+`
27
+ `);return}let i,s="hour",r,a,l,w=!1,f,u=!0,o,k=!1,h,p=!1,m=!1;for(let t=0;t<e.length;t++){const n=e[t];if(n!==void 0){if(n==="--since")t+=1,i=e[t];else if(n==="--bucket")t+=1,s=ee(e[t]),p=!0;else if(n==="--by")t+=1,r=e[t];else if(n==="--depth")t+=1,a=e[t];else if(n==="--dir")t+=1,l=e[t];else if(n==="--interactive")w=!0;else if(n==="--min")t+=1,f=e[t];else if(n==="--histogram")u=!0,m=!0;else if(n==="--no-histogram")u=!1,m=!0;else if(n==="--metric")t+=1,o=e[t];else if(n==="--json")k=!0;else if(n==="--host")t+=1,h=e[t];else if(n.startsWith("--host="))h=n.slice(7);else if(n.startsWith("--"))throw new Error(`Unknown option: ${n}
26
28
  Run "hydra budgeter usage --help" for usage.`)}}if(o!==void 0&&o!=="cost"&&o!=="tokens"&&o!=="loc")throw new Error(`Invalid --metric value. Must be 'cost', 'tokens', or 'loc'.
27
- Run "hydra budgeter usage --help" for usage.`);const a=o==="loc";if(i!==void 0&&!new Set(["dir","session","model","agent","filetype"]).has(i))throw new Error(`Invalid --by value: ${i}. Must be one of: dir, session, model, agent, filetype.
28
- Run "hydra budgeter usage --help" for usage.`);if(i==="filetype"&&!a)throw new Error(`--by filetype requires --metric loc.
29
- Run "hydra budgeter usage --help" for usage.`);let g;if(r!==void 0)try{g=K(r)}catch(t){const s=t;throw new Error(`Invalid --since value: ${s.message}
30
- Run "hydra budgeter usage --help" for usage.`)}i!==void 0&&(m||(n=void 0),p||(u=!1)),!u&&!m&&(n=void 0);let c;if(g!==void 0)c=g;else{const t=new Date;n==="day"?t.setDate(t.getDate()-30):n==="week"?t.setMonth(t.getMonth()-6):n==="month"?t.setFullYear(t.getFullYear()-2):t.setHours(t.getHours()-24),c=t}const v=w?!0:void 0,y=o==="tokens",k=f!==void 0?parseFloat(f):void 0,E=await A()??_();(a||i==="filetype")&&await B(E);const S=V(E,{since:c,dir:l,interactive:v,min:k,minMetric:a?"loc":y?"tokens":"cost",host:h??"local"});let R;if(n!==void 0){const t=await J();t!==void 0&&(R=t.map(s=>({sessionId:s.sessionId,ts:s.ts,deltaCost:0,cumulativeCost:s.costCumulative,currency:s.costCurrency,inputTokens:s.contextTokens})))}let I;if(a&&n!==void 0){const t=[];for await(const s of Y(S))t.push(s);I=t}const C=d!==void 0?parseInt(d,10):void 0,$=q(S,R,{by:i,depth:C,bucket:n,since:c,interactive:v,dir:l,tokens:y,loc:a,min:k},I),x=ee({sinceRaw:r,effectiveSince:c,bucket:n});if(b)process.stdout.write(z($)+`
31
- `);else{const t=Z($,{histogram:u,tokens:y,loc:a,windowLabel:x});process.stdout.write(t)}}function ee(e){if(e.sinceRaw!==void 0){const r=te(e.sinceRaw);return r!==void 0?`last ${r}`:`since ${e.sinceRaw}`}return e.bucket==="day"?"last 30 days":e.bucket==="week"?"last 6 months":e.bucket==="month"?"last 2 years":e.bucket==="hour"?"last 24 hours":"last 24 hours (default; pass --since to widen)"}function te(e){const r=e.match(/^(\d+)\s*([dhmwy])$/i);if(r===null)return;let n=parseInt(r[1]??"0",10),i=(r[2]??"").toLowerCase();i==="h"&&n%24===0&&(n=n/24,i="d"),i==="d"&&n%7===0&&n<30&&(n=n/7,i="w"),i==="m"&&n%12===0&&(n=n/12,i="y");const d={h:"hour",d:"day",w:"week",m:"month",y:"year"}[i];if(d!==void 0)return`${n} ${d}${n===1?"":"s"}`}async function se(){const e=H();G(e.debug);const r=new M({daemonWsUrl:e.hydraWsUrl,daemonHttpBase:e.hydraDaemonUrl,token:e.hydraToken,softLimit:e.softLimit,hardLimit:e.hardLimit,currency:e.currency,rule:W});r.start();const n=i=>{D.info(`${i} received \u2014 shutting down`),r.stop(),setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>n("SIGINT")),process.on("SIGTERM",()=>n("SIGTERM")),D.info(`hydra-acp-budgeter up; daemon=${e.hydraDaemonUrl} soft=${e.softLimit} hard=${e.hardLimit} ${e.currency} state=per-session (extension_state)`)}function ne(){process.stdout.write(`hydra-acp-budgeter ${L()}
29
+ Run "hydra budgeter usage --help" for usage.`);const d=o==="loc";if(r!==void 0&&!new Set(["dir","session","model","agent","filetype"]).has(r))throw new Error(`Invalid --by value: ${r}. Must be one of: dir, session, model, agent, filetype.
30
+ Run "hydra budgeter usage --help" for usage.`);if(r==="filetype"&&!d)throw new Error(`--by filetype requires --metric loc.
31
+ Run "hydra budgeter usage --help" for usage.`);let y;if(i!==void 0)try{y=z(i)}catch(t){const n=t;throw new Error(`Invalid --since value: ${n.message}
32
+ Run "hydra budgeter usage --help" for usage.`)}r!==void 0&&(p||(s=void 0),m||(u=!1)),!u&&!p&&(s=void 0);let c;if(y!==void 0)c=y;else{const t=new Date;s==="day"||s==="weekday"?t.setDate(t.getDate()-30):s==="week"?t.setMonth(t.getMonth()-6):s==="month"?t.setFullYear(t.getFullYear()-2):t.setHours(t.getHours()-24),c=t}const b=w?!0:void 0,g=o==="tokens",v=f!==void 0?parseFloat(f):void 0,E=await A()??B();(d||r==="filetype")&&await G(E);const S=V(E,{since:c,dir:l,interactive:b,min:v,minMetric:d?"loc":g?"tokens":"cost",host:h??"local"});let R;if(s!==void 0){const t=await J();t!==void 0&&(R=t.map(n=>({sessionId:n.sessionId,ts:n.ts,deltaCost:0,cumulativeCost:n.costCumulative,currency:n.costCurrency,inputTokens:n.contextTokens})))}let $;if(d&&s!==void 0){const t=[];for await(const n of Y(S))t.push(n);$=t}const T=a!==void 0?parseInt(a,10):void 0,I=q(S,R,{by:r,depth:T,bucket:s,since:c,interactive:b,dir:l,tokens:g,loc:d,min:v},$),x=te({sinceRaw:i,effectiveSince:c,bucket:s});if(k)process.stdout.write(Z(I)+`
33
+ `);else{const t=K(I,{histogram:u,tokens:g,loc:d,windowLabel:x});process.stdout.write(t)}}function ee(e){const i=(e??"").toLowerCase();switch(i){case"weekday":case"dayofweek":case"day-of-week":case"day_of_week":case"dow":return"weekday";case"hour":case"day":case"week":case"month":return i;default:throw new Error(`unknown --bucket value ${JSON.stringify(e??"")} (expected hour, day, week, month, or weekday)`)}}function te(e){if(e.sinceRaw!==void 0){const i=se(e.sinceRaw);return i!==void 0?`last ${i}`:`since ${e.sinceRaw}`}return e.bucket==="day"||e.bucket==="weekday"?"last 30 days":e.bucket==="week"?"last 6 months":e.bucket==="month"?"last 2 years":e.bucket==="hour"?"last 24 hours":"last 24 hours (default; pass --since to widen)"}function se(e){const i=e.match(/^(\d+)\s*([dhmwy])$/i);if(i===null)return;let s=parseInt(i[1]??"0",10),r=(i[2]??"").toLowerCase();r==="h"&&s%24===0&&(s=s/24,r="d"),r==="d"&&s%7===0&&s<30&&(s=s/7,r="w"),r==="m"&&s%12===0&&(s=s/12,r="y");const a={h:"hour",d:"day",w:"week",m:"month",y:"year"}[r];if(a!==void 0)return`${s} ${a}${s===1?"":"s"}`}async function ne(){const e=P();j(e.debug);const i=new _({daemonWsUrl:e.hydraWsUrl,daemonHttpBase:e.hydraDaemonUrl,token:e.hydraToken,softLimit:e.softLimit,hardLimit:e.hardLimit,currency:e.currency,rule:H});i.start();const s=r=>{L.info(`${r} received \u2014 shutting down`),i.stop(),setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>s("SIGINT")),process.on("SIGTERM",()=>s("SIGTERM")),L.info(`hydra-acp-budgeter up; daemon=${e.hydraDaemonUrl} soft=${e.softLimit} hard=${e.hardLimit} ${e.currency} state=per-session (extension_state)`)}function ie(){process.stdout.write(`hydra-acp-budgeter ${C()}
32
34
 
33
35
  Usage:
34
36
  hydra budgeter [usage] <flags> Report historical cost/usage across sessions
@@ -36,7 +38,8 @@ Usage:
36
38
 
37
39
  Flags:
38
40
  -v, --version Print version and exit
41
+ repair Reconcile recorded cost against the agent ledger
39
42
  -h, --help Show this help
40
- `)}async function ie(){const e=process.argv.slice(2);if(e.includes("--version")||e.includes("-v")){process.stdout.write(`hydra-acp-budgeter ${L()}
41
- `);return}if((e[0]==="help"||e.includes("--help")||e.includes("-h"))&&e[0]!=="usage"&&e[0]!=="cost"){ne();return}if(e[0]==="reset"){Q();return}if(e[0]==="usage"||e[0]==="cost"){await T(e.slice(1));return}if(e[0]==="run"||process.env.HYDRA_ACP_TOKEN){await se();return}await T(e)}ie().catch(e=>{process.stderr.write(`hydra-acp-budgeter: ${e.message}
43
+ `)}async function re(){const e=process.argv.slice(2);if(e.includes("--version")||e.includes("-v")){process.stdout.write(`hydra-acp-budgeter ${C()}
44
+ `);return}if((e[0]==="help"||e.includes("--help")||e.includes("-h"))&&e[0]!=="usage"&&e[0]!=="cost"&&e[0]!=="repair"){ie();return}if(e[0]==="reset"){Q();return}if(e[0]==="repair"){const{runRepair:i}=await import("./cost/repair-cmd.js");await i(e.slice(1));return}if(e[0]==="usage"||e[0]==="cost"){await D(e.slice(1));return}if(e[0]==="run"||process.env.HYDRA_ACP_TOKEN){await ne();return}await D(e)}re().catch(e=>{process.stderr.write(`hydra-acp-budgeter: ${e.message}
42
45
  `),process.exit(1)});
package/dist/tracker.js CHANGED
@@ -1 +1 @@
1
- import{logger as c}from"./util/log.js";const i=c("tracker");class m{constructor(t){this.opts=t}opts;sessions=new Map;confirmedAbsent=new Set;hydrating=new Map;currentState="ok";async hydrateAll(){if(!this.opts.store)return;let t;try{t=await this.opts.store.listSessionIds()}catch(e){i.warn(`boot hydration failed to list sessions: ${e.message} \u2014 continuing with lazy loading`);return}for(const e of t)if(!(this.sessions.has(e)||this.confirmedAbsent.has(e)))try{const s=await this.opts.store.get(e);s?this.sessions.set(e,s):this.confirmedAbsent.add(e)}catch(s){i.warn(`boot hydration failed for session ${e.slice(0,12)}: ${s.message}`)}this.currentState=o(this.totalCost(),this.opts.softLimit,this.opts.hardLimit),i.info(`hydrated ${this.sessions.size} session(s), total=${this.totalCost().toFixed(2)}`)}applyUsageUpdate(t,e){i.debug(`usage_update session=${t.slice(0,12)} raw=${JSON.stringify(e).slice(0,200)}`);const s=d(e);if(s===void 0)return i.debug(`readCost returned undefined for session=${t.slice(0,12)}`),this.snapshotFor(t);const n=this.sessions.get(t)??{cost:0,baseline:0,currency:void 0},a={cost:Math.max(n.cost,s.amount),baseline:n.baseline,currency:s.currency??n.currency};return this.sessions.set(t,a),this.confirmedAbsent.delete(t),this.persistOne(t,a),this.snapshotFor(t)}async ensureHydrated(t){if(!this.opts.store||this.sessions.has(t)||this.confirmedAbsent.has(t))return;const e=this.hydrating.get(t);if(e)return e;const s=(async()=>{try{const n=await this.opts.store.get(t);n?this.sessions.set(t,n):this.confirmedAbsent.add(t)}catch(n){i.warn(`hydrate session ${t.slice(0,12)} failed: ${n.message}`)}finally{this.hydrating.delete(t)}})();return this.hydrating.set(t,s),s}snapshotFor(t){const e=this.sessions.get(t),s=this.totalCost();return{total:s,perSession:e?Math.max(0,e.cost-e.baseline):0,currency:e?.currency??this.opts.currency,soft:this.opts.softLimit,hard:this.opts.hardLimit,state:o(s,this.opts.softLimit,this.opts.hardLimit)}}consumeStateTransition(){const t=o(this.totalCost(),this.opts.softLimit,this.opts.hardLimit);if(u(t)>u(this.currentState))return this.currentState=t,t;t!==this.currentState&&(this.currentState=t)}get state(){return this.currentState}reset(){for(const[t,e]of this.sessions){const s={...e,baseline:e.cost};this.sessions.set(t,s),this.persistOne(t,s)}this.currentState="ok"}async persistOne(t,e){if(this.opts.store)try{await this.opts.store.set(t,e)}catch(s){i.warn(`persist ${t.slice(0,12)} failed: ${s.message}`)}}totalCost(){let t=0;for(const e of this.sessions.values())t+=Math.max(0,e.cost-e.baseline);return t}}function o(r,t,e){return r>=e?"hard":r>=t?"soft":"ok"}function u(r){return r==="hard"?2:r==="soft"?1:0}function d(r){const t=h(r._meta),e=r.cost??void 0,s=t!==void 0?t:typeof e?.amount=="number"?e.amount:void 0;if(s===void 0)return;const n=typeof e?.currency=="string"?e.currency:void 0;return{amount:s,currency:n}}function h(r){if(!r||typeof r!="object"||Array.isArray(r))return;const t=r["hydra-acp"];if(!t||typeof t!="object"||Array.isArray(t))return;const e=t.cumulativeCost;return typeof e=="number"?e:void 0}export{m as CostTracker};
1
+ import{logger as u}from"./util/log.js";const i=u("tracker");class f{constructor(t){this.opts=t}opts;sessions=new Map;confirmedAbsent=new Set;hydrating=new Map;currentState="ok";async hydrateAll(){if(!this.opts.store)return;let t;try{t=await this.opts.store.listSessionIds()}catch(e){i.warn(`boot hydration failed to list sessions: ${e.message} \u2014 continuing with lazy loading`);return}for(const e of t)if(!(this.sessions.has(e)||this.confirmedAbsent.has(e)))try{const s=await this.opts.store.get(e);s?this.sessions.set(e,s):this.confirmedAbsent.add(e)}catch(s){i.warn(`boot hydration failed for session ${e.slice(0,12)}: ${s.message}`)}this.currentState=o(this.totalCost(),this.opts.softLimit,this.opts.hardLimit),i.info(`hydrated ${this.sessions.size} session(s), total=${this.totalCost().toFixed(2)}`)}applyUsageUpdate(t,e){i.debug(`usage_update session=${t.slice(0,12)} raw=${JSON.stringify(e).slice(0,200)}`);const s=h(e);if(s===void 0)return i.debug(`readCost returned undefined for session=${t.slice(0,12)}`),this.snapshotFor(t);const n=this.sessions.get(t)??{cost:0,baseline:0,currency:void 0},a={cost:Math.max(n.cost,s.amount),baseline:n.baseline,currency:s.currency??n.currency};return this.sessions.set(t,a),this.confirmedAbsent.delete(t),this.persistOne(t,a),this.snapshotFor(t)}async ensureHydrated(t){if(!this.opts.store||this.sessions.has(t)||this.confirmedAbsent.has(t))return;const e=this.hydrating.get(t);if(e)return e;const s=(async()=>{try{const n=await this.opts.store.get(t);n?this.sessions.set(t,n):this.confirmedAbsent.add(t)}catch(n){i.warn(`hydrate session ${t.slice(0,12)} failed: ${n.message}`)}finally{this.hydrating.delete(t)}})();return this.hydrating.set(t,s),s}snapshotFor(t){const e=this.sessions.get(t),s=this.totalCost();return{total:s,perSession:e?Math.max(0,e.cost-e.baseline):0,currency:e?.currency??this.opts.currency,soft:this.opts.softLimit,hard:this.opts.hardLimit,state:o(s,this.opts.softLimit,this.opts.hardLimit)}}consumeStateTransition(){const t=o(this.totalCost(),this.opts.softLimit,this.opts.hardLimit);if(c(t)>c(this.currentState))return this.currentState=t,t;t!==this.currentState&&(this.currentState=t)}get state(){return this.currentState}reset(){for(const[t,e]of this.sessions){const s={...e,baseline:e.cost};this.sessions.set(t,s),this.persistOne(t,s)}this.currentState="ok"}async persistOne(t,e){if(this.opts.store)try{await this.opts.store.set(t,e)}catch(s){i.warn(`persist ${t.slice(0,12)} failed: ${s.message}`)}}totalCost(){let t=0;for(const e of this.sessions.values())t+=Math.max(0,e.cost-e.baseline);return t}}function o(r,t,e){return r>=e?"hard":r>=t?"soft":"ok"}function c(r){return r==="hard"?2:r==="soft"?1:0}function h(r){const t=r.cost??void 0;if(typeof t?.amount!="number")return;const e=typeof t.currency=="string"?t.currency:void 0;return{amount:t.amount,currency:e}}export{f as CostTracker};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/budgeter",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Cost-budget transformer extension for hydra-acp — warns on soft limit, rejects further prompts/sessions on hard limit.",
5
5
  "license": "MIT",
6
6
  "type": "module",