@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.
- package/CHANGELOG.md +6 -0
- package/dist/chunks/{billing-bkpxy4kv.js → billing-22az60hy.js} +2 -2
- package/dist/chunks/{config-j9mv3ehk.js → config-hsp7ndhj.js} +2 -2
- package/dist/chunks/{index-dj3tctqh.js → index-1vz1arz5.js} +14 -13
- package/dist/chunks/{index-hesxq90g.js → index-9wa1f2kv.js} +1 -1
- package/dist/chunks/{index-kzwqw3ve.js → index-bn04v378.js} +2 -2
- package/dist/chunks/{index-zpvea08j.js → index-c7h2rxx1.js} +38 -12
- package/dist/chunks/{index-k540hfww.js → index-dk5rq1rq.js} +1 -1
- package/dist/chunks/{index-ez9thsr7.js → index-f5jv6q5q.js} +38 -12
- package/dist/chunks/{index-9d2aky06.js → index-mhrkwvtk.js} +3 -3
- package/dist/chunks/{index-npsjjv96.js → index-s9mvven6.js} +2 -2
- package/dist/chunks/{index-2hwzmfgv.js → index-vajzfzgv.js} +38 -12
- package/dist/chunks/{open-projects-6z4pq6mx.js → open-projects-8qm46m3d.js} +1 -1
- package/dist/chunks/{pricing-vf2pycmh.js → pricing-462f8xkm.js} +1 -1
- package/dist/chunks/{pricing-f80p81gr.js → pricing-k9wsjwbz.js} +1 -1
- package/dist/chunks/{pricing-y21mcjsa.js → pricing-vxejj1qa.js} +1 -1
- package/dist/chunks/{serve-p2yarhm8.js → serve-8yqe7nhs.js} +18 -10
- package/dist/chunks/{sqlite-store-st99garx.js → sqlite-store-13nhr1z0.js} +2 -2
- package/dist/chunks/{sqlite-store-501f32ts.js → sqlite-store-gamn65xp.js} +2 -2
- package/dist/chunks/{sqlite-store-fq4natnf.js → sqlite-store-ht6frztj.js} +2 -2
- package/dist/chunks/{tui-x64hjfrh.js → tui-fe94bvp9.js} +4 -4
- package/dist/chunks/{watch-bxstjpbx.js → watch-ct8dvrpr.js} +5 -5
- package/dist/chunks/{webhooks-k4fh2t9c.js → webhooks-nw4daw9y.js} +20 -9
- package/dist/cli/index.js +27 -27
- package/dist/db/database.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +15 -14
- package/dist/lib/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/periods.d.ts +20 -1
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +3 -3
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/mcp/index.js +18 -17
- package/dist/otel/index.js +1 -1
- package/dist/server/index.js +78 -33
- package/dist/server/service-operations.d.ts.map +1 -1
- package/dist/server/service-worker.js +78 -33
- 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-
|
|
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.
|
|
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-
|
|
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-
|
|
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 >=
|
|
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
|
|
4772
|
-
const
|
|
4773
|
-
const
|
|
4774
|
-
const
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
5197
|
+
const { openDatabase } = await import("../chunks/sqlite-store-gamn65xp.js");
|
|
5197
5198
|
_db = openDatabase();
|
|
5198
5199
|
ensurePricingSeeded(_db);
|
|
5199
5200
|
}
|
package/dist/otel/index.js
CHANGED
|
@@ -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.
|
|
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",
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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(
|
|
1124
|
+
const start = new Date(now);
|
|
1125
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
1125
1126
|
const end = new Date(start);
|
|
1126
|
-
|
|
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",
|
|
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",
|
|
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 ? [
|
|
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 >=
|
|
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
|
|
2225
|
-
const
|
|
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("${
|
|
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 >=
|
|
4090
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
4054
4091
|
GROUP BY DATE(timestamp)
|
|
4055
4092
|
ORDER BY date ASC
|
|
4056
|
-
`).all(
|
|
4093
|
+
`).all(bounds.start, bounds.end_exclusive);
|
|
4057
4094
|
return detectCostSpikes(rows);
|
|
4058
4095
|
}
|
|
4059
|
-
function getTodaySpike(db) {
|
|
4060
|
-
const today =
|
|
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.
|
|
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 >=
|
|
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
|
|
11687
|
-
const
|
|
11688
|
-
const
|
|
11689
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
|
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(
|
|
1123
|
+
const start = new Date(now);
|
|
1124
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
1124
1125
|
const end = new Date(start);
|
|
1125
|
-
|
|
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",
|
|
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",
|
|
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 ? [
|
|
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 >=
|
|
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
|
|
3036
|
-
const
|
|
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 >=
|
|
3783
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
3748
3784
|
GROUP BY DATE(timestamp)
|
|
3749
3785
|
ORDER BY date ASC
|
|
3750
|
-
`).all(
|
|
3786
|
+
`).all(bounds.start, bounds.end_exclusive);
|
|
3751
3787
|
return detectCostSpikes(rows);
|
|
3752
3788
|
}
|
|
3753
|
-
function getTodaySpike(db) {
|
|
3754
|
-
const today =
|
|
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("${
|
|
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 >=
|
|
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
|
|
7163
|
-
const
|
|
7164
|
-
const
|
|
7165
|
-
const
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|