@hasna/economy 0.5.2 → 0.5.3

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/chunks/{billing-bkpxy4kv.js → billing-22az60hy.js} +2 -2
  3. package/dist/chunks/{config-j9mv3ehk.js → config-hsp7ndhj.js} +2 -2
  4. package/dist/chunks/{index-dj3tctqh.js → index-1vz1arz5.js} +14 -13
  5. package/dist/chunks/{index-hesxq90g.js → index-9wa1f2kv.js} +1 -1
  6. package/dist/chunks/{index-kzwqw3ve.js → index-bn04v378.js} +2 -2
  7. package/dist/chunks/{index-zpvea08j.js → index-c7h2rxx1.js} +38 -12
  8. package/dist/chunks/{index-k540hfww.js → index-dk5rq1rq.js} +1 -1
  9. package/dist/chunks/{index-ez9thsr7.js → index-f5jv6q5q.js} +38 -12
  10. package/dist/chunks/{index-9d2aky06.js → index-mhrkwvtk.js} +3 -3
  11. package/dist/chunks/{index-npsjjv96.js → index-s9mvven6.js} +2 -2
  12. package/dist/chunks/{index-2hwzmfgv.js → index-vajzfzgv.js} +38 -12
  13. package/dist/chunks/{open-projects-6z4pq6mx.js → open-projects-8qm46m3d.js} +1 -1
  14. package/dist/chunks/{pricing-vf2pycmh.js → pricing-462f8xkm.js} +1 -1
  15. package/dist/chunks/{pricing-f80p81gr.js → pricing-k9wsjwbz.js} +1 -1
  16. package/dist/chunks/{pricing-y21mcjsa.js → pricing-vxejj1qa.js} +1 -1
  17. package/dist/chunks/{serve-p2yarhm8.js → serve-8yqe7nhs.js} +18 -10
  18. package/dist/chunks/{sqlite-store-st99garx.js → sqlite-store-13nhr1z0.js} +2 -2
  19. package/dist/chunks/{sqlite-store-501f32ts.js → sqlite-store-gamn65xp.js} +2 -2
  20. package/dist/chunks/{sqlite-store-fq4natnf.js → sqlite-store-ht6frztj.js} +2 -2
  21. package/dist/chunks/{tui-x64hjfrh.js → tui-fe94bvp9.js} +4 -4
  22. package/dist/chunks/{watch-bxstjpbx.js → watch-ct8dvrpr.js} +5 -5
  23. package/dist/chunks/{webhooks-k4fh2t9c.js → webhooks-nw4daw9y.js} +20 -9
  24. package/dist/cli/index.js +27 -27
  25. package/dist/db/database.d.ts +2 -2
  26. package/dist/db/database.d.ts.map +1 -1
  27. package/dist/db/sync-pg.d.ts.map +1 -1
  28. package/dist/index.js +15 -14
  29. package/dist/lib/analytics.d.ts +1 -1
  30. package/dist/lib/analytics.d.ts.map +1 -1
  31. package/dist/lib/periods.d.ts +20 -1
  32. package/dist/lib/periods.d.ts.map +1 -1
  33. package/dist/lib/spikes.d.ts +3 -3
  34. package/dist/lib/spikes.d.ts.map +1 -1
  35. package/dist/mcp/index.js +18 -17
  36. package/dist/otel/index.js +1 -1
  37. package/dist/server/index.js +78 -33
  38. package/dist/server/service-operations.d.ts.map +1 -1
  39. package/dist/server/service-worker.js +78 -33
  40. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  queryZeroCostTokenizedModels,
57
57
  requireCurrentBillingSummary,
58
58
  requireCurrentCostSummary,
59
+ resolveReportingInterval,
59
60
  rollupSession,
60
61
  scanReceiptId,
61
62
  sessionOverlapWhere,
