@hasna/economy 0.2.15 → 0.2.17

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/dist/cli/index.js CHANGED
@@ -96,9 +96,9 @@ var init_pricing = __esm(() => {
96
96
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
97
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
98
98
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
99
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
100
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
101
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
99
+ "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
100
+ "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
101
+ "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
102
102
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
103
103
  "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
104
104
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
@@ -134,6 +134,7 @@ __export(exports_database, {
134
134
  upsertModelPricing: () => upsertModelPricing,
135
135
  upsertGoal: () => upsertGoal,
136
136
  upsertBudget: () => upsertBudget,
137
+ upsertBillingDaily: () => upsertBillingDaily,
137
138
  setIngestState: () => setIngestState,
138
139
  seedModelPricing: () => seedModelPricing,
139
140
  rollupSession: () => rollupSession,
@@ -144,6 +145,7 @@ __export(exports_database, {
144
145
  queryProjectBreakdown: () => queryProjectBreakdown,
145
146
  queryModelBreakdown: () => queryModelBreakdown,
146
147
  queryDailyBreakdown: () => queryDailyBreakdown,
148
+ queryBillingSummary: () => queryBillingSummary,
147
149
  openDatabase: () => openDatabase,
148
150
  listProjects: () => listProjects,
149
151
  listModelPricing: () => listModelPricing,
@@ -161,7 +163,8 @@ __export(exports_database, {
161
163
  deleteProject: () => deleteProject,
162
164
  deleteModelPricing: () => deleteModelPricing,
163
165
  deleteGoal: () => deleteGoal,
164
- deleteBudget: () => deleteBudget
166
+ deleteBudget: () => deleteBudget,
167
+ clearBillingRange: () => clearBillingRange
165
168
  });
166
169
  import { SqliteAdapter as Database } from "@hasna/cloud";
167
170
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
@@ -309,6 +312,18 @@ function initSchema(db) {
309
312
  machine_id TEXT,
310
313
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
311
314
  );
315
+
316
+ CREATE TABLE IF NOT EXISTS billing_daily (
317
+ date TEXT NOT NULL,
318
+ provider TEXT NOT NULL,
319
+ description TEXT DEFAULT '',
320
+ cost_usd REAL NOT NULL DEFAULT 0,
321
+ updated_at TEXT NOT NULL,
322
+ PRIMARY KEY (date, provider, description)
323
+ );
324
+
325
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
326
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
312
327
  `);
313
328
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
314
329
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -327,11 +342,11 @@ function periodWhere(period) {
327
342
  case "yesterday":
328
343
  return `DATE(timestamp) = DATE('now', '-1 day')`;
329
344
  case "week":
330
- return `timestamp >= DATE('now', '-7 days')`;
345
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
331
346
  case "month":
332
- return `timestamp >= DATE('now', '-30 days')`;
347
+ return `timestamp >= DATE('now', 'start of month')`;
333
348
  case "year":
334
- return `timestamp >= DATE('now', '-365 days')`;
349
+ return `timestamp >= DATE('now', 'start of year')`;
335
350
  case "all":
336
351
  return "1=1";
337
352
  }
@@ -343,11 +358,11 @@ function sessionPeriodWhere(period) {
343
358
  case "yesterday":
344
359
  return `DATE(started_at) = DATE('now', '-1 day')`;
345
360
  case "week":
346
- return `started_at >= DATE('now', '-7 days')`;
361
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
347
362
  case "month":
348
- return `started_at >= DATE('now', '-30 days')`;
363
+ return `started_at >= DATE('now', 'start of month')`;
349
364
  case "year":
350
- return `started_at >= DATE('now', '-365 days')`;
365
+ return `started_at >= DATE('now', 'start of year')`;
351
366
  case "all":
352
367
  return "1=1";
353
368
  }
@@ -457,23 +472,66 @@ function queryModelBreakdown(db) {
457
472
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
458
473
  `).all();
459
474
  }
475
+ function labelForPath(projectPath, projectName) {
476
+ if (projectName && projectName.trim() !== "")
477
+ return projectName;
478
+ if (!projectPath)
479
+ return "";
480
+ const segments = projectPath.split("/").filter(Boolean);
481
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
482
+ for (const seg of segments) {
483
+ if (projectPrefix.test(seg))
484
+ return seg;
485
+ }
486
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
487
+ for (let i = segments.length - 1;i >= 0; i--) {
488
+ if (!generic.has(segments[i].toLowerCase()))
489
+ return segments[i];
490
+ }
491
+ return segments[segments.length - 1] ?? projectPath;
492
+ }
460
493
  function queryProjectBreakdown(db) {
461
- return db.prepare(`
462
- SELECT
463
- s.project_path,
464
- COALESCE(p.name, s.project_name) as project_name,
465
- COUNT(DISTINCT s.id) as sessions,
466
- COUNT(r.id) as requests,
467
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
468
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
469
- MAX(s.started_at) as last_active
470
- FROM sessions s
471
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
472
- LEFT JOIN requests r ON r.session_id = s.id
473
- WHERE s.project_path != '' OR s.project_name != ''
474
- GROUP BY s.project_path
475
- ORDER BY cost_usd DESC
494
+ const sessions = db.prepare(`
495
+ SELECT id, project_path, project_name, total_cost_usd, started_at
496
+ FROM sessions
497
+ WHERE project_path != '' OR project_name != ''
476
498
  `).all();
499
+ const groups = new Map;
500
+ for (const s of sessions) {
501
+ const label = labelForPath(s.project_path, s.project_name);
502
+ if (!label)
503
+ continue;
504
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
505
+ g.sessionIds.push(s.id);
506
+ g.totalCost += s.total_cost_usd || 0;
507
+ if (!g.lastActive || s.started_at > g.lastActive)
508
+ g.lastActive = s.started_at;
509
+ if (!g.samplePath)
510
+ g.samplePath = s.project_path;
511
+ groups.set(label, g);
512
+ }
513
+ const result = [];
514
+ for (const [label, g] of groups.entries()) {
515
+ const placeholders = g.sessionIds.map(() => "?").join(",");
516
+ const reqStats = placeholders.length ? db.prepare(`
517
+ SELECT
518
+ COUNT(*) as requests,
519
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
520
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
521
+ FROM requests WHERE session_id IN (${placeholders})
522
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
523
+ result.push({
524
+ project_path: g.samplePath,
525
+ project_name: label,
526
+ sessions: g.sessionIds.length,
527
+ requests: reqStats.requests,
528
+ total_tokens: reqStats.total_tokens,
529
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
530
+ last_active: g.lastActive
531
+ });
532
+ }
533
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
534
+ return result;
477
535
  }
478
536
  function queryDailyBreakdown(db, days = 30) {
479
537
  return db.prepare(`
@@ -591,6 +649,26 @@ function setIngestState(db, source, key, value) {
591
649
  function queryRequestsSince(db, since) {
592
650
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
593
651
  }
652
+ function upsertBillingDaily(db, row) {
653
+ db.prepare(`
654
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
655
+ VALUES (?, ?, ?, ?, ?)
656
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
657
+ }
658
+ function clearBillingRange(db, provider, fromDate, toDate) {
659
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
660
+ }
661
+ function queryBillingSummary(db, period) {
662
+ const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
663
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
664
+ const by_provider = {};
665
+ let total = 0;
666
+ for (const r of rows) {
667
+ by_provider[r.provider] = r.cost;
668
+ total += r.cost;
669
+ }
670
+ return { total_usd: total, by_provider };
671
+ }
594
672
  function listMachines(db) {
595
673
  return db.prepare(`
596
674
  SELECT
@@ -1971,6 +2049,119 @@ init_claude();
1971
2049
  init_codex();
1972
2050
  init_gemini();
1973
2051
 
2052
+ // src/ingest/billing.ts
2053
+ init_database();
2054
+ function getAnthropicAdminKey() {
2055
+ return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
2056
+ }
2057
+ function getOpenAIAdminKey() {
2058
+ return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
2059
+ }
2060
+ function toISODate(d) {
2061
+ return d.toISOString().substring(0, 10);
2062
+ }
2063
+ async function syncAnthropicBilling(db, opts = {}) {
2064
+ const key = getAnthropicAdminKey();
2065
+ if (!key)
2066
+ throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
2067
+ const now = new Date;
2068
+ const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
2069
+ const days = opts.days ?? 31;
2070
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2071
+ const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2072
+ const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2073
+ let totalUsd = 0;
2074
+ const buckets = [];
2075
+ let nextPage;
2076
+ do {
2077
+ const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
2078
+ url.searchParams.set("starting_at", startIso);
2079
+ url.searchParams.set("ending_at", endIso);
2080
+ url.searchParams.set("bucket_width", "1d");
2081
+ url.searchParams.set("limit", "31");
2082
+ url.searchParams.append("group_by[]", "description");
2083
+ if (nextPage)
2084
+ url.searchParams.set("page", nextPage);
2085
+ const res = await fetch(url.toString(), {
2086
+ headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
2087
+ });
2088
+ const data = await res.json();
2089
+ if (data.error)
2090
+ throw new Error(`Anthropic API: ${data.error.message}`);
2091
+ if (data.data)
2092
+ buckets.push(...data.data);
2093
+ nextPage = data.has_more ? data.next_page : undefined;
2094
+ } while (nextPage);
2095
+ const fromDateStr = toISODate(start);
2096
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
2097
+ clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
2098
+ const updatedAt = new Date().toISOString();
2099
+ for (const bucket of buckets) {
2100
+ const date = bucket.starting_at.substring(0, 10);
2101
+ for (const r of bucket.results) {
2102
+ const usd = Number(r.amount) / 100;
2103
+ if (usd === 0)
2104
+ continue;
2105
+ const desc = (r.description ?? "unknown").substring(0, 200);
2106
+ upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
2107
+ totalUsd += usd;
2108
+ }
2109
+ }
2110
+ return { days: buckets.length, totalUsd };
2111
+ }
2112
+ async function syncOpenAIBilling(db, opts = {}) {
2113
+ const key = getOpenAIAdminKey();
2114
+ if (!key)
2115
+ throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
2116
+ const now = new Date;
2117
+ const end = opts.toDate ? new Date(opts.toDate) : now;
2118
+ const days = opts.days ?? 31;
2119
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2120
+ const startSec = Math.floor(start.getTime() / 1000);
2121
+ const endSec = Math.floor(end.getTime() / 1000);
2122
+ let totalUsd = 0;
2123
+ const buckets = [];
2124
+ let nextPage;
2125
+ do {
2126
+ const url = new URL("https://api.openai.com/v1/organization/costs");
2127
+ url.searchParams.set("start_time", String(startSec));
2128
+ url.searchParams.set("end_time", String(endSec));
2129
+ url.searchParams.set("bucket_width", "1d");
2130
+ url.searchParams.set("limit", "31");
2131
+ url.searchParams.append("group_by[]", "line_item");
2132
+ if (nextPage)
2133
+ url.searchParams.set("page", nextPage);
2134
+ const res = await fetch(url.toString(), {
2135
+ headers: { Authorization: `Bearer ${key}` }
2136
+ });
2137
+ const data = await res.json();
2138
+ if (data.error)
2139
+ throw new Error(`OpenAI API: ${data.error.message}`);
2140
+ if (data.data)
2141
+ buckets.push(...data.data);
2142
+ nextPage = data.has_more ? data.next_page : undefined;
2143
+ } while (nextPage);
2144
+ const fromDateStr = toISODate(start);
2145
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
2146
+ clearBillingRange(db, "openai", fromDateStr, toDateStr);
2147
+ const updatedAt = new Date().toISOString();
2148
+ for (const bucket of buckets) {
2149
+ const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
2150
+ for (const r of bucket.results) {
2151
+ const usd = Number(r.amount?.value ?? 0);
2152
+ if (usd === 0)
2153
+ continue;
2154
+ const desc = (r.line_item ?? "unknown").substring(0, 200);
2155
+ upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
2156
+ totalUsd += usd;
2157
+ }
2158
+ }
2159
+ return { days: buckets.length, totalUsd };
2160
+ }
2161
+
2162
+ // src/cli/index.ts
2163
+ init_database();
2164
+
1974
2165
  // src/lib/package-metadata.ts
1975
2166
  import { readFileSync as readFileSync5 } from "fs";
1976
2167
  var cachedMetadata = null;
@@ -3035,5 +3226,46 @@ cloudCmd.command("status").description("Check cloud connection status").action(a
3035
3226
  }
3036
3227
  console.log();
3037
3228
  });
3229
+ var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
3230
+ billingCmd.command("sync").description("Sync actual billing from Anthropic and OpenAI admin APIs").option("--days <n>", "Days of history to fetch", "31").option("--anthropic", "Only sync Anthropic").option("--openai", "Only sync OpenAI").action(async (opts) => {
3231
+ const db = openDatabase();
3232
+ const days = Number(opts.days ?? 31);
3233
+ const doBoth = !opts.anthropic && !opts.openai;
3234
+ if (opts.anthropic || doBoth) {
3235
+ process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
3236
+ try {
3237
+ const r = await syncAnthropicBilling(db, { days });
3238
+ console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3239
+ } catch (e) {
3240
+ console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3241
+ }
3242
+ }
3243
+ if (opts.openai || doBoth) {
3244
+ process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
3245
+ try {
3246
+ const r = await syncOpenAIBilling(db, { days });
3247
+ console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3248
+ } catch (e) {
3249
+ console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3250
+ }
3251
+ }
3252
+ });
3253
+ billingCmd.command("show").description("Show actual billing totals vs our estimated costs").option("--period <p>", "Period: today|yesterday|week|month|year|all", "month").action((opts) => {
3254
+ const db = openDatabase();
3255
+ const period = opts.period ?? "month";
3256
+ const actual = queryBillingSummary(db, period);
3257
+ const estimated = querySummary(db, period);
3258
+ console.log();
3259
+ console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
3260
+ `));
3261
+ printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
3262
+ console.log();
3263
+ console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
3264
+ console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
3265
+ const diff = estimated.total_usd - actual.total_usd;
3266
+ const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
3267
+ console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
3268
+ console.log();
3269
+ });
3038
3270
  registerBrainsCommand(program);
3039
3271
  program.parse();
@@ -48,6 +48,19 @@ export declare function getGoalStatuses(db: Database): GoalStatus[];
48
48
  export declare function getIngestState(db: Database, source: string, key: string): string | null;
49
49
  export declare function setIngestState(db: Database, source: string, key: string, value: string): void;
50
50
  export declare function queryRequestsSince(db: Database, since: string): EconomyRequest[];
51
+ export interface BillingDaily {
52
+ date: string;
53
+ provider: 'anthropic' | 'openai' | string;
54
+ description: string;
55
+ cost_usd: number;
56
+ updated_at: string;
57
+ }
58
+ export declare function upsertBillingDaily(db: Database, row: BillingDaily): void;
59
+ export declare function clearBillingRange(db: Database, provider: string, fromDate: string, toDate: string): void;
60
+ export declare function queryBillingSummary(db: Database, period: Period): {
61
+ total_usd: number;
62
+ by_provider: Record<string, number>;
63
+ };
51
64
  export interface MachineInfo {
52
65
  machine_id: string;
53
66
  sessions: number;
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAKxD,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,aAAa,EACd,MAAM,mBAAmB,CAAA;AAE1B,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAkBnC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAIlC;AAED,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAgBxE;AAmJD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAarE;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAYzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAYnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,aAAkB,GAAG,cAAc,EAAE,CAkBxF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvF;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,CA8BxF;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAUlE;AA0BD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CA+CtE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,SAAK,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKzE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAI5E;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAG3D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAU/D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,EAAE,CA2B9D;AAID,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI;IACtC,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;IACpB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CASzD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,EAAE,CAE9C;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,UAAU,EAAE,CA6B1D;AAID,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7F;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAEhF;AAID,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAA;IACzC,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAKxE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAExG;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAY5H;AAID,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,WAAW,EAAE,CAaxD;AAID,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,GAAG,IAAI,CAMxE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAElF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAE/D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAgB3K"}
package/dist/index.js CHANGED
@@ -94,9 +94,9 @@ var init_pricing = __esm(() => {
94
94
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
95
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
96
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
98
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
99
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
97
+ "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
98
+ "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
99
+ "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
100
100
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
101
101
  "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
102
102
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
@@ -270,6 +270,18 @@ function initSchema(db) {
270
270
  machine_id TEXT,
271
271
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
272
272
  );
273
+
274
+ CREATE TABLE IF NOT EXISTS billing_daily (
275
+ date TEXT NOT NULL,
276
+ provider TEXT NOT NULL,
277
+ description TEXT DEFAULT '',
278
+ cost_usd REAL NOT NULL DEFAULT 0,
279
+ updated_at TEXT NOT NULL,
280
+ PRIMARY KEY (date, provider, description)
281
+ );
282
+
283
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
284
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
273
285
  `);
274
286
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
275
287
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -288,11 +300,11 @@ function periodWhere(period) {
288
300
  case "yesterday":
289
301
  return `DATE(timestamp) = DATE('now', '-1 day')`;
290
302
  case "week":
291
- return `timestamp >= DATE('now', '-7 days')`;
303
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
292
304
  case "month":
293
- return `timestamp >= DATE('now', '-30 days')`;
305
+ return `timestamp >= DATE('now', 'start of month')`;
294
306
  case "year":
295
- return `timestamp >= DATE('now', '-365 days')`;
307
+ return `timestamp >= DATE('now', 'start of year')`;
296
308
  case "all":
297
309
  return "1=1";
298
310
  }
@@ -304,11 +316,11 @@ function sessionPeriodWhere(period) {
304
316
  case "yesterday":
305
317
  return `DATE(started_at) = DATE('now', '-1 day')`;
306
318
  case "week":
307
- return `started_at >= DATE('now', '-7 days')`;
319
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
308
320
  case "month":
309
- return `started_at >= DATE('now', '-30 days')`;
321
+ return `started_at >= DATE('now', 'start of month')`;
310
322
  case "year":
311
- return `started_at >= DATE('now', '-365 days')`;
323
+ return `started_at >= DATE('now', 'start of year')`;
312
324
  case "all":
313
325
  return "1=1";
314
326
  }
@@ -418,23 +430,66 @@ function queryModelBreakdown(db) {
418
430
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
419
431
  `).all();
420
432
  }
433
+ function labelForPath(projectPath, projectName) {
434
+ if (projectName && projectName.trim() !== "")
435
+ return projectName;
436
+ if (!projectPath)
437
+ return "";
438
+ const segments = projectPath.split("/").filter(Boolean);
439
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
440
+ for (const seg of segments) {
441
+ if (projectPrefix.test(seg))
442
+ return seg;
443
+ }
444
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
445
+ for (let i = segments.length - 1;i >= 0; i--) {
446
+ if (!generic.has(segments[i].toLowerCase()))
447
+ return segments[i];
448
+ }
449
+ return segments[segments.length - 1] ?? projectPath;
450
+ }
421
451
  function queryProjectBreakdown(db) {
422
- return db.prepare(`
423
- SELECT
424
- s.project_path,
425
- COALESCE(p.name, s.project_name) as project_name,
426
- COUNT(DISTINCT s.id) as sessions,
427
- COUNT(r.id) as requests,
428
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
429
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
430
- MAX(s.started_at) as last_active
431
- FROM sessions s
432
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
433
- LEFT JOIN requests r ON r.session_id = s.id
434
- WHERE s.project_path != '' OR s.project_name != ''
435
- GROUP BY s.project_path
436
- ORDER BY cost_usd DESC
452
+ const sessions = db.prepare(`
453
+ SELECT id, project_path, project_name, total_cost_usd, started_at
454
+ FROM sessions
455
+ WHERE project_path != '' OR project_name != ''
437
456
  `).all();
457
+ const groups = new Map;
458
+ for (const s of sessions) {
459
+ const label = labelForPath(s.project_path, s.project_name);
460
+ if (!label)
461
+ continue;
462
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
463
+ g.sessionIds.push(s.id);
464
+ g.totalCost += s.total_cost_usd || 0;
465
+ if (!g.lastActive || s.started_at > g.lastActive)
466
+ g.lastActive = s.started_at;
467
+ if (!g.samplePath)
468
+ g.samplePath = s.project_path;
469
+ groups.set(label, g);
470
+ }
471
+ const result = [];
472
+ for (const [label, g] of groups.entries()) {
473
+ const placeholders = g.sessionIds.map(() => "?").join(",");
474
+ const reqStats = placeholders.length ? db.prepare(`
475
+ SELECT
476
+ COUNT(*) as requests,
477
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
478
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
479
+ FROM requests WHERE session_id IN (${placeholders})
480
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
481
+ result.push({
482
+ project_path: g.samplePath,
483
+ project_name: label,
484
+ sessions: g.sessionIds.length,
485
+ requests: reqStats.requests,
486
+ total_tokens: reqStats.total_tokens,
487
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
488
+ last_active: g.lastActive
489
+ });
490
+ }
491
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
492
+ return result;
438
493
  }
439
494
  function queryDailyBreakdown(db, days = 30) {
440
495
  return db.prepare(`
@@ -552,6 +607,26 @@ function setIngestState(db, source, key, value) {
552
607
  function queryRequestsSince(db, since) {
553
608
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
554
609
  }
610
+ function upsertBillingDaily(db, row) {
611
+ db.prepare(`
612
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
613
+ VALUES (?, ?, ?, ?, ?)
614
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
615
+ }
616
+ function clearBillingRange(db, provider, fromDate, toDate) {
617
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
618
+ }
619
+ function queryBillingSummary(db, period) {
620
+ const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
621
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
622
+ const by_provider = {};
623
+ let total = 0;
624
+ for (const r of rows) {
625
+ by_provider[r.provider] = r.cost;
626
+ total += r.cost;
627
+ }
628
+ return { total_usd: total, by_provider };
629
+ }
555
630
  function listMachines(db) {
556
631
  return db.prepare(`
557
632
  SELECT
@@ -1037,6 +1112,7 @@ export {
1037
1112
  upsertModelPricing,
1038
1113
  upsertGoal,
1039
1114
  upsertBudget,
1115
+ upsertBillingDaily,
1040
1116
  setIngestState,
1041
1117
  setActiveModel,
1042
1118
  seedModelPricing,
@@ -1049,6 +1125,7 @@ export {
1049
1125
  queryProjectBreakdown,
1050
1126
  queryModelBreakdown,
1051
1127
  queryDailyBreakdown,
1128
+ queryBillingSummary,
1052
1129
  openDatabase,
1053
1130
  normalizeModelName,
1054
1131
  listProjects,
@@ -1078,6 +1155,7 @@ export {
1078
1155
  deleteBudget,
1079
1156
  computeCostFromDb,
1080
1157
  computeCost,
1158
+ clearBillingRange,
1081
1159
  clearActiveModel,
1082
1160
  DEFAULT_PRICING,
1083
1161
  DEFAULT_MODEL
@@ -0,0 +1,18 @@
1
+ import type { SqliteAdapter as Database } from '@hasna/cloud';
2
+ export declare function syncAnthropicBilling(db: Database, opts?: {
3
+ days?: number;
4
+ fromDate?: string;
5
+ toDate?: string;
6
+ }): Promise<{
7
+ days: number;
8
+ totalUsd: number;
9
+ }>;
10
+ export declare function syncOpenAIBilling(db: Database, opts?: {
11
+ days?: number;
12
+ fromDate?: string;
13
+ toDate?: string;
14
+ }): Promise<{
15
+ days: number;
16
+ totalUsd: number;
17
+ }>;
18
+ //# sourceMappingURL=billing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../src/ingest/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAgC7D,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAuD7C;AAeD,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,QAAQ,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAqD7C"}
package/dist/mcp/index.js CHANGED
@@ -95,9 +95,9 @@ var init_pricing = __esm(() => {
95
95
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
96
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
97
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
98
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
99
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
100
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
98
+ "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
99
+ "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
100
+ "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
101
101
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
102
102
  "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
103
103
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
@@ -271,6 +271,18 @@ function initSchema(db) {
271
271
  machine_id TEXT,
272
272
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
273
273
  );
274
+
275
+ CREATE TABLE IF NOT EXISTS billing_daily (
276
+ date TEXT NOT NULL,
277
+ provider TEXT NOT NULL,
278
+ description TEXT DEFAULT '',
279
+ cost_usd REAL NOT NULL DEFAULT 0,
280
+ updated_at TEXT NOT NULL,
281
+ PRIMARY KEY (date, provider, description)
282
+ );
283
+
284
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
285
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
274
286
  `);
275
287
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
276
288
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -289,11 +301,11 @@ function periodWhere(period) {
289
301
  case "yesterday":
290
302
  return `DATE(timestamp) = DATE('now', '-1 day')`;
291
303
  case "week":
292
- return `timestamp >= DATE('now', '-7 days')`;
304
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
293
305
  case "month":
294
- return `timestamp >= DATE('now', '-30 days')`;
306
+ return `timestamp >= DATE('now', 'start of month')`;
295
307
  case "year":
296
- return `timestamp >= DATE('now', '-365 days')`;
308
+ return `timestamp >= DATE('now', 'start of year')`;
297
309
  case "all":
298
310
  return "1=1";
299
311
  }
@@ -305,11 +317,11 @@ function sessionPeriodWhere(period) {
305
317
  case "yesterday":
306
318
  return `DATE(started_at) = DATE('now', '-1 day')`;
307
319
  case "week":
308
- return `started_at >= DATE('now', '-7 days')`;
320
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
309
321
  case "month":
310
- return `started_at >= DATE('now', '-30 days')`;
322
+ return `started_at >= DATE('now', 'start of month')`;
311
323
  case "year":
312
- return `started_at >= DATE('now', '-365 days')`;
324
+ return `started_at >= DATE('now', 'start of year')`;
313
325
  case "all":
314
326
  return "1=1";
315
327
  }
@@ -419,23 +431,66 @@ function queryModelBreakdown(db) {
419
431
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
420
432
  `).all();
421
433
  }
434
+ function labelForPath(projectPath, projectName) {
435
+ if (projectName && projectName.trim() !== "")
436
+ return projectName;
437
+ if (!projectPath)
438
+ return "";
439
+ const segments = projectPath.split("/").filter(Boolean);
440
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
441
+ for (const seg of segments) {
442
+ if (projectPrefix.test(seg))
443
+ return seg;
444
+ }
445
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
446
+ for (let i = segments.length - 1;i >= 0; i--) {
447
+ if (!generic.has(segments[i].toLowerCase()))
448
+ return segments[i];
449
+ }
450
+ return segments[segments.length - 1] ?? projectPath;
451
+ }
422
452
  function queryProjectBreakdown(db) {
423
- return db.prepare(`
424
- SELECT
425
- s.project_path,
426
- COALESCE(p.name, s.project_name) as project_name,
427
- COUNT(DISTINCT s.id) as sessions,
428
- COUNT(r.id) as requests,
429
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
430
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
431
- MAX(s.started_at) as last_active
432
- FROM sessions s
433
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
434
- LEFT JOIN requests r ON r.session_id = s.id
435
- WHERE s.project_path != '' OR s.project_name != ''
436
- GROUP BY s.project_path
437
- ORDER BY cost_usd DESC
453
+ const sessions = db.prepare(`
454
+ SELECT id, project_path, project_name, total_cost_usd, started_at
455
+ FROM sessions
456
+ WHERE project_path != '' OR project_name != ''
438
457
  `).all();
458
+ const groups = new Map;
459
+ for (const s of sessions) {
460
+ const label = labelForPath(s.project_path, s.project_name);
461
+ if (!label)
462
+ continue;
463
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
464
+ g.sessionIds.push(s.id);
465
+ g.totalCost += s.total_cost_usd || 0;
466
+ if (!g.lastActive || s.started_at > g.lastActive)
467
+ g.lastActive = s.started_at;
468
+ if (!g.samplePath)
469
+ g.samplePath = s.project_path;
470
+ groups.set(label, g);
471
+ }
472
+ const result = [];
473
+ for (const [label, g] of groups.entries()) {
474
+ const placeholders = g.sessionIds.map(() => "?").join(",");
475
+ const reqStats = placeholders.length ? db.prepare(`
476
+ SELECT
477
+ COUNT(*) as requests,
478
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
479
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
480
+ FROM requests WHERE session_id IN (${placeholders})
481
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
482
+ result.push({
483
+ project_path: g.samplePath,
484
+ project_name: label,
485
+ sessions: g.sessionIds.length,
486
+ requests: reqStats.requests,
487
+ total_tokens: reqStats.total_tokens,
488
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
489
+ last_active: g.lastActive
490
+ });
491
+ }
492
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
493
+ return result;
439
494
  }
440
495
  function queryDailyBreakdown(db, days = 30) {
441
496
  return db.prepare(`
@@ -96,9 +96,9 @@ var init_pricing = __esm(() => {
96
96
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
97
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
98
98
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
99
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
100
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
101
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
99
+ "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
100
+ "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
101
+ "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
102
102
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
103
103
  "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
104
104
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
@@ -272,6 +272,18 @@ function initSchema(db) {
272
272
  machine_id TEXT,
273
273
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
274
274
  );
275
+
276
+ CREATE TABLE IF NOT EXISTS billing_daily (
277
+ date TEXT NOT NULL,
278
+ provider TEXT NOT NULL,
279
+ description TEXT DEFAULT '',
280
+ cost_usd REAL NOT NULL DEFAULT 0,
281
+ updated_at TEXT NOT NULL,
282
+ PRIMARY KEY (date, provider, description)
283
+ );
284
+
285
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
286
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
275
287
  `);
276
288
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
277
289
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -290,11 +302,11 @@ function periodWhere(period) {
290
302
  case "yesterday":
291
303
  return `DATE(timestamp) = DATE('now', '-1 day')`;
292
304
  case "week":
293
- return `timestamp >= DATE('now', '-7 days')`;
305
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
294
306
  case "month":
295
- return `timestamp >= DATE('now', '-30 days')`;
307
+ return `timestamp >= DATE('now', 'start of month')`;
296
308
  case "year":
297
- return `timestamp >= DATE('now', '-365 days')`;
309
+ return `timestamp >= DATE('now', 'start of year')`;
298
310
  case "all":
299
311
  return "1=1";
300
312
  }
@@ -306,11 +318,11 @@ function sessionPeriodWhere(period) {
306
318
  case "yesterday":
307
319
  return `DATE(started_at) = DATE('now', '-1 day')`;
308
320
  case "week":
309
- return `started_at >= DATE('now', '-7 days')`;
321
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
310
322
  case "month":
311
- return `started_at >= DATE('now', '-30 days')`;
323
+ return `started_at >= DATE('now', 'start of month')`;
312
324
  case "year":
313
- return `started_at >= DATE('now', '-365 days')`;
325
+ return `started_at >= DATE('now', 'start of year')`;
314
326
  case "all":
315
327
  return "1=1";
316
328
  }
@@ -420,23 +432,66 @@ function queryModelBreakdown(db) {
420
432
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
421
433
  `).all();
422
434
  }
435
+ function labelForPath(projectPath, projectName) {
436
+ if (projectName && projectName.trim() !== "")
437
+ return projectName;
438
+ if (!projectPath)
439
+ return "";
440
+ const segments = projectPath.split("/").filter(Boolean);
441
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
442
+ for (const seg of segments) {
443
+ if (projectPrefix.test(seg))
444
+ return seg;
445
+ }
446
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
447
+ for (let i = segments.length - 1;i >= 0; i--) {
448
+ if (!generic.has(segments[i].toLowerCase()))
449
+ return segments[i];
450
+ }
451
+ return segments[segments.length - 1] ?? projectPath;
452
+ }
423
453
  function queryProjectBreakdown(db) {
424
- return db.prepare(`
425
- SELECT
426
- s.project_path,
427
- COALESCE(p.name, s.project_name) as project_name,
428
- COUNT(DISTINCT s.id) as sessions,
429
- COUNT(r.id) as requests,
430
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
431
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
432
- MAX(s.started_at) as last_active
433
- FROM sessions s
434
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
435
- LEFT JOIN requests r ON r.session_id = s.id
436
- WHERE s.project_path != '' OR s.project_name != ''
437
- GROUP BY s.project_path
438
- ORDER BY cost_usd DESC
454
+ const sessions = db.prepare(`
455
+ SELECT id, project_path, project_name, total_cost_usd, started_at
456
+ FROM sessions
457
+ WHERE project_path != '' OR project_name != ''
439
458
  `).all();
459
+ const groups = new Map;
460
+ for (const s of sessions) {
461
+ const label = labelForPath(s.project_path, s.project_name);
462
+ if (!label)
463
+ continue;
464
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
465
+ g.sessionIds.push(s.id);
466
+ g.totalCost += s.total_cost_usd || 0;
467
+ if (!g.lastActive || s.started_at > g.lastActive)
468
+ g.lastActive = s.started_at;
469
+ if (!g.samplePath)
470
+ g.samplePath = s.project_path;
471
+ groups.set(label, g);
472
+ }
473
+ const result = [];
474
+ for (const [label, g] of groups.entries()) {
475
+ const placeholders = g.sessionIds.map(() => "?").join(",");
476
+ const reqStats = placeholders.length ? db.prepare(`
477
+ SELECT
478
+ COUNT(*) as requests,
479
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
480
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
481
+ FROM requests WHERE session_id IN (${placeholders})
482
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
483
+ result.push({
484
+ project_path: g.samplePath,
485
+ project_name: label,
486
+ sessions: g.sessionIds.length,
487
+ requests: reqStats.requests,
488
+ total_tokens: reqStats.total_tokens,
489
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
490
+ last_active: g.lastActive
491
+ });
492
+ }
493
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
494
+ return result;
440
495
  }
441
496
  function queryDailyBreakdown(db, days = 30) {
442
497
  return db.prepare(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",