@hydra-acp/budgeter 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,3 +142,114 @@ The file is optional — all keys have defaults and the transformer works withou
142
142
  - All cost state is in-memory; restart the transformer to reset.
143
143
 
144
144
  For a working example of the transformer protocol the budgeter speaks, see [`hydra-acp/cli/examples/transformer-observe.mjs`](https://github.com/smagnuso/hydra-acp/blob/main/cli/examples/transformer-observe.mjs).
145
+
146
+ ## Reporting historical cost
147
+
148
+ The `cost` subcommand reads session metadata from `~/.hydra-acp/sessions/<id>/meta.json` and optionally streams `history.jsonl` for time-bucketed or token-level queries. It is a pure reader — no new files are written.
149
+
150
+ ### Quick start
151
+
152
+ ```sh
153
+ # All-time total cost across all sessions
154
+ hydra budgeter usage
155
+
156
+ # JSON output (machine-readable)
157
+ hydra budgeter usage --json
158
+ ```
159
+
160
+ ### Time-based queries
161
+
162
+ ```sh
163
+ # Last 7 days
164
+ hydra budgeter usage --since 7d
165
+
166
+ # Last 30 days, grouped by day buckets
167
+ hydra budgeter usage --since 30d --bucket day
168
+
169
+ # Last 6 months, grouped by week
170
+ hydra budgeter usage --since 180d --bucket week
171
+
172
+ # Calendar-month buckets over the last 2 years
173
+ hydra budgeter usage --bucket month
174
+ ```
175
+
176
+ ### Grouping
177
+
178
+ ```sh
179
+ # By directory (depth-1 below $HOME)
180
+ hydra budgeter usage --by dir
181
+
182
+ # By session ID
183
+ hydra budgeter usage --by session
184
+
185
+ # By model
186
+ hydra budgeter usage --by model
187
+
188
+ # By agent
189
+ hydra budgeter usage --by agent
190
+
191
+ # Directory with custom depth
192
+ hydra budgeter usage --by dir --depth 2
193
+ ```
194
+
195
+ ### Filtering
196
+
197
+ ```sh
198
+ # Only interactive sessions
199
+ hydra budgeter usage --interactive
200
+
201
+ # Only non-interactive (background) sessions
202
+ hydra budgeter usage --no-interactive
203
+
204
+ # Only sessions under a specific directory prefix
205
+ hydra budgeter usage --dir ~/dev/hydra-acp
206
+
207
+ # Combine filters
208
+ hydra budgeter usage --since 7d --dir ~/dev/hydra-acp --by session
209
+ ```
210
+
211
+ ### Token-level queries
212
+
213
+ ```sh
214
+ # Total tokens across all sessions
215
+ hydra budgeter usage --metric tokens
216
+
217
+ # Tokens with histogram
218
+ hydra budgeter usage --metric tokens --histogram
219
+ ```
220
+
221
+ ### Output formats
222
+
223
+ **Text (default):**
224
+
225
+ ```
226
+ Total: $12.34 across 5 session(s)
227
+ ──────────────────────────────────────────────────────────────────────────────
228
+ Label Cost Tokens
229
+ myapp $7.50 142k
230
+ other $4.84 89k
231
+ ```
232
+
233
+ **JSON (`--json`):**
234
+
235
+ ```json
236
+ {
237
+ "kind": "grouped",
238
+ "currency": "USD",
239
+ "groups": [
240
+ {
241
+ "label": "myapp",
242
+ "items": [{ "label": "myapp", "costAmount": 7.50, "deltaCost": 2.30 }]
243
+ },
244
+ {
245
+ "label": "other",
246
+ "items": [{ "label": "other", "costAmount": 4.84, "deltaCost": 1.10 }]
247
+ }
248
+ ]
249
+ }
250
+ ```
251
+
252
+ ### Fast path vs slow path
253
+
254
+ - **Fast path** — `--by dir/session/model/agent` without `--since`, `--dir`, or `--interactive`: reads only `meta.json` across all sessions, returns instantly even with ~1000 sessions.
255
+ - **Slow path** — any query with `--since`, `--bucket`, `--metric tokens`, `--dir`, or `--interactive`: also streams `history.jsonl` for delta-cost and token computation, pre-filtering sessions by `updatedAt`.
@@ -0,0 +1 @@
1
+ import{relative as G,resolve as w}from"node:path";import{homedir as W}from"node:os";import{realpathSync as E}from"node:fs";function P(c){const d=c.match(/^(\d+)\s*([dhmwy])$/i);if(d!==null){const a=d[1],h=d[2];if(a===void 0||h===void 0)throw new Error(`invalid since spec: ${c}`);const k=parseInt(a,10),r=h.toLowerCase(),u=new Date;return r==="d"?u.setDate(u.getDate()-k):r==="h"?u.setHours(u.getHours()-k):r==="w"?u.setDate(u.getDate()-k*7):r==="m"?u.setMonth(u.getMonth()-k):r==="y"&&u.setFullYear(u.getFullYear()-k),u}const n=new Date(c);if(Number.isNaN(n.getTime()))throw new Error(`invalid since spec: ${c}`);return n}const v=new Map;function C(c){if(v.has(c))return v.get(c);try{const d=E(c);return v.set(c,d),d}catch{return v.set(c,c),c}}function F(c,d){let n=c;if(d.since!==void 0){const r=d.since.getTime(),u=[];for(const f of n)new Date(f.updatedAt).getTime()<r||u.push(f);n=u}if(d.dir!==void 0){const r=C(w(d.dir)),u=[];for(const f of n)f.cwd===void 0||f.cwd===""||(f.cwd.startsWith(r+"/")||f.cwd===r)&&u.push(f);n=u}if(d.interactive!==void 0){const r=d.interactive,u=[];for(const f of n)f.interactive===r&&u.push(f);n=u}const a=d.min??0,h=d.minMetric==="tokens",k=[];for(const r of n)(h?r.contextTokens:r.costAmount)>a&&k.push(r);return k}function O(c){return c.dir!==void 0?w(c.dir):w(W())}function L(c,d,n){if(c===void 0||c==="")return"<unknown>";const a=C(w(c));if(d===void 0){const p=C(w(W()));return a===p?"~":a.startsWith(p+"/")?"~/"+a.slice(p.length+1):a}const h=C(w(n));let k=G(h,a);if(k===""||k===".")return"<root>";if(k.startsWith(".."))return"<unknown>";const r=k.split("/");if(r.length===0)return"<root>";const u=Math.min(d,r.length),f=r.slice(0,u);return f.length===0?"<root>":f.join("/")}function Y(c,d,n={}){let a=n.since;if(a===void 0&&n.bucket!==void 0){const e=new Date;n.bucket==="hour"?(e.setHours(e.getHours()-24),a=e):n.bucket==="day"?(e.setDate(e.getDate()-30),a=e):n.bucket==="week"?(e.setMonth(e.getMonth()-6),a=e):n.bucket==="month"&&(e.setFullYear(e.getFullYear()-2),a=e)}if(!(a!==void 0||n.bucket!==void 0||n.tokens===!0||n.by!==void 0||n.dir!==void 0||n.interactive!==void 0)){let e=0;for(const i of c)e+=i.costAmount;let s="";for(const i of c){s=i.costCurrency;break}return{kind:"total",row:{label:"All sessions",costAmount:e,sessionCount:c.length},currency:s}}const k={since:a,dir:n.dir,interactive:n.interactive,min:n.min,minMetric:n.tokens===!0?"tokens":"cost"},r=F(c,k),u=new Map;if(d!==void 0)for(const e of d){const s=u.get(e.sessionId);s===void 0?u.set(e.sessionId,[e]):s.push(e)}let f="";if(d!==void 0&&d.length>0)for(const e of d){f=e.currency;break}else if(r.length>0)for(const e of r){f=e.costCurrency;break}const p=e=>n.by==="dir"?L(e.cwd,n.depth,O(n)):n.by==="session"?e.sessionId.startsWith("hydra_session_")?e.sessionId.slice(14):e.sessionId:n.by==="model"?e.model===""?"<unknown>":e.model:n.by==="agent"?e.agentId===""?"<unknown>":e.agentId:"<all>",R=e=>{const s=new Date(e);if(Number.isNaN(s.getTime()))return"<invalid>";if(n.bucket==="hour"){const i=s.toLocaleDateString(void 0,{month:"2-digit",day:"2-digit"}),o=String(s.getHours()).padStart(2,"0");return`${i} ${o}:00`}if(n.bucket==="day")return s.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"});if(n.bucket==="week"){const i=new Date(s),o=i.getDay(),t=i.getDate()-o+(o===0?-6:1);return i.setDate(t),i.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"})}return n.bucket==="month"?s.toLocaleDateString(void 0,{year:"numeric",month:"2-digit"}):s.toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit"})};if(n.by===void 0&&n.bucket===void 0){let e=0,s=0,i=0,o=0,t=0,l=0;for(const b of r){e+=b.costAmount;const T=u.get(b.sessionId);if(T!==void 0)for(const m of T)s+=m.deltaCost,n.tokens===!0&&(m.inputTokens!==void 0&&(i+=m.inputTokens),m.outputTokens!==void 0&&(o+=m.outputTokens),m.cacheReadTokens!==void 0&&(t+=m.cacheReadTokens),m.cacheWriteTokens!==void 0&&(l+=m.cacheWriteTokens))}const g={label:"All sessions",costAmount:e,deltaCost:s,sessionCount:r.length};return n.tokens===!0&&(g.inputTokens=i,g.outputTokens=o,g.cacheReadTokens=t,g.cacheWriteTokens=l),{kind:"total",row:g,currency:f}}if(n.by!==void 0&&n.bucket===void 0){const e=new Map;for(const i of r){const o=p(i);let t=e.get(o);t===void 0&&(t={label:o,rows:{label:o,costAmount:0,deltaCost:0,sessionCount:0}},e.set(o,t)),t.rows.costAmount+=i.costAmount,t.rows.sessionCount=(t.rows.sessionCount??0)+1,n.tokens===!0&&(t.rows.inputTokens===void 0&&(t.rows.inputTokens=0),t.rows.inputTokens+=i.contextTokens);const l=u.get(i.sessionId);if(l!==void 0)for(const g of l)t.rows.deltaCost+=g.deltaCost,n.tokens===!0&&(t.rows.inputTokens===void 0&&(t.rows.inputTokens=0),t.rows.outputTokens===void 0&&(t.rows.outputTokens=0),t.rows.cacheReadTokens===void 0&&(t.rows.cacheReadTokens=0),t.rows.cacheWriteTokens===void 0&&(t.rows.cacheWriteTokens=0),g.inputTokens!==void 0&&(t.rows.inputTokens+=g.inputTokens),g.outputTokens!==void 0&&(t.rows.outputTokens+=g.outputTokens),g.cacheReadTokens!==void 0&&(t.rows.cacheReadTokens+=g.cacheReadTokens),g.cacheWriteTokens!==void 0&&(t.rows.cacheWriteTokens+=g.cacheWriteTokens))}const s=[];for(const i of e.values())s.push({label:i.label,items:[i.rows]});return{kind:"grouped",groups:s,currency:f}}const _=(e,s)=>{n.tokens===!0&&(e.inputTokens===void 0&&(e.inputTokens=0),e.inputTokens+=s.contextTokens)},M=new Map;for(const e of r)M.set(e.sessionId,e);const y=new Map;if(d!==void 0){for(const e of d){if(!M.has(e.sessionId))continue;const s=y.get(e.sessionId);s===void 0?y.set(e.sessionId,[e]):s.push(e)}for(const e of y.values())e.sort((s,i)=>s.ts.localeCompare(i.ts))}const D=(e,s)=>{e._sessions===void 0&&(e._sessions=new Set),e._sessions.add(s),e.sessionCount=e._sessions.size},B=e=>{delete e._sessions},I=(e,s,i)=>{const o=R(s.ts);let t=e.get(o);t===void 0&&(t={bucket:o,costAmount:0,deltaCost:0,sessionCount:0},e.set(o,t)),t.costAmount+=i,t.deltaCost+=i,D(t,s.sessionId),n.tokens===!0&&(t.inputTokens===void 0||(s.inputTokens??0)>t.inputTokens)&&(t.inputTokens=s.inputTokens??t.inputTokens??0)},x=(e,s)=>{const i=R(s.updatedAt);let o=e.get(i);o===void 0&&(o={bucket:i,costAmount:0,deltaCost:0,sessionCount:0},e.set(i,o)),o.costAmount+=s.costAmount,o.deltaCost+=s.costAmount,D(o,s.sessionId),_(o,s)};if(n.by!==void 0&&n.bucket!==void 0){const e=new Map,s=o=>{const t=p(o);let l=e.get(t);return l===void 0&&(l={label:t,buckets:new Map},e.set(t,l)),l.buckets};for(const o of r){const t=s(o),l=y.get(o.sessionId);if(l!==void 0&&l.length>0){let g=l[0].cumulativeCost;for(let b=1;b<l.length;b++){const T=l[b],m=Math.max(0,T.cumulativeCost-g);g=T.cumulativeCost,!(a!==void 0&&new Date(T.ts)<a)&&I(t,T,m)}}else x(t,o)}const i=[];for(const o of e.values()){const t=Array.from(o.buckets.values());for(const l of t)B(l);t.sort((l,g)=>l.bucket.localeCompare(g.bucket)),t.length>0&&i.push({label:o.label,items:t})}return{kind:"timeSeriesGrouped",groups:i,currency:f}}const S=new Map;for(const e of r){const s=y.get(e.sessionId);if(s!==void 0&&s.length>0){let i=s[0].cumulativeCost;for(let o=1;o<s.length;o++){const t=s[o],l=Math.max(0,t.cumulativeCost-i);i=t.cumulativeCost,!(a!==void 0&&new Date(t.ts)<a)&&I(S,t,l)}}else x(S,e)}const A=Array.from(S.values());for(const e of A)B(e);return A.sort((e,s)=>e.bucket.localeCompare(s.bucket)),{kind:"timeSeries",timeSeries:A,currency:f}}export{Y as aggregate,F as applyFilters,P as parseSince};
@@ -0,0 +1,2 @@
1
+ import{readFileSync as m}from"node:fs";import{homedir as y}from"node:os";import{resolve as g}from"node:path";import{logger as E}from"../util/log.js";const i=E("cost/daemon-client");function b(){if(process.env.HYDRA_ACP_TOKEN)return process.env.HYDRA_ACP_TOKEN;const e=g(process.env.HYDRA_ACP_HOME??g(y(),".hydra-acp"),"auth-token");try{const t=m(e,"utf8").trim();if(t.length>0)return t}catch(t){const n=t;n.code!=="ENOENT"&&i.debug(`auth-token read failed: ${n.message}`)}}function R(){if(process.env.HYDRA_ACP_DAEMON_URL)return process.env.HYDRA_ACP_DAEMON_URL;const e=g(process.env.HYDRA_ACP_HOME??g(y(),".hydra-acp"),"daemon.pid");try{const t=m(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"&&i.debug(`daemon.pid read failed: ${n.message}`)}return"http://127.0.0.1:8765"}function l(){const e=b();if(e!==void 0)return{daemonUrl:R(),token:e}}function C(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"]??{},d=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:"",u=typeof e.currentModel=="string"?e.currentModel:typeof s.currentModel=="string"?s.currentModel:"",f=typeof e.title=="string"?e.title:"",c=e.interactive===!0||s.interactive===!0,p=e.currentUsage,h=s.currentUsage,a=p??h,k=typeof a?.costAmount=="number"?a.costAmount:0,A=typeof a?.costCurrency=="string"?a.costCurrency:"",v=typeof a?.used=="number"?a.used:0;return{sessionId:t,cwd:d,agentId:o,model:u,interactive:c,costAmount:k,costCurrency:A,contextTokens:v,title:f,createdAt:"",updatedAt:n}}async function T(){const e=l();if(e===void 0){i.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){i.debug(`daemon fetch failed (${t}): ${o.message}`);return}if(!n.ok){i.debug(`daemon returned HTTP ${n.status} for ${t}`);return}let r;try{r=await n.json()}catch(o){i.debug(`daemon response not JSON: ${o.message}`);return}const s=r;if(!Array.isArray(s.sessions)){i.debug("daemon response missing sessions[]");return}const d=[];for(const o of s.sessions)if(o&&typeof o=="object"&&!Array.isArray(o)){const u=C(o);u!==void 0&&d.push(u)}return d}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,d=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:d,contextTokens:o}}async function I(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(u){i.debug(`events fetch failed: ${u.message}`);return}if(!s.ok){i.debug(`events endpoint HTTP ${s.status}`);return}const d=await s.text(),o=[];for(const u of d.split(`
2
+ `)){const f=u.trim();if(f.length===0)continue;let c;try{c=JSON.parse(f)}catch{continue}if(c&&typeof c=="object"&&!Array.isArray(c)){const p=$(c);p!==void 0&&o.push(p)}}return o}export{I as fetchUsageEventsFromDaemon,T as listSessionsFromDaemon};
@@ -0,0 +1,16 @@
1
+ function h(e){return"label"in e?e.label:e.bucket}function R(e){let t=0,u=!1;for(const c of e){if(c===void 0)continue;const o=String(c);o.length>t&&(t=o.length),c!==1&&(u=!0)}const s=u?8:7,i=e.map(c=>{if(c===void 0)return"";const o=String(c).padStart(t),n=(c===1?"session":"sessions").padEnd(s);return`${o} ${n}`}),r=t+1+s;return{strs:i,width:r}}function g(e){if(e<1e3)return String(e);let t,u;return e<1e6?(t=e/1e3,u="k"):e<1e9?(t=e/1e6,u="M"):(t=e/1e9,u="B"),(t%1===0?t.toFixed(0):t.toFixed(2)).replace(/\.?0+$/,"")+u}function _(e,t){let u=0;for(const r of e)r>u&&(u=r);if(u<=0||t<=0)return e.map(()=>"");const s=Math.max(1,t),i=[];for(const r of e){const c=r/u,o=Math.round(c*s),n=s-o;i.push("\u2588".repeat(o)+"\u2591".repeat(n))}return i}function v(e,t={}){const u=process.stdout.columns??80,s=t.histogram===!0,i=t.tokens===!0;let r="";const c=o=>`${o} session${o===1?"":"s"}`;if(e.kind==="total"){const o=m(e.row.costAmount),n=e.row.sessionCount,l=n!==void 0?c(n):e.row.label??"all sessions";r+=`Total: ${o} across ${l}
2
+ `}else if(e.kind==="grouped"){const o=x(e.groups),n=m(o);let l=0;for(const a of e.groups)for(const d of a.items)l+=d.sessionCount??0;r+=`Total: ${n} across ${c(l)}
3
+ `}else if(e.kind==="timeSeries"){const o=e.timeSeries.reduce((a,d)=>a+d.costAmount,0),n=m(o),l=e.timeSeries.reduce((a,d)=>a+(d.sessionCount??0),0);r+=`Total: ${n} across ${c(l)}
4
+ `}else if(e.kind==="timeSeriesGrouped"){const o=x(e.groups),n=m(o);let l=0;for(const a of e.groups)for(const d of a.items)l+=d.sessionCount??0;r+=`Total: ${n} across ${c(l)}
5
+ `}if(e.kind==="grouped")if(e.groups.every(n=>n.items.length===1)){const n=[];for(const l of e.groups){const a=l.items[0];a!==void 0&&n.push({...a,label:l.label})}n.sort((l,a)=>{const d=i?p(l):l.costAmount;return(i?p(a):a.costAmount)-d}),s?r+=T(n,u,i):r+=w(n,i)}else for(const n of e.groups){r+=`
6
+ ${n.label}:
7
+ `;const l=n.items,a=l.some(d=>d.inputTokens!==void 0||d.outputTokens!==void 0);s?r+=T(l,u,i):r+=S(l,a,i)}else if(e.kind==="timeSeries"){const o=e.timeSeries.some(n=>n.inputTokens!==void 0||n.outputTokens!==void 0);s?r+=T(e.timeSeries,u,i):r+=S(e.timeSeries,o,i)}else if(e.kind==="timeSeriesGrouped")for(const o of e.groups){r+=`
8
+ ${o.label}:
9
+ `;const n=o.items.some(l=>l.inputTokens!==void 0||l.outputTokens!==void 0);s?r+=T(o.items,u,i):r+=S(o.items,n,i)}return r}function w(e,t){if(e.length===0)return` (no data)
10
+ `;let u=0;for(const n of e)n.label.length>u&&(u=n.label.length);const s=e.map(n=>t?g(p(n)):m(n.costAmount));let i=0;for(const n of s)n.length>i&&(i=n.length);const{strs:r,width:c}=R(e.map(n=>n.sessionCount)),o=[];for(let n=0;n<e.length;n++){const l=e[n];if(l===void 0)continue;let a=` ${l.label.padEnd(u)} ${s[n].padStart(i)}`;c>0&&(a+=` ${r[n]}`),o.push(a+`
11
+ `)}return o.join("")}function T(e,t,u){if(e.length===0)return` (no data)
12
+ `;let s=0;for(const f of e){const k=h(f);k.length>s&&(s=k.length)}const i=e.map(f=>u?p(f):f.costAmount),r=i.map(f=>u?g(f):m(f));let c=0;for(const f of r)f.length>c&&(c=f.length);const{strs:o,width:n}=R(e.map(f=>f.sessionCount)),l=n>0?n+2:0;let a=t-s-c-l-8;a<1&&(a=e.length>1?1:0);const d=_(i,a),b=[];for(let f=0;f<e.length;f++){const k=e[f];if(k===void 0)continue;const L=h(k).padEnd(s),W=r[f].padStart(c);let C=` ${L} ${W} ${d[f]}`;n>0&&(C+=` ${o[f]}`),b.push(C+`
13
+ `)}return b.join("")}function S(e,t,u){if(e.length===0)return` (no data)
14
+ `;let s=28;for(const r of e){const c=h(r);c.length>s&&(s=c.length)}let i=" ";i+=B("Label",s),i+=" ",u?i+=" Tokens":(i+=" Cost",t&&(i+=" Tokens")),i+=`
15
+ `;for(const r of e){const c=h(r).padEnd(s);let o=" ";if(o+=c,o+=" ",u){const n=p(r);o+=g(n).padStart(12)}else{const n=r.costAmount??0;if(o+=m(n).padStart(10),t){const l=p(r);o+=" ".padStart(2),o+=g(l).padStart(12)}}i+=o+`
16
+ `}return i}function G(e){const t={kind:e.kind,currency:e.currency};if(e.kind==="total")t.row=A(e.row);else if(e.kind==="grouped"){const u=e.groups.slice().sort((s,i)=>s.label.localeCompare(i.label));t.groups=u.map(s=>({label:s.label,items:s.items.map(A)}))}else if(e.kind==="timeSeries")t.timeSeries=e.timeSeries.map($);else if(e.kind==="timeSeriesGrouped"){const u=e.groups.slice().sort((s,i)=>s.label.localeCompare(i.label));t.groups=u.map(s=>({label:s.label,items:s.items.map($)}))}return JSON.stringify(t,null,2)}function A(e){const t={label:e.label,costAmount:e.costAmount};return e.deltaCost!==void 0&&(t.deltaCost=e.deltaCost),e.inputTokens!==void 0&&(t.inputTokens=e.inputTokens),e.outputTokens!==void 0&&(t.outputTokens=e.outputTokens),e.cacheReadTokens!==void 0&&(t.cacheReadTokens=e.cacheReadTokens),e.cacheWriteTokens!==void 0&&(t.cacheWriteTokens=e.cacheWriteTokens),t}function $(e){const t={bucket:e.bucket,costAmount:e.costAmount,deltaCost:e.deltaCost};return e.inputTokens!==void 0&&(t.inputTokens=e.inputTokens),e.outputTokens!==void 0&&(t.outputTokens=e.outputTokens),e.cacheReadTokens!==void 0&&(t.cacheReadTokens=e.cacheReadTokens),e.cacheWriteTokens!==void 0&&(t.cacheWriteTokens=e.cacheWriteTokens),t}function m(e){return`$${e.toFixed(2)}`}function p(e){let t=0;return e.inputTokens!==void 0&&(t+=e.inputTokens),e.outputTokens!==void 0&&(t+=e.outputTokens),t}function B(e,t){return e.length>=t?e:e+" ".repeat(t-e.length)}function x(e){let t=0;for(const u of e)for(const s of u.items)t+=s.costAmount;return t}export{G as renderJson,v as renderText};
@@ -0,0 +1 @@
1
+ import{createInterface as j}from"node:readline";import{createReadStream as E,statSync as N}from"node:fs";import{resolve as x}from"node:path";import{logger as O}from"../util/log.js";import{sessionsDir as $}from"./session-store.js";const b=O("cost/history-stream");function D(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 J(t){const n=D(t._meta),e=t.cost??void 0,s=n!==void 0?n:typeof e?.amount=="number"?e.amount:void 0;if(s===void 0)return;const u=typeof e?.currency=="string"?e.currency:void 0;return{amount:s,currency:u}}function M(t){const n=t.recordedAt;return typeof n=="number"?new Date(n).toISOString():typeof n=="string"?n:""}async function*L(t){const n=Array.isArray(t)?t:[t];for(const e of n){const s=x($(),e.sessionId,"history.jsonl");try{N(s)}catch(c){const o=c;if(o.code==="ENOENT"){b.debug(`no history for ${e.sessionId}`);continue}b.debug(`stat failed for ${s}: ${o.message}`);continue}const u=E(s,{encoding:"utf8"}),k=j({input:u,crlfDelay:1/0}),h=new Map;try{for await(const c of k){if(c.length===0)continue;let o;try{o=JSON.parse(c)}catch{b.debug(`malformed JSON in history.jsonl for ${e.sessionId}`);continue}if(!o||typeof o!="object"||Array.isArray(o))continue;const a=o;if(a.method!=="session/update")continue;const d=a.params??void 0;if(!d||typeof d!="object"||Array.isArray(d))continue;const i=d.update??void 0;if(!i||typeof i!="object"||Array.isArray(i)||i.sessionUpdate!=="usage_update")continue;const I=M(a),f=J(i);if(f===void 0)continue;const T=f.currency??"",m=f.amount,C=h.get(e.sessionId)??0,S=Math.max(0,m-C);h.set(e.sessionId,m);const r=i.usage??void 0;let y,p,l,g;if(r&&typeof r=="object"&&!Array.isArray(r)){const w=r.inputTokens;typeof w=="number"&&(y=w);const A=r.outputTokens;typeof A=="number"&&(p=A);const R=r.cacheReadInputTokens;typeof R=="number"&&(l=R);const v=r.cacheCreationInputTokens;typeof v=="number"&&(g=v)}yield{sessionId:e.sessionId,ts:I,deltaCost:S,cumulativeCost:m,currency:T,...y!==void 0&&{inputTokens:y},...p!==void 0&&{outputTokens:p},...l!==void 0&&{cacheReadTokens:l},...g!==void 0&&{cacheWriteTokens:g}}}}finally{k.close(),u.destroy()}}}export{L as streamHistoryEvents};
@@ -0,0 +1 @@
1
+ import{readdirSync as C,readFileSync as I,realpathSync as R,statSync as h}from"node:fs";import{isAbsolute as j,resolve as a}from"node:path";import{homedir as x}from"node:os";import{logger as E}from"../util/log.js";const d=E("cost/session-store"),f=new Map;function $(n){if(f.has(n))return f.get(n);try{const s=R(n);return f.set(n,s),s}catch{f.set(n,void 0);return}}function N(){const n=process.env.HYDRA_ACP_HOME??a(x(),".hydra-acp");return a(n,"sessions")}function J(n){const s=a(n,"meta.json");let i;try{i=I(s,"utf8")}catch(g){const p=g;if(p.code==="ENOENT")return;d.debug(`skipping ${n}: read meta.json failed: ${p.message}`);return}let t;try{t=JSON.parse(i)}catch{d.debug(`skipping ${n}: meta.json is not valid JSON`);return}if(!t||typeof t!="object"||Array.isArray(t)){d.debug(`skipping ${n}: meta.json is not an object`);return}const e=t,c=typeof e.sessionId=="string"?e.sessionId:void 0;if(c===void 0){d.debug(`skipping ${n}: missing sessionId`);return}let r;const u=typeof e.cwd=="string"?e.cwd:void 0;u!==void 0&&(j(u)?r=$(u):r=u);const y=typeof e.agentId=="string"?e.agentId:"",m=typeof e.currentModel=="string"?e.currentModel:"",l=e.interactive===!0,o=e.currentUsage??void 0,A=typeof o?.costAmount=="number"?o.costAmount:0,w=typeof o?.costCurrency=="string"?o.costCurrency:"",b=typeof o?.used=="number"?o.used:0,S=typeof e.title=="string"?e.title:"",k=typeof e.createdAt=="string"?e.createdAt:"",v=typeof e.updatedAt=="string"?e.updatedAt:"";return{sessionId:c,cwd:r,agentId:y,model:m,interactive:l,costAmount:A,costCurrency:w,contextTokens:b,title:S,createdAt:k,updatedAt:v}}function U(){const n=N();let s;try{s=C(n)}catch(t){const e=t;return d.debug(`session store not found at ${n}: ${e.message}`),[]}const i=[];for(const t of s){const e=a(n,t);let c;try{c=h(e)}catch{continue}if(!c.isDirectory())continue;const r=J(e);r!==void 0&&i.push(r)}return i}export{U as scanSessions,N as sessionsDir};
package/dist/index.js CHANGED
@@ -1,5 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as a}from"node:fs";import{dirname as c,resolve as d}from"node:path";import{fileURLToPath as m}from"node:url";import{loadConfig as u}from"./config.js";import{BudgeterBridge as p}from"./bridge.js";import{stateFilePath as n}from"./paths.js";import{DEFAULT_RULE as f}from"./rule.js";import{deleteStateFile as g}from"./tracker.js";import{logger as h,setDebug as l}from"./util/log.js";const s=h("main");function y(){try{const r=c(m(import.meta.url));return JSON.parse(a(d(r,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}function v(){g(n()),process.stdout.write(`hydra-acp-budgeter accumulated cost reset
3
- `)}async function w(){const r=u();l(r.debug);const e=n(),t=new p({daemonWsUrl:r.hydraWsUrl,token:r.hydraToken,softLimit:r.softLimit,hardLimit:r.hardLimit,currency:r.currency,rule:f,statePath:e});t.start();const o=i=>{s.info(`${i} received \u2014 shutting down`),t.stop(),setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>o("SIGINT")),process.on("SIGTERM",()=>o("SIGTERM")),s.info(`hydra-acp-budgeter up; daemon=${r.hydraDaemonUrl} soft=${r.softLimit} hard=${r.hardLimit} ${r.currency} state=${e}`)}async function L(){const r=process.argv.slice(2);if(r.includes("--version")||r.includes("-v")){process.stdout.write(`hydra-acp-budgeter ${y()}
4
- `);return}if(r[0]==="reset"){v();return}await w()}L().catch(r=>{process.stderr.write(`hydra-acp-budgeter: ${r.message}
2
+ import{readFileSync as C}from"node:fs";import{dirname as F,resolve as R}from"node:path";import{fileURLToPath as U}from"node:url";import{loadConfig as $}from"./config.js";import{BudgeterBridge as L}from"./bridge.js";import{stateFilePath as v}from"./paths.js";import{DEFAULT_RULE as P}from"./rule.js";import{deleteStateFile as N}from"./tracker.js";import{logger as x,setDebug as G}from"./util/log.js";import{scanSessions as M}from"./cost/session-store.js";import{listSessionsFromDaemon as j,fetchUsageEventsFromDaemon as H}from"./cost/daemon-client.js";import{aggregate as A,applyFilters as _,parseSince as J}from"./cost/aggregate.js";import{renderText as W,renderJson as Y}from"./cost/format.js";const k=x("main");function S(){try{const e=F(U(import.meta.url));return JSON.parse(C(R(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}function B(){N(v()),process.stdout.write(`hydra-acp-budgeter accumulated cost reset
3
+ `)}const V=`Usage: hydra budgeter usage [OPTIONS]
4
+
5
+ Options:
6
+ --since <date|duration> Only include sessions updated after this date (e.g. 7d, 2024-01-01)
7
+ --bucket <hour|day|week|month> Group results into time buckets (implies --since 24h/30d/6m/2y)
8
+ --by <dir|session|model|agent> Group by dimension
9
+ --depth <N> Depth for --by dir grouping (default: 1)
10
+ --dir <path> Only include sessions under this directory prefix
11
+ --interactive Only include interactive sessions (default: include both)
12
+ --min <N> Drop sessions whose active-metric value is <= N (default: 0)
13
+ --histogram Show an ASCII histogram bar next to each row (implies --bucket week if no bucket given)
14
+ --metric <cost|tokens> Display metric (default: cost)
15
+ --json Output as JSON
16
+ --help Show this help message`;async function E(e){if(e.includes("--help")){process.stdout.write(V+`
17
+ `);return}let r,s,d,a,c,l=!1,f,u=!1,o,m=!1;for(let t=0;t<e.length;t++){const n=e[t];if(n!==void 0){if(n==="--since")t+=1,r=e[t];else if(n==="--bucket")t+=1,s=e[t];else if(n==="--by")t+=1,d=e[t];else if(n==="--depth")t+=1,a=e[t];else if(n==="--dir")t+=1,c=e[t];else if(n==="--interactive")l=!0;else if(n==="--min")t+=1,f=e[t];else if(n==="--histogram")u=!0;else if(n==="--metric")t+=1,o=e[t];else if(n==="--json")m=!0;else if(n.startsWith("--"))throw new Error(`Unknown option: ${n}
18
+ Run "hydra budgeter usage --help" for usage.`)}}if(o!==void 0&&o!=="cost"&&o!=="tokens")throw new Error(`Invalid --metric value. Must be 'cost' or 'tokens'.
19
+ Run "hydra budgeter usage --help" for usage.`);let p;if(r!==void 0)try{p=J(r)}catch(t){const n=t;throw new Error(`Invalid --since value: ${n.message}
20
+ Run "hydra budgeter usage --help" for usage.`)}e.length===0&&(s="hour",u=!0),u&&s===void 0&&(s="week");let i=p;if(i===void 0&&s!==void 0){const t=new Date;s==="hour"?(t.setHours(t.getHours()-24),i=t):s==="day"?(t.setDate(t.getDate()-30),i=t):s==="week"?(t.setMonth(t.getMonth()-6),i=t):s==="month"&&(t.setFullYear(t.getFullYear()-2),i=t)}const h=l?!0:void 0,g=o==="tokens",y=f!==void 0?parseFloat(f):void 0,T=await j()??M(),D=_(T,{since:i,dir:c,interactive:h,min:y,minMetric:g?"tokens":"cost"});let w;if(s!==void 0){const t=await H();t!==void 0&&(w=t.map(n=>({sessionId:n.sessionId,ts:n.ts,deltaCost:0,cumulativeCost:n.costCumulative,currency:n.costCurrency,inputTokens:n.contextTokens})))}const I=a!==void 0?parseInt(a,10):void 0,b=A(D,w,{by:d,depth:I,bucket:s,since:i,interactive:h,dir:c,tokens:g,min:y});if(m)process.stdout.write(Y(b)+`
21
+ `);else{const t=W(b,{histogram:u,tokens:o==="tokens"});process.stdout.write(t)}}async function K(){const e=$();G(e.debug);const r=v(),s=new L({daemonWsUrl:e.hydraWsUrl,token:e.hydraToken,softLimit:e.softLimit,hardLimit:e.hardLimit,currency:e.currency,rule:P,statePath:r});s.start();const d=a=>{k.info(`${a} received \u2014 shutting down`),s.stop(),setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>d("SIGINT")),process.on("SIGTERM",()=>d("SIGTERM")),k.info(`hydra-acp-budgeter up; daemon=${e.hydraDaemonUrl} soft=${e.softLimit} hard=${e.hardLimit} ${e.currency} state=${r}`)}function Z(){process.stdout.write(`hydra-acp-budgeter ${S()}
22
+
23
+ Usage:
24
+ hydra budgeter [usage] <flags> Report historical cost/usage across sessions
25
+ hydra budgeter reset Zero the accumulated-cost baseline
26
+
27
+ Flags:
28
+ -v, --version Print version and exit
29
+ -h, --help Show this help
30
+ `)}async function q(){const e=process.argv.slice(2);if(e.includes("--version")||e.includes("-v")){process.stdout.write(`hydra-acp-budgeter ${S()}
31
+ `);return}if((e[0]==="help"||e.includes("--help")||e.includes("-h"))&&e[0]!=="usage"&&e[0]!=="cost"){Z();return}if(e[0]==="reset"){B();return}if(e[0]==="usage"||e[0]==="cost"){await E(e.slice(1));return}if(e[0]==="run"||process.env.HYDRA_ACP_TOKEN){await K();return}await E(e)}q().catch(e=>{process.stderr.write(`hydra-acp-budgeter: ${e.message}
5
32
  `),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/budgeter",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",