@@ -74,7 +75,7 @@ import {
74
75
  upsertUsageSnapshot,
75
76
  usageSnapshotFilterForPeriod,
76
77
  validateIngest
77
- } from "../chunks/index-2hwzmfgv.js";
78
+ } from "../chunks/index-vajzfzgv.js";
78
79
  import {
79
80
  __require
80
81
  } from "../chunks/index-p35cyhc4.js";
@@ -84,7 +85,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
84
85
  // package.json
85
86
  var package_default = {
86
87
  name: "@hasna/economy",
87
- version: "0.5.2",
88
+ version: "0.5.3",
88
89
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
89
90
  repository: {
90
91
  type: "git",
@@ -3579,7 +3580,7 @@ function backfillMachineId(db) {
3579
3580
  return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
3580
3581
  }
3581
3582
  async function recalculateZeroCostRequests(db) {
3582
- const { assessCostFromDb: assessCostFromDb2 } = await import("../chunks/pricing-vf2pycmh.js");
3583
+ const { assessCostFromDb: assessCostFromDb2 } = await import("../chunks/pricing-vxejj1qa.js");
3583
3584
  const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens
3584
3585
  FROM requests
3585
3586
  WHERE cost_usd = 0
@@ -3788,7 +3789,7 @@ async function postIngest(cloud, body) {
3788
3789
  }
3789
3790
  async function syncAllToCloud(cloud, opts = {}) {
3790
3791
  await runIngestJournalOperation(cloud, { action: "reconcile" });
3791
- const { openDatabase } = await import("../chunks/sqlite-store-st99garx.js");
3792
+ const { openDatabase } = await import("../chunks/sqlite-store-gamn65xp.js");
3792
3793
  const scratch = openDatabase(":memory:", true);
3793
3794
  ensurePricingSeeded(scratch);
3794
3795
  const cachePath = opts.cachePath ?? getIngestCachePath();
@@ -4714,7 +4715,8 @@ function buildBrief(db, opts = {}) {
4714
4715
  }
4715
4716
 
4716
4717
  // src/lib/analytics.ts
4717
- function queryProjectDetail(db, nameOrPath) {
4718
+ function queryProjectDetail(db, nameOrPath, now2 = new Date) {
4719
+ const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now2);
4718
4720
  const like = `%${nameOrPath}%`;
4719
4721
  const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
4720
4722
  FROM economy_effective_sessions
@@ -4728,8 +4730,8 @@ function queryProjectDetail(db, nameOrPath) {
4728
4730
  const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
4729
4731
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
4730
4732
  WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
4731
- AND r.timestamp >= DATE('now', '-14 days')
4732
- GROUP BY date ORDER BY date ASC`).all(like, like);
4733
+ AND r.timestamp >= ? AND r.timestamp < ?
4734
+ GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
4733
4735
  const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
4734
4736
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
4735
4737
  WHERE s.project_name LIKE ? OR s.project_path LIKE ?
@@ -4768,18 +4770,17 @@ function queryRangeStats(db, from, to) {
4768
4770
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
4769
4771
  }
4770
4772
  function queryForecast(db, now2 = new Date) {
4771
- const daysInMonth = new Date(now2.getFullYear(), now2.getMonth() + 1, 0).getDate();
4772
- const dayOfMonth = now2.getDate();
4773
- const monthStart = `${now2.getFullYear()}-${String(now2.getMonth() + 1).padStart(2, "0")}-01`;
4774
- const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
4773
+ const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now2);
4774
+ const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now2);
4775
+ const daysInMonth = new Date(Date.UTC(now2.getUTCFullYear(), now2.getUTCMonth() + 1, 0)).getUTCDate();
4776
+ const dayOfMonth = now2.getUTCDate();
4777
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
4775
4778
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
4776
4779
  const projected = dailyAvg * daysInMonth;
4777
- const sevenDaysAgo = new Date(now2);
4778
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
4779
- const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
4780
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
4780
4781
  const last7DailyAvg = last7.cost / 7;
4781
4782
  const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
4782
- FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
4783
+ FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
4783
4784
  return {
4784
4785
  day_of_month: dayOfMonth,
4785
4786
  days_in_month: daysInMonth,
@@ -4830,7 +4831,7 @@ class LocalStore2 {
4830
4831
  _db;
4831
4832
  async db() {
4832
4833
  if (!this._db) {
4833
- const { openDatabase } = await import("../chunks/sqlite-store-st99garx.js");
4834
+ const { openDatabase } = await import("../chunks/sqlite-store-gamn65xp.js");
4834
4835
  this._db = openDatabase();
4835
4836
  ensurePricingSeeded(this._db);
4836
4837
  }
@@ -5193,7 +5194,7 @@ function buildServer() {
5193
5194
  let _db;
5194
5195
  const localDb = async () => {
5195
5196
  if (!_db) {
5196
- const { openDatabase } = await import("../chunks/sqlite-store-st99garx.js");
5197
+ const { openDatabase } = await import("../chunks/sqlite-store-gamn65xp.js");
5197
5198
  _db = openDatabase();
5198
5199
  ensurePricingSeeded(_db);
5199
5200
  }
@@ -3458,7 +3458,7 @@ async function pushIngestRows(cloud, scratch) {
3458
3458
  // package.json
3459
3459
  var package_default = {
3460
3460
  name: "@hasna/economy",
3461
- version: "0.5.2",
3461
+ version: "0.5.3",
3462
3462
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
3463
3463
  repository: {
3464
3464
  type: "git",
@@ -1118,12 +1118,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
1118
1118
  function ymd(date2) {
1119
1119
  return date2.toISOString().substring(0, 10);
1120
1120
  }
1121
- function calendarPeriodBounds(period, now = new Date) {
1121
+ function resolveReportingInterval(interval, now = new Date) {
1122
1122
  if (!Number.isFinite(now.getTime()))
1123
1123
  throw new RangeError("Invalid reporting clock");
1124
- const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
1124
+ const start = new Date(now);
1125
+ start.setUTCHours(0, 0, 0, 0);
1125
1126
  const end = new Date(start);
1126
- switch (period) {
1127
+ if (interval.kind === "rolling") {
1128
+ if (!Number.isSafeInteger(interval.count) || interval.count < 1)
1129
+ throw new RangeError("Invalid reporting window");
1130
+ if (interval.unit === "hour") {
1131
+ start.setTime(now.getTime() - interval.count * 3600000);
1132
+ if (!Number.isFinite(start.getTime()))
1133
+ throw new RangeError("Invalid reporting window");
1134
+ return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
1135
+ }
1136
+ if (interval.unit !== "day")
1137
+ throw new RangeError("Invalid reporting unit");
1138
+ start.setUTCDate(start.getUTCDate() - interval.count + 1);
1139
+ end.setUTCDate(end.getUTCDate() + 1);
1140
+ if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
1141
+ throw new RangeError("Invalid reporting window");
1142
+ return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
1143
+ }
1144
+ if (interval.kind !== "calendar")
1145
+ throw new RangeError("Invalid reporting interval");
1146
+ switch (interval.period) {
1127
1147
  case "today":
1128
1148
  end.setUTCDate(end.getUTCDate() + 1);
1129
1149
  break;
@@ -1146,11 +1166,15 @@ function calendarPeriodBounds(period, now = new Date) {
1146
1166
  end.setUTCFullYear(end.getUTCFullYear() + 1);
1147
1167
  break;
1148
1168
  case "all":
1149
- return { time_zone: "UTC", start_date: null, end_date_exclusive: null };
1169
+ return { time_zone: "UTC", start: null, end_exclusive: null };
1150
1170
  default:
1151
1171
  throw new RangeError("Invalid reporting period");
1152
1172
  }
1153
- return { time_zone: "UTC", start_date: ymd(start), end_date_exclusive: ymd(end) };
1173
+ return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
1174
+ }
1175
+ function calendarPeriodBounds(period, now = new Date) {
1176
+ const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
1177
+ return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
1154
1178
  }
1155
1179
  function calendarPeriodWhere(period, column, now = new Date) {
1156
1180
  if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
@@ -2209,20 +2233,22 @@ function queryAccountBreakdownSince(db, since) {
2209
2233
  ORDER BY api_equivalent_usd DESC
2210
2234
  `).all(since, since);
2211
2235
  }
2212
- function queryDailyBreakdown(db, days = 30, machine) {
2236
+ function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
2237
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
2213
2238
  const machineClause = machine ? " AND machine_id = ?" : "";
2214
- const params = machine ? [`-${days}`, machine] : [`-${days}`];
2239
+ const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
2215
2240
  return db.prepare(`
2216
2241
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
2217
2242
  FROM economy_effective_requests
2218
- WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
2243
+ WHERE timestamp >= ? AND timestamp < ?${machineClause}
2219
2244
  GROUP BY DATE(timestamp), agent
2220
2245
  ORDER BY date ASC
2221
2246
  `).all(...params);
2222
2247
  }
2223
- function queryHourlyBreakdown(db, machine, hours) {
2224
- const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
2225
- const params = hours == null ? [] : [`-${hours} hours`];
2248
+ function queryHourlyBreakdown(db, machine, hours, now = new Date) {
2249
+ const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
2250
+ const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
2251
+ const params = [bounds.start, bounds.end_exclusive];
2226
2252
  if (machine) {
2227
2253
  clauses.push("machine_id = ?");
2228
2254
  params.push(machine);
@@ -3143,15 +3169,16 @@ function translateSqliteDates(sql) {
3143
3169
  s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
3144
3170
  s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
3145
3171
  s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
3172
+ s = s.replace(/STRFTIME\('%Y-%m-%dT%H:%M:%fZ',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, col) => `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`);
3146
3173
  const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
3147
3174
  s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
3148
3175
  const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
3149
- return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
3176
+ return `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', '${pgFmt}')`;
3150
3177
  });
3151
3178
  s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
3152
3179
  s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
3153
3180
  s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
3154
- s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
3181
+ s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\s*\)/gi, (_m, col) => `substr(${col.split(".").map((part) => `"${part}"`).join(".")}, 1, 10)`);
3155
3182
  return s;
3156
3183
  }
3157
3184
  function resolveWorkerPath() {
@@ -4029,12 +4056,21 @@ var init_config = __esm(() => {
4029
4056
  function detectCostSpikes(dailyTotals, opts) {
4030
4057
  const windowDays = opts?.windowDays ?? 7;
4031
4058
  const multiplier = opts?.multiplier ?? 2;
4059
+ if (!Number.isSafeInteger(windowDays) || windowDays < 1)
4060
+ throw new RangeError("Invalid spike window");
4061
+ if (!Number.isFinite(multiplier) || multiplier <= 0)
4062
+ throw new RangeError("Invalid spike multiplier");
4032
4063
  const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
4033
4064
  const spikes = [];
4034
4065
  for (let i = windowDays;i < sorted.length; i++) {
4035
4066
  const window = sorted.slice(i - windowDays, i);
4036
- const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
4037
4067
  const current = sorted[i];
4068
+ const previous = new Date(current.date + "T00:00:00.000Z");
4069
+ previous.setUTCDate(previous.getUTCDate() - 1);
4070
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: windowDays }, previous);
4071
+ if (window[0].date !== bounds.start || window[window.length - 1].date !== previous.toISOString().slice(0, 10) || new Set(window.map((day) => day.date)).size !== windowDays)
4072
+ continue;
4073
+ const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
4038
4074
  if (avg > 0 && current.cost_usd > avg * multiplier) {
4039
4075
  spikes.push({
4040
4076
  date: current.date,
@@ -4046,21 +4082,23 @@ function detectCostSpikes(dailyTotals, opts) {
4046
4082
  }
4047
4083
  return spikes;
4048
4084
  }
4049
- function queryRecentSpikes(db, days = 14) {
4085
+ function queryRecentSpikes(db, days = 14, now = new Date) {
4086
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
4050
4087
  const rows = db.prepare(`
4051
4088
  SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
4052
4089
  FROM economy_effective_requests
4053
- WHERE timestamp >= DATE('now', ? || ' days')
4090
+ WHERE timestamp >= ? AND timestamp < ?
4054
4091
  GROUP BY DATE(timestamp)
4055
4092
  ORDER BY date ASC
4056
- `).all(-days);
4093
+ `).all(bounds.start, bounds.end_exclusive);
4057
4094
  return detectCostSpikes(rows);
4058
4095
  }
4059
- function getTodaySpike(db) {
4060
- const today = new Date().toISOString().substring(0, 10);
4061
- const spikes = queryRecentSpikes(db, 14);
4096
+ function getTodaySpike(db, now = new Date) {
4097
+ const today = resolveReportingInterval({ kind: "calendar", period: "today" }, now).start;
4098
+ const spikes = queryRecentSpikes(db, 14, now);
4062
4099
  return spikes.find((s) => s.date === today) ?? null;
4063
4100
  }
4101
+ var init_spikes = () => {};
4064
4102
 
4065
4103
  // src/lib/webhooks.ts
4066
4104
  var exports_webhooks = {};
@@ -4127,6 +4165,7 @@ async function fireWebhook(url, payload) {
4127
4165
  var init_webhooks = __esm(() => {
4128
4166
  init_config();
4129
4167
  init_database();
4168
+ init_spikes();
4130
4169
  });
4131
4170
 
4132
4171
  // src/db/pg-migrate.ts
@@ -5490,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
5490
5529
  // package.json
5491
5530
  var package_default = {
5492
5531
  name: "@hasna/economy",
5493
- version: "0.5.2",
5532
+ version: "0.5.3",
5494
5533
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5495
5534
  repository: {
5496
5535
  type: "git",
@@ -11629,7 +11668,8 @@ function buildBrief(db, opts = {}) {
11629
11668
  }
11630
11669
 
11631
11670
  // src/lib/analytics.ts
11632
- function queryProjectDetail(db, nameOrPath) {
11671
+ function queryProjectDetail(db, nameOrPath, now = new Date) {
11672
+ const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
11633
11673
  const like = `%${nameOrPath}%`;
11634
11674
  const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
11635
11675
  FROM economy_effective_sessions
@@ -11643,8 +11683,8 @@ function queryProjectDetail(db, nameOrPath) {
11643
11683
  const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
11644
11684
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
11645
11685
  WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
11646
- AND r.timestamp >= DATE('now', '-14 days')
11647
- GROUP BY date ORDER BY date ASC`).all(like, like);
11686
+ AND r.timestamp >= ? AND r.timestamp < ?
11687
+ GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
11648
11688
  const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
11649
11689
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
11650
11690
  WHERE s.project_name LIKE ? OR s.project_path LIKE ?
@@ -11683,18 +11723,17 @@ function queryRangeStats(db, from, to) {
11683
11723
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
11684
11724
  }
11685
11725
  function queryForecast(db, now = new Date) {
11686
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
11687
- const dayOfMonth = now.getDate();
11688
- const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
11689
- const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
11726
+ const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
11727
+ const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now);
11728
+ const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
11729
+ const dayOfMonth = now.getUTCDate();
11730
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
11690
11731
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
11691
11732
  const projected = dailyAvg * daysInMonth;
11692
- const sevenDaysAgo = new Date(now);
11693
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
11694
- const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
11733
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
11695
11734
  const last7DailyAvg = last7.cost / 7;
11696
11735
  const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
11697
- FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
11736
+ FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
11698
11737
  return {
11699
11738
  day_of_month: dayOfMonth,
11700
11739
  days_in_month: daysInMonth,
@@ -11871,7 +11910,13 @@ function createDataHandler(db, actorId) {
11871
11910
  if (path === "/api/daily" && method === "GET") {
11872
11911
  const days = Number(url.searchParams.get("days") ?? 30);
11873
11912
  const machine = url.searchParams.get("machine") ?? undefined;
11874
- return ok(queryDailyBreakdown(db, days, machine));
11913
+ const now = new Date;
11914
+ try {
11915
+ resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
11916
+ } catch {
11917
+ return err("days must be a positive integer within the supported UTC date range");
11918
+ }
11919
+ return ok(queryDailyBreakdown(db, days, machine, now));
11875
11920
  }
11876
11921
  if (path === "/api/hourly" && method === "GET") {
11877
11922
  const machine = url.searchParams.get("machine") ?? undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"service-operations.d.ts","sourceRoot":"","sources":["../../src/server/service-operations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAuCxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AA6jB5E,gHAAgH;AAChH,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAe/G"}
1
+ {"version":3,"file":"service-operations.d.ts","sourceRoot":"","sources":["../../src/server/service-operations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAuCxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAgkB5E,gHAAgH;AAChH,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAe/G"}
@@ -1117,12 +1117,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
1117
1117
  function ymd(date2) {
1118
1118
  return date2.toISOString().substring(0, 10);
1119
1119
  }
1120
- function calendarPeriodBounds(period, now = new Date) {
1120
+ function resolveReportingInterval(interval, now = new Date) {
1121
1121
  if (!Number.isFinite(now.getTime()))
1122
1122
  throw new RangeError("Invalid reporting clock");
1123
- const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
1123
+ const start = new Date(now);
1124
+ start.setUTCHours(0, 0, 0, 0);
1124
1125
  const end = new Date(start);
1125
- switch (period) {
1126
+ if (interval.kind === "rolling") {
1127
+ if (!Number.isSafeInteger(interval.count) || interval.count < 1)
1128
+ throw new RangeError("Invalid reporting window");
1129
+ if (interval.unit === "hour") {
1130
+ start.setTime(now.getTime() - interval.count * 3600000);
1131
+ if (!Number.isFinite(start.getTime()))
1132
+ throw new RangeError("Invalid reporting window");
1133
+ return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
1134
+ }
1135
+ if (interval.unit !== "day")
1136
+ throw new RangeError("Invalid reporting unit");
1137
+ start.setUTCDate(start.getUTCDate() - interval.count + 1);
1138
+ end.setUTCDate(end.getUTCDate() + 1);
1139
+ if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
1140
+ throw new RangeError("Invalid reporting window");
1141
+ return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
1142
+ }
1143
+ if (interval.kind !== "calendar")
1144
+ throw new RangeError("Invalid reporting interval");
1145
+ switch (interval.period) {
1126
1146
  case "today":
1127
1147
  end.setUTCDate(end.getUTCDate() + 1);
1128
1148
  break;
@@ -1145,11 +1165,15 @@ function calendarPeriodBounds(period, now = new Date) {
1145
1165
  end.setUTCFullYear(end.getUTCFullYear() + 1);
1146
1166
  break;
1147
1167
  case "all":
1148
- return { time_zone: "UTC", start_date: null, end_date_exclusive: null };
1168
+ return { time_zone: "UTC", start: null, end_exclusive: null };
1149
1169
  default:
1150
1170
  throw new RangeError("Invalid reporting period");
1151
1171
  }
1152
- return { time_zone: "UTC", start_date: ymd(start), end_date_exclusive: ymd(end) };
1172
+ return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
1173
+ }
1174
+ function calendarPeriodBounds(period, now = new Date) {
1175
+ const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
1176
+ return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
1153
1177
  }
1154
1178
  function calendarPeriodWhere(period, column, now = new Date) {
1155
1179
  if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
@@ -3020,20 +3044,22 @@ function queryAccountBreakdownSince(db, since) {
3020
3044
  ORDER BY api_equivalent_usd DESC
3021
3045
  `).all(since, since);
3022
3046
  }
3023
- function queryDailyBreakdown(db, days = 30, machine) {
3047
+ function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
3048
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
3024
3049
  const machineClause = machine ? " AND machine_id = ?" : "";
3025
- const params = machine ? [`-${days}`, machine] : [`-${days}`];
3050
+ const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
3026
3051
  return db.prepare(`
3027
3052
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
3028
3053
  FROM economy_effective_requests
3029
- WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
3054
+ WHERE timestamp >= ? AND timestamp < ?${machineClause}
3030
3055
  GROUP BY DATE(timestamp), agent
3031
3056
  ORDER BY date ASC
3032
3057
  `).all(...params);
3033
3058
  }
3034
- function queryHourlyBreakdown(db, machine, hours) {
3035
- const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
3036
- const params = hours == null ? [] : [`-${hours} hours`];
3059
+ function queryHourlyBreakdown(db, machine, hours, now = new Date) {
3060
+ const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
3061
+ const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
3062
+ const params = [bounds.start, bounds.end_exclusive];
3037
3063
  if (machine) {
3038
3064
  clauses.push("machine_id = ?");
3039
3065
  params.push(machine);
@@ -3723,12 +3749,21 @@ var init_config = __esm(() => {
3723
3749
  function detectCostSpikes(dailyTotals, opts) {
3724
3750
  const windowDays = opts?.windowDays ?? 7;
3725
3751
  const multiplier = opts?.multiplier ?? 2;
3752
+ if (!Number.isSafeInteger(windowDays) || windowDays < 1)
3753
+ throw new RangeError("Invalid spike window");
3754
+ if (!Number.isFinite(multiplier) || multiplier <= 0)
3755
+ throw new RangeError("Invalid spike multiplier");
3726
3756
  const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
3727
3757
  const spikes = [];
3728
3758
  for (let i = windowDays;i < sorted.length; i++) {
3729
3759
  const window = sorted.slice(i - windowDays, i);
3730
- const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
3731
3760
  const current = sorted[i];
3761
+ const previous = new Date(current.date + "T00:00:00.000Z");
3762
+ previous.setUTCDate(previous.getUTCDate() - 1);
3763
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: windowDays }, previous);
3764
+ if (window[0].date !== bounds.start || window[window.length - 1].date !== previous.toISOString().slice(0, 10) || new Set(window.map((day) => day.date)).size !== windowDays)
3765
+ continue;
3766
+ const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
3732
3767
  if (avg > 0 && current.cost_usd > avg * multiplier) {
3733
3768
  spikes.push({
3734
3769
  date: current.date,
@@ -3740,21 +3775,23 @@ function detectCostSpikes(dailyTotals, opts) {
3740
3775
  }
3741
3776
  return spikes;
3742
3777
  }
3743
- function queryRecentSpikes(db, days = 14) {
3778
+ function queryRecentSpikes(db, days = 14, now = new Date) {
3779
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
3744
3780
  const rows = db.prepare(`
3745
3781
  SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
3746
3782
  FROM economy_effective_requests
3747
- WHERE timestamp >= DATE('now', ? || ' days')
3783
+ WHERE timestamp >= ? AND timestamp < ?
3748
3784
  GROUP BY DATE(timestamp)
3749
3785
  ORDER BY date ASC
3750
- `).all(-days);
3786
+ `).all(bounds.start, bounds.end_exclusive);
3751
3787
  return detectCostSpikes(rows);
3752
3788
  }
3753
- function getTodaySpike(db) {
3754
- const today = new Date().toISOString().substring(0, 10);
3755
- const spikes = queryRecentSpikes(db, 14);
3789
+ function getTodaySpike(db, now = new Date) {
3790
+ const today = resolveReportingInterval({ kind: "calendar", period: "today" }, now).start;
3791
+ const spikes = queryRecentSpikes(db, 14, now);
3756
3792
  return spikes.find((s) => s.date === today) ?? null;
3757
3793
  }
3794
+ var init_spikes = () => {};
3758
3795
 
3759
3796
  // src/lib/webhooks.ts
3760
3797
  var exports_webhooks = {};
@@ -3821,6 +3858,7 @@ async function fireWebhook(url, payload) {
3821
3858
  var init_webhooks = __esm(() => {
3822
3859
  init_config();
3823
3860
  init_database();
3861
+ init_spikes();
3824
3862
  });
3825
3863
 
3826
3864
  // src/db/sync-pg.ts
@@ -3882,15 +3920,16 @@ function translateSqliteDates(sql) {
3882
3920
  s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
3883
3921
  s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
3884
3922
  s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
3923
+ s = s.replace(/STRFTIME\('%Y-%m-%dT%H:%M:%fZ',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, col) => `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`);
3885
3924
  const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
3886
3925
  s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
3887
3926
  const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
3888
- return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
3927
+ return `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', '${pgFmt}')`;
3889
3928
  });
3890
3929
  s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
3891
3930
  s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
3892
3931
  s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
3893
- s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
3932
+ s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\s*\)/gi, (_m, col) => `substr(${col.split(".").map((part) => `"${part}"`).join(".")}, 1, 10)`);
3894
3933
  return s;
3895
3934
  }
3896
3935
  function resolveWorkerPath() {
@@ -7105,7 +7144,8 @@ function buildBrief(db, opts = {}) {
7105
7144
  }
7106
7145
 
7107
7146
  // src/lib/analytics.ts
7108
- function queryProjectDetail(db, nameOrPath) {
7147
+ function queryProjectDetail(db, nameOrPath, now = new Date) {
7148
+ const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
7109
7149
  const like = `%${nameOrPath}%`;
7110
7150
  const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
7111
7151
  FROM economy_effective_sessions
@@ -7119,8 +7159,8 @@ function queryProjectDetail(db, nameOrPath) {
7119
7159
  const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
7120
7160
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
7121
7161
  WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
7122
- AND r.timestamp >= DATE('now', '-14 days')
7123
- GROUP BY date ORDER BY date ASC`).all(like, like);
7162
+ AND r.timestamp >= ? AND r.timestamp < ?
7163
+ GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
7124
7164
  const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
7125
7165
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
7126
7166
  WHERE s.project_name LIKE ? OR s.project_path LIKE ?
@@ -7159,18 +7199,17 @@ function queryRangeStats(db, from, to) {
7159
7199
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
7160
7200
  }
7161
7201
  function queryForecast(db, now = new Date) {
7162
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
7163
- const dayOfMonth = now.getDate();
7164
- const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
7165
- const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
7202
+ const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
7203
+ const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now);
7204
+ const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
7205
+ const dayOfMonth = now.getUTCDate();
7206
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
7166
7207
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
7167
7208
  const projected = dailyAvg * daysInMonth;
7168
- const sevenDaysAgo = new Date(now);
7169
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
7170
- const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
7209
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
7171
7210
  const last7DailyAvg = last7.cost / 7;
7172
7211
  const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
7173
- FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
7212
+ FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
7174
7213
  return {
7175
7214
  day_of_month: dayOfMonth,
7176
7215
  days_in_month: daysInMonth,
@@ -7266,7 +7305,7 @@ import { randomUUID } from "crypto";
7266
7305
  // package.json
7267
7306
  var package_default = {
7268
7307
  name: "@hasna/economy",
7269
- version: "0.5.2",
7308
+ version: "0.5.3",
7270
7309
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
7271
7310
  repository: {
7272
7311
  type: "git",
@@ -7471,7 +7510,13 @@ function createDataHandler(db, actorId) {
7471
7510
  if (path === "/api/daily" && method === "GET") {
7472
7511
  const days = Number(url.searchParams.get("days") ?? 30);
7473
7512
  const machine = url.searchParams.get("machine") ?? undefined;
7474
- return ok(queryDailyBreakdown(db, days, machine));
7513
+ const now = new Date;
7514
+ try {
7515
+ resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
7516
+ } catch {
7517
+ return err("days must be a positive integer within the supported UTC date range");
7518
+ }
7519
+ return ok(queryDailyBreakdown(db, days, machine, now));
7475
7520
  }
7476
7521
  if (path === "/api/hourly" && method === "GET") {
7477
7522
  const machine = url.searchParams.get("machine") ?? undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5
5
  "repository": {
6
6
  "type": "git",