@hydra-acp/budgeter 0.1.19 → 0.1.21

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 H,resolve as R}from"node:path";import{homedir as L}from"node:os";import{realpathSync as $}from"node:fs";function V(w){const c=w.match(/^(\d+)\s*([dhmwy])$/i);if(c!==null){const y=c[1],a=c[2];if(y===void 0||a===void 0)throw new Error(`invalid since spec: ${w}`);const h=parseInt(y,10),k=a.toLowerCase(),r=new Date;return k==="d"?r.setDate(r.getDate()-h):k==="h"?r.setHours(r.getHours()-h):k==="w"?r.setDate(r.getDate()-h*7):k==="m"?r.setMonth(r.getMonth()-h):k==="y"&&r.setFullYear(r.getFullYear()-h),r}const t=new Date(w);if(Number.isNaN(t.getTime()))throw new Error(`invalid since spec: ${w}`);return t}function q(w){const c=w.locByFiletype;if(c===void 0)return 0;let t=0;for(const y of Object.keys(c)){const a=c[y];a!==void 0&&(t+=a.added-a.removed)}return t}const I=new Map;function D(w){if(I.has(w))return I.get(w);try{const c=$(w);return I.set(w,c),c}catch{return I.set(w,w),w}}function P(w,c){let t=w;if(c.since!==void 0){const h=c.since.getTime(),k=[];for(const r of t)new Date(r.updatedAt).getTime()<h||k.push(r);t=k}if(c.dir!==void 0){const h=D(R(c.dir)),k=[];for(const r of t)r.cwd===void 0||r.cwd===""||(r.cwd.startsWith(h+"/")||r.cwd===h)&&k.push(r);t=k}if(c.host!==void 0&&c.host!=="all"){const h=c.host,k=[];for(const r of t)h==="local"?(!r.importedFromMachine||r.upstreamSessionId)&&k.push(r):r.importedFromMachine===h&&!r.upstreamSessionId&&k.push(r);t=k}if(c.interactive!==void 0){const h=c.interactive,k=[];for(const r of t)r.interactive===h&&k.push(r);t=k}const y=c.min??0,a=[];for(const h of t){let k;c.minMetric==="tokens"?k=h.contextTokens:c.minMetric==="loc"?k=q(h):k=h.costAmount,k>y&&a.push(h)}return a}function Y(w){return w.dir!==void 0?R(w.dir):R(L())}function K(w,c,t){if(w===void 0||w==="")return"<unknown>";const y=D(R(w));if(c===void 0){const S=D(R(L()));return y===S?"~":y.startsWith(S+"/")?"~/"+y.slice(S.length+1):y}const a=D(R(t));let h=H(a,y);if(h===""||h===".")return"<root>";if(h.startsWith(".."))return"<unknown>";const k=h.split("/");if(k.length===0)return"<root>";const r=Math.min(c,k.length),C=k.slice(0,r);return C.length===0?"<root>":C.join("/")}function X(w,c,t={},y){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"||t.bucket==="weekday"?(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 i of w)e+=i.costAmount;let s="";for(const i of w){s=i.costCurrency;break}return{kind:"total",row:{label:"All sessions",costAmount:e,sessionCount:w.length},currency:s}}const k={since:a,dir:t.dir,interactive:t.interactive,min:t.min,minMetric:t.loc===!0?"loc":t.tokens===!0?"tokens":"cost"},r=P(w,k),C=new Map;if(c!==void 0)for(const e of c){const s=C.get(e.sessionId);s===void 0?C.set(e.sessionId,[e]):s.push(e)}let S="";if(c!==void 0&&c.length>0)for(const e of c){S=e.currency;break}else if(r.length>0)for(const e of r){S=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 l=!1;for(const m of Object.keys(n)){const f=n[m];if(f===void 0)continue;let b=e.get(m);b===void 0&&(b={added:0,removed:0,sessions:new Set},e.set(m,b)),b.added+=f.added,b.removed+=f.removed,(f.added>0||f.removed>0)&&(b.sessions.add(u.sessionId),l=!0)}l&&s.add(u.sessionId)}const i=[];for(const[u,n]of e){const l={label:u,costAmount:0,sessionCount:n.sessions.size,linesAdded:n.added,linesRemoved:n.removed};i.push({label:u,items:[l],sessionCount:n.sessions.size})}return{kind:"grouped",groups:i,currency:S,totalSessions:s.size}}const E=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 i=s.toLocaleDateString(void 0,{month:"2-digit",day:"2-digit"}),u=String(s.getHours()).padStart(2,"0");return`${i} ${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 i=new Date(s),u=i.getDay(),n=i.getDate()-u+(u===0?-6:1);return i.setDate(n),i.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"})},x=new Map;if(t.bucket==="weekday"){const e=new Date(2024,0,1);for(let s=0;s<7;s++){const i=new Date(e);i.setDate(e.getDate()+s),x.set(i.toLocaleDateString(void 0,{weekday:"short"}),s)}}const M=(e,s)=>{if(t.bucket==="weekday"){const i=x.get(e.bucket)??7,u=x.get(s.bucket)??7;if(i!==u)return i-u}return e.bucket.localeCompare(s.bucket)},F=new Map;for(const e of r)F.set(e.sessionId,e);const A=new Map;if(c!==void 0){for(const e of c){if(!F.has(e.sessionId))continue;const s=A.get(e.sessionId);s===void 0?A.set(e.sessionId,[e]):s.push(e)}for(const e of A.values())e.sort((s,i)=>s.ts.localeCompare(i.ts))}if(t.by===void 0&&t.bucket===void 0){let e=0,s=0,i=0,u=0,n=0,l=0;for(const f of r){const b=A.get(f.sessionId);if(b===void 0||b.length<2)e+=f.costAmount,l++;else{let d=b[0].cumulativeCost,g=!1;for(let p=1;p<b.length;p++){const v=b[p];if(v===void 0)continue;const T=Math.max(0,v.cumulativeCost-d);d=v.cumulativeCost,!(a!==void 0&&new Date(v.ts)<a)&&(e+=T,g=!0)}g&&l++}if(t.tokens===!0){const o=C.get(f.sessionId);if(o!==void 0)for(const d of o)d.inputTokens!==void 0&&(s+=d.inputTokens),d.outputTokens!==void 0&&(i+=d.outputTokens),d.cacheReadTokens!==void 0&&(u+=d.cacheReadTokens),d.cacheWriteTokens!==void 0&&(n+=d.cacheWriteTokens)}}const m={label:"All sessions",costAmount:e,deltaCost:e,sessionCount:l};if(t.tokens===!0&&(m.inputTokens=s,m.outputTokens=i,m.cacheReadTokens=u,m.cacheWriteTokens=n),t.loc===!0){let f=0,b=0;for(const o of r){const d=o.locByFiletype;if(d!==void 0)for(const g of Object.keys(d)){const p=d[g];p!==void 0&&(f+=p.added,b+=p.removed)}}m.linesAdded=f,m.linesRemoved=b}return{kind:"total",row:m,currency:S}}if(t.by!==void 0&&t.bucket===void 0){const e=new Map;for(const i of r){const u=E(i);let n=e.get(u);if(n===void 0&&(n={label:u,rows:{label:u,costAmount:0,deltaCost:0,sessionCount:0},sessions:new Set},e.set(u,n)),t.tokens===!0&&(n.rows.inputTokens===void 0&&(n.rows.inputTokens=0),n.rows.inputTokens+=i.contextTokens),t.loc===!0){const m=i.locByFiletype;if(m!==void 0){n.rows.linesAdded===void 0&&(n.rows.linesAdded=0),n.rows.linesRemoved===void 0&&(n.rows.linesRemoved=0);for(const f of Object.keys(m)){const b=m[f];b!==void 0&&(n.rows.linesAdded+=b.added,n.rows.linesRemoved+=b.removed)}}}const l=A.get(i.sessionId);if(l===void 0||l.length<2)n.rows.costAmount+=i.costAmount,n.rows.deltaCost+=i.costAmount,n.sessions.add(i.sessionId);else{let f=l[0].cumulativeCost,b=!1;for(let o=1;o<l.length;o++){const d=l[o];if(d===void 0)continue;const g=Math.max(0,d.cumulativeCost-f);f=d.cumulativeCost,!(a!==void 0&&new Date(d.ts)<a)&&(n.rows.costAmount+=g,n.rows.deltaCost+=g,b=!0)}b&&n.sessions.add(i.sessionId)}if(t.tokens===!0){const m=C.get(i.sessionId);if(m!==void 0)for(const f of m)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),f.inputTokens!==void 0&&(n.rows.inputTokens+=f.inputTokens),f.outputTokens!==void 0&&(n.rows.outputTokens+=f.outputTokens),f.cacheReadTokens!==void 0&&(n.rows.cacheReadTokens+=f.cacheReadTokens),f.cacheWriteTokens!==void 0&&(n.rows.cacheWriteTokens+=f.cacheWriteTokens)}}const s=[];for(const i of e.values())i.rows.sessionCount=i.sessions.size,s.push({label:i.label,items:[i.rows],sessionCount:i.sessions.size});return{kind:"grouped",groups:s,currency:S}}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,i)=>{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+=i,n.deltaCost+=i,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 o of r)e.add(o.sessionId);const s=new Map;for(const o of r)s.set(o.sessionId,o);const i=(o,d)=>{let g=o.get(d);return g===void 0&&(g={bucket:d,costAmount:0,deltaCost:0,sessionCount:0,linesAdded:0,linesRemoved:0},o.set(d,g)),(g.linesAdded??void 0)===void 0&&(g.linesAdded=0),(g.linesRemoved??void 0)===void 0&&(g.linesRemoved=0),g},u=(o,d)=>{o.linesAdded=(o.linesAdded??0)+d.linesAdded,o.linesRemoved=(o.linesRemoved??0)+d.linesRemoved,o._sessions===void 0&&(o._sessions=new Set),o._sessions.add(d.sessionId),o.sessionCount=o._sessions.size},n=o=>{delete o._sessions},l=y??[];if(t.by==="filetype"){const o=new Map,d=new Set;for(const p of l){if(!e.has(p.sessionId)||a!==void 0&&new Date(p.ts)<a)continue;let v=o.get(p.filetype);v===void 0&&(v={label:p.filetype,buckets:new Map,sessions:new Set},o.set(p.filetype,v));const T=i(v.buckets,B(p.ts));u(T,p),v.sessions.add(p.sessionId),d.add(p.sessionId)}const g=[];for(const p of o.values()){const v=Array.from(p.buckets.values());for(const T of v)n(T);v.sort(M),v.length>0&&g.push({label:p.label,items:v,sessionCount:p.sessions.size})}return{kind:"timeSeriesGrouped",groups:g,currency:S,totalSessions:d.size}}if(t.by!==void 0){const o=new Map;for(const g of l){if(!e.has(g.sessionId)||a!==void 0&&new Date(g.ts)<a)continue;const p=s.get(g.sessionId);if(p===void 0)continue;const v=E(p);let T=o.get(v);T===void 0&&(T={label:v,buckets:new Map,sessions:new Set},o.set(v,T));const N=i(T.buckets,B(g.ts));u(N,g),T.sessions.add(g.sessionId)}const d=[];for(const g of o.values()){const p=Array.from(g.buckets.values());for(const v of p)n(v);p.sort(M),p.length>0&&d.push({label:g.label,items:p,sessionCount:g.sessions.size})}return{kind:"timeSeriesGrouped",groups:d,currency:S}}const m=new Map,f=new Set;for(const o of l){if(!e.has(o.sessionId)||a!==void 0&&new Date(o.ts)<a)continue;const d=i(m,B(o.ts));u(d,o),f.add(o.sessionId)}const b=Array.from(m.values());for(const o of b)n(o);return b.sort(M),{kind:"timeSeries",timeSeries:b,currency:S,totalSessions:f.size}}if(t.by!==void 0&&t.bucket!==void 0){const e=new Map,s=u=>{const n=E(u);let l=e.get(n);return l===void 0&&(l={label:n,buckets:new Map,sessions:new Set},e.set(n,l)),l};for(const u of r){const n=A.get(u.sessionId);if(n===void 0||n.length===0)continue;const l=s(u),m=l.buckets;l.sessions.add(u.sessionId);const f=n[0];if(f===void 0)continue;let b=f.cumulativeCost;for(let o=1;o<n.length;o++){const d=n[o];if(d===void 0)continue;const g=Math.max(0,d.cumulativeCost-b);b=d.cumulativeCost,!(a!==void 0&&new Date(d.ts)<a)&&W(m,d,g)}}const i=[];for(const u of e.values()){const n=Array.from(u.buckets.values());for(const l of n)G(l);n.sort(M),n.length>0&&i.push({label:u.label,items:n,sessionCount:u.sessions.size})}return{kind:"timeSeriesGrouped",groups:i,currency:S}}const z=new Map,O=new Set;for(const e of r){const s=A.get(e.sessionId);if(s===void 0||s.length===0)continue;const i=s[0];if(i===void 0)continue;let u=i.cumulativeCost,n=!1;for(let l=1;l<s.length;l++){const m=s[l];if(m===void 0)continue;const f=Math.max(0,m.cumulativeCost-u);u=m.cumulativeCost,!(a!==void 0&&new Date(m.ts)<a)&&(W(z,m,f),n=!0)}n&&O.add(e.sessionId)}const _=Array.from(z.values());for(const e of _)G(e);return _.sort(M),{kind:"timeSeries",timeSeries:_,currency:S,totalSessions:O.size}}export{X as aggregate,P as applyFilters,V as parseSince};
1
+ import{relative as H,resolve as A,sep as N}from"node:path";import{homedir as L}from"node:os";import{realpathSync as $}from"node:fs";function ee(r){const f=r.match(/^(\d+)\s*([dhmwy])$/i);if(f!==null){const S=f[1],l=f[2];if(S===void 0||l===void 0)throw new Error(`invalid since spec: ${r}`);const h=parseInt(S,10),w=l.toLowerCase(),c=new Date;return w==="d"?c.setDate(c.getDate()-h):w==="h"?c.setHours(c.getHours()-h):w==="w"?c.setDate(c.getDate()-h*7):w==="m"?c.setMonth(c.getMonth()-h):w==="y"&&c.setFullYear(c.getFullYear()-h),c}const t=new Date(r);if(Number.isNaN(t.getTime()))throw new Error(`invalid since spec: ${r}`);return t}function q(r){const f=r.locByFiletype;if(f===void 0)return 0;let t=0;for(const S of Object.keys(f)){const l=f[S];l!==void 0&&(t+=l.added-l.removed)}return t}function Y(r){if(r.remote!==void 0)return r.remote;if(r.importedFromMachine!==void 0&&!r.upstreamSessionId)return r.importedFromMachine}function K(r,f){if(f==="local")return!r.remote&&(!r.importedFromMachine||!!r.upstreamSessionId);const t=!!r.importedFromMachine&&!r.upstreamSessionId;return r.remote===f&&!t||r.importedFromMachine===f&&!r.upstreamSessionId}const B=new Map;function D(r){if(B.has(r))return B.get(r);try{const f=$(r);return B.set(r,f),f}catch{return B.set(r,r),r}}function J(r,f){let t=r;if(f.since!==void 0){const h=f.since.getTime(),w=[];for(const c of t)new Date(c.updatedAt).getTime()<h||w.push(c);t=w}if(f.dir!==void 0){const h=D(A(f.dir)),w=[];for(const c of t)c.cwd===void 0||c.cwd===""||(c.cwd===h||c.cwd.startsWith(h+N))&&w.push(c);t=w}if(f.host!==void 0&&f.host!=="all"){const h=f.host,w=[];for(const c of t)K(c,h)&&w.push(c);t=w}if(f.interactive!==void 0){const h=f.interactive,w=[];for(const c of t)c.interactive===h&&w.push(c);t=w}const S=f.min??0,l=[];for(const h of t){let w;f.minMetric==="tokens"?w=h.contextTokens:f.minMetric==="loc"?w=q(h):w=h.costAmount,w>S&&l.push(h)}return l}function Q(r){return r.dir!==void 0?A(r.dir):A(L())}function U(r,f,t){if(r===void 0||r==="")return"<unknown>";const S=D(A(r));if(f===void 0){const y=D(A(L()));return S===y?"~":S.startsWith(y+"/")?"~/"+S.slice(y.length+1):S}const l=D(A(t));let h=H(l,S);if(h===""||h===".")return"<root>";if(h.startsWith(".."))return"<unknown>";const w=h.split("/");if(w.length===0)return"<root>";const c=Math.min(f,w.length),C=w.slice(0,c);return C.length===0?"<root>":C.join("/")}function ne(r,f,t={},S){let l=t.since;if(l===void 0&&t.bucket!==void 0){const e=new Date;t.bucket==="hour"?(e.setHours(e.getHours()-24),l=e):t.bucket==="day"||t.bucket==="weekday"?(e.setDate(e.getDate()-30),l=e):t.bucket==="week"?(e.setMonth(e.getMonth()-6),l=e):t.bucket==="month"&&(e.setFullYear(e.getFullYear()-2),l=e)}if(!(l!==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 i of r)e+=i.costAmount;let s="";for(const i of r){s=i.costCurrency;break}return{kind:"total",row:{label:"All sessions",costAmount:e,sessionCount:r.length},currency:s}}const w={since:l,dir:t.dir,interactive:t.interactive,min:t.min,minMetric:t.loc===!0?"loc":t.tokens===!0?"tokens":"cost"},c=J(r,w),C=new Map;if(f!==void 0)for(const e of f){const s=C.get(e.sessionId);s===void 0?C.set(e.sessionId,[e]):s.push(e)}let y="";if(f!==void 0&&f.length>0)for(const e of f){y=e.currency;break}else if(c.length>0)for(const e of c){y=e.costCurrency;break}if(t.by==="filetype"&&t.bucket===void 0){const e=new Map,s=new Set;for(const u of c){const n=u.locByFiletype;if(n===void 0)continue;let g=!1;for(const k of Object.keys(n)){const a=n[k];if(a===void 0)continue;let p=e.get(k);p===void 0&&(p={added:0,removed:0,sessions:new Set},e.set(k,p)),p.added+=a.added,p.removed+=a.removed,(a.added>0||a.removed>0)&&(p.sessions.add(u.sessionId),g=!0)}g&&s.add(u.sessionId)}const i=[];for(const[u,n]of e){const g={label:u,costAmount:0,sessionCount:n.sessions.size,linesAdded:n.added,linesRemoved:n.removed};i.push({label:u,items:[g],sessionCount:n.sessions.size})}return{kind:"grouped",groups:i,currency:y,totalSessions:s.size}}const E=e=>{if(t.by==="dir"){const s=Y(e);return s!==void 0?`<${s}>`:U(e.cwd,t.depth,Q(t))}return 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>"},I=e=>{const s=new Date(e);if(Number.isNaN(s.getTime()))return"<invalid>";if(t.bucket==="hour"){const i=s.toLocaleDateString(void 0,{month:"2-digit",day:"2-digit"}),u=String(s.getHours()).padStart(2,"0");return`${i} ${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 i=new Date(s),u=i.getDay(),n=i.getDate()-u+(u===0?-6:1);return i.setDate(n),i.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"})},x=new Map;if(t.bucket==="weekday"){const e=new Date(2024,0,1);for(let s=0;s<7;s++){const i=new Date(e);i.setDate(e.getDate()+s),x.set(i.toLocaleDateString(void 0,{weekday:"short"}),s)}}const R=(e,s)=>{if(t.bucket==="weekday"){const i=x.get(e.bucket)??7,u=x.get(s.bucket)??7;if(i!==u)return i-u}return e.bucket.localeCompare(s.bucket)},_=new Map;for(const e of c)_.set(e.sessionId,e);const M=new Map;if(f!==void 0){for(const e of f){if(!_.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,i)=>s.ts.localeCompare(i.ts))}if(t.by===void 0&&t.bucket===void 0){let e=0,s=0,i=0,u=0,n=0,g=0;for(const a of c){const p=M.get(a.sessionId);if(p===void 0||p.length<2)e+=a.costAmount,g++;else{let d=p[0].cumulativeCost,m=!1;for(let b=1;b<p.length;b++){const v=p[b];if(v===void 0)continue;const T=Math.max(0,v.cumulativeCost-d);d=v.cumulativeCost,!(l!==void 0&&new Date(v.ts)<l)&&(e+=T,m=!0)}m&&g++}if(t.tokens===!0){const o=C.get(a.sessionId);if(o!==void 0)for(const d of o)d.inputTokens!==void 0&&(s+=d.inputTokens),d.outputTokens!==void 0&&(i+=d.outputTokens),d.cacheReadTokens!==void 0&&(u+=d.cacheReadTokens),d.cacheWriteTokens!==void 0&&(n+=d.cacheWriteTokens)}}const k={label:"All sessions",costAmount:e,deltaCost:e,sessionCount:g};if(t.tokens===!0&&(k.inputTokens=s,k.outputTokens=i,k.cacheReadTokens=u,k.cacheWriteTokens=n),t.loc===!0){let a=0,p=0;for(const o of c){const d=o.locByFiletype;if(d!==void 0)for(const m of Object.keys(d)){const b=d[m];b!==void 0&&(a+=b.added,p+=b.removed)}}k.linesAdded=a,k.linesRemoved=p}return{kind:"total",row:k,currency:y}}if(t.by!==void 0&&t.bucket===void 0){const e=new Map;for(const i of c){const u=E(i);let n=e.get(u);if(n===void 0&&(n={label:u,rows:{label:u,costAmount:0,deltaCost:0,sessionCount:0},sessions:new Set},e.set(u,n)),t.tokens===!0&&(n.rows.inputTokens===void 0&&(n.rows.inputTokens=0),n.rows.inputTokens+=i.contextTokens),t.loc===!0){const k=i.locByFiletype;if(k!==void 0){n.rows.linesAdded===void 0&&(n.rows.linesAdded=0),n.rows.linesRemoved===void 0&&(n.rows.linesRemoved=0);for(const a of Object.keys(k)){const p=k[a];p!==void 0&&(n.rows.linesAdded+=p.added,n.rows.linesRemoved+=p.removed)}}}const g=M.get(i.sessionId);if(g===void 0||g.length<2)n.rows.costAmount+=i.costAmount,n.rows.deltaCost+=i.costAmount,n.sessions.add(i.sessionId);else{let a=g[0].cumulativeCost,p=!1;for(let o=1;o<g.length;o++){const d=g[o];if(d===void 0)continue;const m=Math.max(0,d.cumulativeCost-a);a=d.cumulativeCost,!(l!==void 0&&new Date(d.ts)<l)&&(n.rows.costAmount+=m,n.rows.deltaCost+=m,p=!0)}p&&n.sessions.add(i.sessionId)}if(t.tokens===!0){const k=C.get(i.sessionId);if(k!==void 0)for(const a of k)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),a.inputTokens!==void 0&&(n.rows.inputTokens+=a.inputTokens),a.outputTokens!==void 0&&(n.rows.outputTokens+=a.outputTokens),a.cacheReadTokens!==void 0&&(n.rows.cacheReadTokens+=a.cacheReadTokens),a.cacheWriteTokens!==void 0&&(n.rows.cacheWriteTokens+=a.cacheWriteTokens)}}const s=[];for(const i of e.values())i.rows.sessionCount=i.sessions.size,s.push({label:i.label,items:[i.rows],sessionCount:i.sessions.size});return{kind:"grouped",groups:s,currency:y}}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},O=(e,s,i)=>{const u=I(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+=i,n.deltaCost+=i,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 o of c)e.add(o.sessionId);const s=new Map;for(const o of c)s.set(o.sessionId,o);const i=(o,d)=>{let m=o.get(d);return m===void 0&&(m={bucket:d,costAmount:0,deltaCost:0,sessionCount:0,linesAdded:0,linesRemoved:0},o.set(d,m)),(m.linesAdded??void 0)===void 0&&(m.linesAdded=0),(m.linesRemoved??void 0)===void 0&&(m.linesRemoved=0),m},u=(o,d)=>{o.linesAdded=(o.linesAdded??0)+d.linesAdded,o.linesRemoved=(o.linesRemoved??0)+d.linesRemoved,o._sessions===void 0&&(o._sessions=new Set),o._sessions.add(d.sessionId),o.sessionCount=o._sessions.size},n=o=>{delete o._sessions},g=S??[];if(t.by==="filetype"){const o=new Map,d=new Set;for(const b of g){if(!e.has(b.sessionId)||l!==void 0&&new Date(b.ts)<l)continue;let v=o.get(b.filetype);v===void 0&&(v={label:b.filetype,buckets:new Map,sessions:new Set},o.set(b.filetype,v));const T=i(v.buckets,I(b.ts));u(T,b),v.sessions.add(b.sessionId),d.add(b.sessionId)}const m=[];for(const b of o.values()){const v=Array.from(b.buckets.values());for(const T of v)n(T);v.sort(R),v.length>0&&m.push({label:b.label,items:v,sessionCount:b.sessions.size})}return{kind:"timeSeriesGrouped",groups:m,currency:y,totalSessions:d.size}}if(t.by!==void 0){const o=new Map;for(const m of g){if(!e.has(m.sessionId)||l!==void 0&&new Date(m.ts)<l)continue;const b=s.get(m.sessionId);if(b===void 0)continue;const v=E(b);let T=o.get(v);T===void 0&&(T={label:v,buckets:new Map,sessions:new Set},o.set(v,T));const P=i(T.buckets,I(m.ts));u(P,m),T.sessions.add(m.sessionId)}const d=[];for(const m of o.values()){const b=Array.from(m.buckets.values());for(const v of b)n(v);b.sort(R),b.length>0&&d.push({label:m.label,items:b,sessionCount:m.sessions.size})}return{kind:"timeSeriesGrouped",groups:d,currency:y}}const k=new Map,a=new Set;for(const o of g){if(!e.has(o.sessionId)||l!==void 0&&new Date(o.ts)<l)continue;const d=i(k,I(o.ts));u(d,o),a.add(o.sessionId)}const p=Array.from(k.values());for(const o of p)n(o);return p.sort(R),{kind:"timeSeries",timeSeries:p,currency:y,totalSessions:a.size}}if(t.by!==void 0&&t.bucket!==void 0){const e=new Map,s=u=>{const n=E(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 c){const n=M.get(u.sessionId);if(n===void 0||n.length===0)continue;const g=s(u),k=g.buckets;g.sessions.add(u.sessionId);const a=n[0];if(a===void 0)continue;let p=a.cumulativeCost;for(let o=1;o<n.length;o++){const d=n[o];if(d===void 0)continue;const m=Math.max(0,d.cumulativeCost-p);p=d.cumulativeCost,!(l!==void 0&&new Date(d.ts)<l)&&O(k,d,m)}}const i=[];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&&i.push({label:u.label,items:n,sessionCount:u.sessions.size})}return{kind:"timeSeriesGrouped",groups:i,currency:y}}const W=new Map,z=new Set;for(const e of c){const s=M.get(e.sessionId);if(s===void 0||s.length===0)continue;const i=s[0];if(i===void 0)continue;let u=i.cumulativeCost,n=!1;for(let g=1;g<s.length;g++){const k=s[g];if(k===void 0)continue;const a=Math.max(0,k.cumulativeCost-u);u=k.cumulativeCost,!(l!==void 0&&new Date(k.ts)<l)&&(O(W,k,a),n=!0)}n&&z.add(e.sessionId)}const F=Array.from(W.values());for(const e of F)G(e);return F.sort(R),{kind:"timeSeries",timeSeries:F,currency:y,totalSessions:z.size}}export{ne as aggregate,J as applyFilters,ee as parseSince};
@@ -1,2 +1,3 @@
1
- import{readFileSync as h}from"node:fs";import{homedir as k}from"node:os";import{resolve as m}from"node:path";import{logger as S}from"../util/log.js";const d=S("cost/daemon-client");function I(){if(process.env.HYDRA_ACP_TOKEN)return process.env.HYDRA_ACP_TOKEN;const e=m(process.env.HYDRA_ACP_HOME??m(k(),".hydra-acp"),"auth-token");try{const t=h(e,"utf8").trim();if(t.length>0)return t}catch(t){const n=t;n.code!=="ENOENT"&&d.debug(`auth-token read failed: ${n.message}`)}}function U(){if(process.env.HYDRA_ACP_DAEMON_URL)return process.env.HYDRA_ACP_DAEMON_URL;const e=m(process.env.HYDRA_ACP_HOME??m(k(),".hydra-acp"),"daemon.pid");try{const t=h(e,"utf8"),n=JSON.parse(t);if(typeof n.host=="string"&&typeof n.port=="number")return`http://${n.host}:${n.port}`}catch(t){const n=t;n.code!=="ENOENT"&&d.debug(`daemon.pid read failed: ${n.message}`)}return"http://127.0.0.1:8765"}function l(){const e=I();if(e!==void 0)return{daemonUrl:U(),token:e}}function T(e){const t=typeof e.sessionId=="string"?e.sessionId:void 0;if(t===void 0)return;const n=typeof e.updatedAt=="string"?e.updatedAt:"",s=(e._meta??{})["hydra-acp"]??{},c=typeof e.cwd=="string"?e.cwd:typeof s.cwd=="string"?s.cwd:void 0,o=typeof e.agentId=="string"?e.agentId:typeof s.agentId=="string"?s.agentId:"",i=typeof e.currentModel=="string"?e.currentModel:typeof s.currentModel=="string"?s.currentModel:"",a=typeof e.title=="string"?e.title:"",u=e.interactive===!0||s.interactive===!0,p=typeof e.importedFromMachine=="string"?e.importedFromMachine:typeof s.importedFromMachine=="string"?s.importedFromMachine:"",A=p.length>0?p:void 0,y=typeof e.upstreamSessionId=="string"?e.upstreamSessionId:typeof s.upstreamSessionId=="string"?s.upstreamSessionId:"",b=y.length>0?y:void 0,v=e.currentUsage,$=s.currentUsage,f=v??$,R=typeof f?.costAmount=="number"?f.costAmount:0,E=typeof f?.costCurrency=="string"?f.costCurrency:"",C=typeof f?.used=="number"?f.used:0;return{sessionId:t,cwd:c,agentId:o,model:i,interactive:u,costAmount:R,costCurrency:E,contextTokens:C,title:a,createdAt:"",updatedAt:n,importedFromMachine:A,upstreamSessionId:b}}async function w(){const e=l();if(e===void 0){d.debug("no daemon token resolved; skipping daemon fetch");return}const t=`${e.daemonUrl.replace(/\/$/,"")}/v1/sessions?includeNonInteractive=1`;let n;try{n=await fetch(t,{headers:{Authorization:`Bearer ${e.token}`}})}catch(o){d.debug(`daemon fetch failed (${t}): ${o.message}`);return}if(!n.ok){d.debug(`daemon returned HTTP ${n.status} for ${t}`);return}let r;try{r=await n.json()}catch(o){d.debug(`daemon response not JSON: ${o.message}`);return}const s=r;if(!Array.isArray(s.sessions)){d.debug("daemon response missing sessions[]");return}const c=[];for(const o of s.sessions)if(o&&typeof o=="object"&&!Array.isArray(o)){const i=T(o);i!==void 0&&c.push(i)}return c}function _(e){const t=typeof e.sessionId=="string"?e.sessionId:void 0,n=typeof e.ts=="string"?e.ts:void 0;if(t===void 0||n===void 0)return;const r=e.update??void 0,s=typeof r?.cost?.amount=="number"?r.cost.amount:0,c=typeof r?.cost?.currency=="string"?r.cost.currency:"USD",o=typeof r?.used=="number"?r.used:0;return{sessionId:t,ts:n,costCumulative:s,costCurrency:c,contextTokens:o}}async function D(e){const t=l();if(t===void 0)return;const n=new URLSearchParams({kinds:"usage_update"});e!==void 0&&n.set("since",e.toISOString());const r=`${t.daemonUrl.replace(/\/$/,"")}/v1/sessions/events?${n.toString()}`;let s;try{s=await fetch(r,{headers:{Authorization:`Bearer ${t.token}`}})}catch(i){d.debug(`events fetch failed: ${i.message}`);return}if(!s.ok){d.debug(`events endpoint HTTP ${s.status}`);return}const c=await s.text(),o=[];for(const i of c.split(`
2
- `)){const a=i.trim();if(a.length===0)continue;let u;try{u=JSON.parse(a)}catch{continue}if(u&&typeof u=="object"&&!Array.isArray(u)){const p=_(u);p!==void 0&&o.push(p)}}return o}const g=new Map;async function O(e,t){const n=`${e}|${t}`,r=g.get(n);if(r!==void 0)return r;const s=l();if(s===void 0)return g.set(n,null),null;const c=`${s.daemonUrl.replace(/\/$/,"")}/v1/sessions/${encodeURIComponent(e)}/tools/${encodeURIComponent(t)}`;let o;try{o=await fetch(c,{headers:{Authorization:`Bearer ${s.token}`}})}catch(a){return d.debug(`tool blob fetch failed: ${a.message}`),g.set(n,null),null}if(!o.ok)return d.debug(`tool blob endpoint HTTP ${o.status} for ${t}`),g.set(n,null),null;const i=await o.text();return g.set(n,i),i}export{O as fetchToolBlob,D as fetchUsageEventsFromDaemon,w as listSessionsFromDaemon};
1
+ import{readFileSync as k}from"node:fs";import{homedir as $}from"node:os";import{resolve as m}from"node:path";import{logger as I}from"../util/log.js";const d=I("cost/daemon-client");function T(){if(process.env.HYDRA_ACP_TOKEN)return process.env.HYDRA_ACP_TOKEN;const e=m(process.env.HYDRA_ACP_HOME??m($(),".hydra-acp"),"auth-token");try{const t=k(e,"utf8").trim();if(t.length>0)return t}catch(t){const n=t;n.code!=="ENOENT"&&d.debug(`auth-token read failed: ${n.message}`)}}function x(){if(process.env.HYDRA_ACP_DAEMON_URL)return process.env.HYDRA_ACP_DAEMON_URL;const e=m(process.env.HYDRA_ACP_HOME??m($(),".hydra-acp"),"daemon.pid");try{const t=k(e,"utf8"),n=JSON.parse(t);if(typeof n.host=="string"&&typeof n.port=="number")return`http://${n.host}:${n.port}`}catch(t){const n=t;n.code!=="ENOENT"&&d.debug(`daemon.pid read failed: ${n.message}`)}return"http://127.0.0.1:8765"}function l(){const e=T();if(e!==void 0)return{daemonUrl:x(),token:e}}function P(e){const t=typeof e.sessionId=="string"?e.sessionId:void 0;if(t===void 0)return;const n=typeof e.updatedAt=="string"?e.updatedAt:"",s=(e._meta??{})["hydra-acp"]??{},i=typeof e.cwd=="string"?e.cwd:typeof s.cwd=="string"?s.cwd:void 0,r=typeof e.agentId=="string"?e.agentId:typeof s.agentId=="string"?s.agentId:"",c=typeof e.currentModel=="string"?e.currentModel:typeof s.currentModel=="string"?s.currentModel:"",a=typeof e.title=="string"?e.title:"",u=e.interactive===!0||s.interactive===!0,p=typeof e.importedFromMachine=="string"?e.importedFromMachine:typeof s.importedFromMachine=="string"?s.importedFromMachine:"",A=p.length>0?p:void 0,y=typeof e.upstreamSessionId=="string"?e.upstreamSessionId:typeof s.upstreamSessionId=="string"?s.upstreamSessionId:"",b=y.length>0?y:void 0,h=typeof e.remote=="string"?e.remote:"",v=h.length>0?h:void 0,R=e.currentUsage,E=s.currentUsage,f=R??E,C=typeof f?.costAmount=="number"?f.costAmount:0,S=typeof f?.costCurrency=="string"?f.costCurrency:"",U=typeof f?.used=="number"?f.used:0;return{sessionId:t,cwd:i,agentId:r,model:c,interactive:u,costAmount:C,costCurrency:S,contextTokens:U,title:a,createdAt:"",updatedAt:n,importedFromMachine:A,upstreamSessionId:b,remote:v}}async function H(){const e=l();if(e===void 0){d.debug("no daemon token resolved; skipping daemon fetch");return}const t=`${e.daemonUrl.replace(/\/$/,"")}/v1/sessions?includeNonInteractive=1`;let n;try{n=await fetch(t,{headers:{Authorization:`Bearer ${e.token}`}})}catch(r){d.debug(`daemon fetch failed (${t}): ${r.message}`);return}if(!n.ok){d.debug(`daemon returned HTTP ${n.status} for ${t}`);return}let o;try{o=await n.json()}catch(r){d.debug(`daemon response not JSON: ${r.message}`);return}const s=o;if(!Array.isArray(s.sessions)){d.debug("daemon response missing sessions[]");return}const i=[];for(const r of s.sessions)if(r&&typeof r=="object"&&!Array.isArray(r)){const c=P(r);c!==void 0&&i.push(c)}return i}function _(e){const t=typeof e.sessionId=="string"?e.sessionId:void 0,n=typeof e.ts=="string"?e.ts:void 0;if(t===void 0||n===void 0)return;const o=e.update??void 0,s=typeof o?.cost?.amount=="number"?o.cost.amount:0,i=typeof o?.cost?.currency=="string"?o.cost.currency:"USD",r=typeof o?.used=="number"?o.used:0;return{sessionId:t,ts:n,costCumulative:s,costCurrency:i,contextTokens:r}}async function O(e){const t=l();if(t===void 0)return;const n=new URLSearchParams({kinds:"usage_update"});e!==void 0&&n.set("since",e.toISOString());const o=`${t.daemonUrl.replace(/\/$/,"")}/v1/sessions/events?${n.toString()}`;let s;try{s=await fetch(o,{headers:{Authorization:`Bearer ${t.token}`}})}catch(c){d.debug(`events fetch failed: ${c.message}`);return}if(!s.ok){d.debug(`events endpoint HTTP ${s.status}`);return}const i=await s.text(),r=[];for(const c of i.split(`
2
+ `)){const a=c.trim();if(a.length===0)continue;let u;try{u=JSON.parse(a)}catch{continue}if(u&&typeof u=="object"&&!Array.isArray(u)){const p=_(u);p!==void 0&&r.push(p)}}return r}const g=new Map;async function F(e,t){const n=`${e}|${t}`,o=g.get(n);if(o!==void 0)return o;const s=l();if(s===void 0)return g.set(n,null),null;const i=`${s.daemonUrl.replace(/\/$/,"")}/v1/sessions/${encodeURIComponent(e)}/tools/${encodeURIComponent(t)}`;let r;try{r=await fetch(i,{headers:{Authorization:`Bearer ${s.token}`}})}catch(a){return d.debug(`tool blob fetch failed: ${a.message}`),g.set(n,null),null}if(!r.ok)return d.debug(`tool blob endpoint HTTP ${r.status} for ${t}`),g.set(n,null),null;const c=await r.text();return g.set(n,c),c}async function j(e){const t=l();if(t===void 0)return;const n=`${t.daemonUrl.replace(/\/$/,"")}/v1/sessions/${encodeURIComponent(e)}/history`;let o;try{o=await fetch(n,{headers:{Authorization:`Bearer ${t.token}`}})}catch(i){d.debug(`session history fetch failed (${n}): ${i.message}`);return}if(!o.ok){d.debug(`session history endpoint HTTP ${o.status} for ${e}`);return}return(await o.text()).split(`
3
+ `).filter(i=>i.length>0)}export{j as fetchSessionHistory,F as fetchToolBlob,O as fetchUsageEventsFromDaemon,H as listSessionsFromDaemon};
@@ -1 +1 @@
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};
1
+ import{createInterface as O}from"node:readline";import{createReadStream as D,statSync as H}from"node:fs";import{resolve as j}from"node:path";import{transcriptLines as J}from"./history-files.js";import{logger as B}from"../util/log.js";import{sessionsDir as _}from"./session-store.js";import{filetypeForPath as F}from"./language.js";import{fetchSessionHistory as G,fetchToolBlob as M}from"./daemon-client.js";const T=B("cost/history-stream");function P(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*Z(t){const n=Array.isArray(t)?t:[t];for(const e of n){const d=j(_(),e.sessionId),r=new Map,o=e.costAmount>0?e.costAmount:void 0,i=1.5;let f=0,y=!1;for await(const c of J(d)){let u;try{u=JSON.parse(c)}catch{T.debug(`malformed JSON in history.jsonl for ${e.sessionId}`);continue}if(!u||typeof u!="object"||Array.isArray(u))continue;const l=u;if(l.method!=="session/update")continue;const g=l.params??void 0;if(!g||typeof g!="object"||Array.isArray(g))continue;const s=g.update??void 0;if(!s||typeof s!="object"||Array.isArray(s)||s.sessionUpdate!=="usage_update")continue;const p=$(l),a=P(s);if(a===void 0)continue;const k=a.currency??"",b=a.amount,h=r.get(e.sessionId)??0,A=Math.max(0,b-h);if(r.set(e.sessionId,b),o!==void 0&&f+A>o*i+1){T.debug(`${e.sessionId}: transcript series overshoots recorded lifetime (${(f+A).toFixed(2)} > ${o.toFixed(2)}); abandoning archive-derived events`),y=!0;break}f+=A;const m=s.usage??void 0;let w,I,R,v;if(m&&typeof m=="object"&&!Array.isArray(m)){const E=m.inputTokens;typeof E=="number"&&(w=E);const C=m.outputTokens;typeof C=="number"&&(I=C);const S=m.cacheReadInputTokens;typeof S=="number"&&(R=S);const x=m.cacheCreationInputTokens;typeof x=="number"&&(v=x)}yield{sessionId:e.sessionId,ts:p,deltaCost:A,cumulativeCost:b,currency:k,...w!==void 0&&{inputTokens:w},...I!==void 0&&{outputTokens:I},...R!==void 0&&{cacheReadTokens:R},...v!==void 0&&{cacheWriteTokens:v}}}}}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 M(t,e)??""}return""}function L(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*U(t){if(t.remote!==void 0){const r=await G(t.sessionId);if(r===void 0)return;for(const o of r)yield o;return}const n=j(_(),t.sessionId,"history.jsonl");try{H(n)}catch(r){const o=r;if(o.code==="ENOENT")return;T.debug(`stat failed for ${n}: ${o.message}`);return}const e=D(n,{encoding:"utf8"}),d=O({input:e,crlfDelay:1/0});try{for await(const r of d)yield r}finally{d.close(),e.destroy()}}async function*nn(t){const n=Array.isArray(t)?t:[t];for(const e of n){const d=new Map,r=[];for await(const o of U(e)){if(o.length===0)continue;let i;try{i=JSON.parse(o)}catch{continue}if(!i||typeof i!="object"||Array.isArray(i))continue;const f=i;if(f.method!=="session/update")continue;const y=f.params??void 0;if(!y||typeof y!="object"||Array.isArray(y))continue;const c=y.update??void 0;if(!c||typeof c!="object"||Array.isArray(c)||c.sessionUpdate!=="tool_call"&&c.sessionUpdate!=="tool_call_update")continue;const u=c.content;if(!Array.isArray(u)||u.length===0)continue;const l=typeof c.toolCallId=="string"?c.toolCallId:"";if(l==="")continue;const g=$(f);for(const s of u){if(!s||typeof s!="object"||Array.isArray(s))continue;const p=s;if(p.type!=="diff")continue;const a=typeof p.path=="string"?p.path:"";if(a==="")continue;const k=await N(e.sessionId,p.oldText),b=await N(e.sessionId,p.newText),h=`${l}|${a}`;d.has(h)||r.push(h),d.set(h,{sessionId:e.sessionId,ts:g,path:a,filetype:F(a),linesAdded:L(b),linesRemoved:L(k)})}}for(const o of r){const i=d.get(o);i!==void 0&&(yield i)}}}export{nn as streamHistoryEditEvents,Z as streamHistoryEvents};
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as F}from"node:fs";import{dirname as N,resolve as M}from"node:path";import{fileURLToPath as P}from"node:url";import{loadConfig as _}from"./config.js";import{BudgeterBridge as H}from"./bridge.js";import{DEFAULT_RULE as W}from"./rule.js";import{logger as j,setDebug as B}from"./util/log.js";import{scanSessions as G,enrichSessionsWithLoc as A}from"./cost/session-store.js";import{listSessionsFromDaemon as J,fetchUsageEventsFromDaemon as Y}from"./cost/daemon-client.js";import{streamHistoryEditEvents as q}from"./cost/history-stream.js";import{aggregate as V,applyFilters as z,parseSince as K}from"./cost/aggregate.js";import{renderText as Z,renderJson as Q}from"./cost/format.js";const C=j("main");function D(){try{const e=N(P(import.meta.url));return JSON.parse(F(M(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}function X(){process.stderr.write(`hydra-acp-budgeter: 'reset' as a CLI subcommand is no longer supported.
2
+ import{readFileSync as M}from"node:fs";import{dirname as P,resolve as _}from"node:path";import{fileURLToPath as H}from"node:url";import{loadConfig as W}from"./config.js";import{BudgeterBridge as j}from"./bridge.js";import{DEFAULT_RULE as B}from"./rule.js";import{logger as G,setDebug as A}from"./util/log.js";import{scanSessions as J,enrichSessionsWithLoc as Y}from"./cost/session-store.js";import{listSessionsFromDaemon as q,fetchUsageEventsFromDaemon as V}from"./cost/daemon-client.js";import{streamHistoryEditEvents as z}from"./cost/history-stream.js";import{aggregate as K,applyFilters as C,parseSince as Z}from"./cost/aggregate.js";import{renderText as Q,renderJson as X}from"./cost/format.js";const D=G("main");function T(){try{const e=P(H(import.meta.url));return JSON.parse(M(_(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}function ee(){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
- `),process.exit(2)}const ee=`Usage: hydra budgeter usage [OPTIONS]
5
+ `),process.exit(2)}const te=`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)
@@ -14,23 +14,24 @@ Options:
14
14
  --dir <path> Only include sessions under this directory prefix
15
15
  --interactive Only include interactive sessions (default: include both)
16
16
  --host <name|local|all> Filter sessions by host. "local" (default) shows sessions
17
- created here plus imports attached locally; "all" includes
18
- every session; <name> shows passive mirrors imported from
19
- that host.
17
+ created here plus imports attached locally, excluding
18
+ anything live on a federated remote; "all" includes every
19
+ session; <name> shows sessions live on the hydra remote
20
+ named <name>, plus passive mirrors imported from that host.
20
21
  --min <N> Drop sessions whose active-metric value is <= N (default: 0)
21
22
  --histogram Show an ASCII histogram bar next to each row (default on; implies --bucket hour if no bucket given)
22
23
  --no-histogram Collapse to a single total row (also drops the default hourly bucket)
23
24
  --metric <cost|tokens|loc> Display metric (default: cost). "loc" counts net lines of code
24
25
  from Edit/Write tool diffs in session history.
25
26
  --json Output as JSON
26
- --help Show this help message`;async function T(e){if(e.includes("--help")){process.stdout.write(ee+`
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=te(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}
27
+ --help Show this help message`;async function x(e){if(e.includes("--help")){process.stdout.write(te+`
28
+ `);return}let i,s="hour",r,a,f,w=!1,h,u=!0,o,k=!1,l,m=!1,p=!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=se(e[t]),m=!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,f=e[t];else if(n==="--interactive")w=!0;else if(n==="--min")t+=1,h=e[t];else if(n==="--histogram")u=!0,p=!0;else if(n==="--no-histogram")u=!1,p=!0;else if(n==="--metric")t+=1,o=e[t];else if(n==="--json")k=!0;else if(n==="--host")t+=1,l=e[t];else if(n.startsWith("--host="))l=n.slice(7);else if(n.startsWith("--"))throw new Error(`Unknown option: ${n}
28
29
  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'.
29
30
  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
31
  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=K(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 J()??G();(d||r==="filetype")&&await A(E);const S=z(E,{since:c,dir:l,interactive:b,min:v,minMetric:d?"loc":g?"tokens":"cost",host:h??"local"});let R;const $=await Y();$!==void 0&&(R=$.map(t=>({sessionId:t.sessionId,ts:t.ts,deltaCost:0,cumulativeCost:t.costCumulative,currency:t.costCurrency,inputTokens:t.contextTokens})));let I;if(d&&s!==void 0){const t=[];for await(const n of q(S))t.push(n);I=t}const x=a!==void 0?parseInt(a,10):void 0,L=V(S,R,{by:r,depth:x,bucket:s,since:c,interactive:b,dir:l,tokens:g,loc:d,min:v},I),O=se({sinceRaw:i,effectiveSince:c,bucket:s});if(k)process.stdout.write(Q(L)+`
33
- `);else{const t=Z(L,{histogram:u,tokens:g,loc:d,windowLabel:O});process.stdout.write(t)}}function te(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 se(e){if(e.sinceRaw!==void 0){const i=ne(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 ne(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 ie(){const e=_();B(e.debug);const i=new H({daemonWsUrl:e.hydraWsUrl,daemonHttpBase:e.hydraDaemonUrl,token:e.hydraToken,softLimit:e.softLimit,hardLimit:e.hardLimit,currency:e.currency,rule:W});i.start();const s=r=>{C.info(`${r} received \u2014 shutting down`),i.stop(),setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>s("SIGINT")),process.on("SIGTERM",()=>s("SIGTERM")),C.info(`hydra-acp-budgeter up; daemon=${e.hydraDaemonUrl} soft=${e.softLimit} hard=${e.hardLimit} ${e.currency} state=per-session (extension_state)`)}function re(){process.stdout.write(`hydra-acp-budgeter ${D()}
32
+ 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}
33
+ Run "hydra budgeter usage --help" for usage.`)}r!==void 0&&(m||(s=void 0),p||(u=!1)),!u&&!m&&(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=h!==void 0?parseFloat(h):void 0,O=await q()??J(),E=C(O,{host:l??"local"});(d||r==="filetype")&&await Y(E);const S=C(E,{since:c,dir:f,interactive:b,min:v,minMetric:d?"loc":g?"tokens":"cost",host:l??"local"});let R;const $=await V();$!==void 0&&(R=$.map(t=>({sessionId:t.sessionId,ts:t.ts,deltaCost:0,cumulativeCost:t.costCumulative,currency:t.costCurrency,inputTokens:t.contextTokens})));let I;if(d&&s!==void 0){const t=[];for await(const n of z(S))t.push(n);I=t}const U=a!==void 0?parseInt(a,10):void 0,L=K(S,R,{by:r,depth:U,bucket:s,since:c,interactive:b,dir:f,tokens:g,loc:d,min:v},I),F=ne({sinceRaw:i,effectiveSince:c,bucket:s});if(k)process.stdout.write(X(L)+`
34
+ `);else{const t=Q(L,{histogram:u,tokens:g,loc:d,windowLabel:F});process.stdout.write(t)}}function se(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 ne(e){if(e.sinceRaw!==void 0){const i=ie(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 ie(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 re(){const e=W();A(e.debug);const i=new j({daemonWsUrl:e.hydraWsUrl,daemonHttpBase:e.hydraDaemonUrl,token:e.hydraToken,softLimit:e.softLimit,hardLimit:e.hardLimit,currency:e.currency,rule:B});i.start();const s=r=>{D.info(`${r} received \u2014 shutting down`),i.stop(),setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>s("SIGINT")),process.on("SIGTERM",()=>s("SIGTERM")),D.info(`hydra-acp-budgeter up; daemon=${e.hydraDaemonUrl} soft=${e.softLimit} hard=${e.hardLimit} ${e.currency} state=per-session (extension_state)`)}function oe(){process.stdout.write(`hydra-acp-budgeter ${T()}
34
35
 
35
36
  Usage:
36
37
  hydra budgeter [usage] <flags> Report historical cost/usage across sessions
@@ -40,6 +41,6 @@ Flags:
40
41
  -v, --version Print version and exit
41
42
  repair Reconcile recorded cost against the agent ledger
42
43
  -h, --help Show this help
43
- `)}async function oe(){const e=process.argv.slice(2);if(e.includes("--version")||e.includes("-v")){process.stdout.write(`hydra-acp-budgeter ${D()}
44
- `);return}if((e[0]==="help"||e.includes("--help")||e.includes("-h"))&&e[0]!=="usage"&&e[0]!=="cost"&&e[0]!=="repair"){re();return}if(e[0]==="reset"){X();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 T(e.slice(1));return}if(e[0]==="run"||process.env.HYDRA_ACP_TOKEN){await ie();return}await T(e)}oe().catch(e=>{process.stderr.write(`hydra-acp-budgeter: ${e.message}
44
+ `)}async function ae(){const e=process.argv.slice(2);if(e.includes("--version")||e.includes("-v")){process.stdout.write(`hydra-acp-budgeter ${T()}
45
+ `);return}if((e[0]==="help"||e.includes("--help")||e.includes("-h"))&&e[0]!=="usage"&&e[0]!=="cost"&&e[0]!=="repair"){oe();return}if(e[0]==="reset"){ee();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 x(e.slice(1));return}if(e[0]==="run"||process.env.HYDRA_ACP_TOKEN){await re();return}await x(e)}ae().catch(e=>{process.stderr.write(`hydra-acp-budgeter: ${e.message}
45
46
  `),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/budgeter",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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",
@@ -22,12 +22,12 @@
22
22
  "watch": "tsup --watch",
23
23
  "start": "node dist/index.js",
24
24
  "dev": "tsup && node dist/index.js",
25
- "test": "node --test --import tsx 'test/**/*.test.ts'",
25
+ "test": "node scripts/run-tests.mjs",
26
26
  "lint": "tsc -p . --noEmit",
27
27
  "prepublishOnly": "npm run build && npm test"
28
28
  },
29
29
  "engines": {
30
- "node": ">=20"
30
+ "node": ">=22.5.0"
31
31
  },
32
32
  "dependencies": {
33
33
  "ws": "^8.20.0"