@hasna/economy 0.2.14 → 0.2.16

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
@@ -79,6 +79,7 @@ var DEFAULT_PRICING;
79
79
  var init_pricing = __esm(() => {
80
80
  init_database();
81
81
  DEFAULT_PRICING = {
82
+ "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
83
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
84
  "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
84
85
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
@@ -89,24 +90,82 @@ var init_pricing = __esm(() => {
89
90
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
90
91
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
91
92
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
92
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
+ "gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
93
94
  "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
97
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
98
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, 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 },
96
102
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
103
+ "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
97
104
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
98
105
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
106
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
107
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
99
108
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
100
109
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
101
110
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
102
111
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
103
112
  o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
104
113
  "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
105
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
114
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
115
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
116
+ "qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
117
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
118
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
119
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
120
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
121
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
122
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
123
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
124
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
106
125
  };
107
126
  });
108
127
 
109
128
  // src/db/database.ts
129
+ var exports_database = {};
130
+ __export(exports_database, {
131
+ upsertSession: () => upsertSession,
132
+ upsertRequest: () => upsertRequest,
133
+ upsertProject: () => upsertProject,
134
+ upsertModelPricing: () => upsertModelPricing,
135
+ upsertGoal: () => upsertGoal,
136
+ upsertBudget: () => upsertBudget,
137
+ upsertBillingDaily: () => upsertBillingDaily,
138
+ setIngestState: () => setIngestState,
139
+ seedModelPricing: () => seedModelPricing,
140
+ rollupSession: () => rollupSession,
141
+ queryTopSessions: () => queryTopSessions,
142
+ querySummary: () => querySummary,
143
+ querySessions: () => querySessions,
144
+ queryRequestsSince: () => queryRequestsSince,
145
+ queryProjectBreakdown: () => queryProjectBreakdown,
146
+ queryModelBreakdown: () => queryModelBreakdown,
147
+ queryDailyBreakdown: () => queryDailyBreakdown,
148
+ queryBillingSummary: () => queryBillingSummary,
149
+ openDatabase: () => openDatabase,
150
+ listProjects: () => listProjects,
151
+ listModelPricing: () => listModelPricing,
152
+ listMachines: () => listMachines,
153
+ listGoals: () => listGoals,
154
+ listBudgets: () => listBudgets,
155
+ getProject: () => getProject,
156
+ getModelPricing: () => getModelPricing,
157
+ getMachineId: () => getMachineId,
158
+ getIngestState: () => getIngestState,
159
+ getGoalStatuses: () => getGoalStatuses,
160
+ getDbPath: () => getDbPath,
161
+ getDataDir: () => getDataDir,
162
+ getBudgetStatuses: () => getBudgetStatuses,
163
+ deleteProject: () => deleteProject,
164
+ deleteModelPricing: () => deleteModelPricing,
165
+ deleteGoal: () => deleteGoal,
166
+ deleteBudget: () => deleteBudget,
167
+ clearBillingRange: () => clearBillingRange
168
+ });
110
169
  import { SqliteAdapter as Database } from "@hasna/cloud";
111
170
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
112
171
  import { hostname } from "os";
@@ -253,6 +312,18 @@ function initSchema(db) {
253
312
  machine_id TEXT,
254
313
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
255
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);
256
327
  `);
257
328
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
258
329
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -271,11 +342,11 @@ function periodWhere(period) {
271
342
  case "yesterday":
272
343
  return `DATE(timestamp) = DATE('now', '-1 day')`;
273
344
  case "week":
274
- return `timestamp >= DATE('now', '-7 days')`;
345
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
275
346
  case "month":
276
- return `timestamp >= DATE('now', '-30 days')`;
347
+ return `timestamp >= DATE('now', 'start of month')`;
277
348
  case "year":
278
- return `timestamp >= DATE('now', '-365 days')`;
349
+ return `timestamp >= DATE('now', 'start of year')`;
279
350
  case "all":
280
351
  return "1=1";
281
352
  }
@@ -287,11 +358,11 @@ function sessionPeriodWhere(period) {
287
358
  case "yesterday":
288
359
  return `DATE(started_at) = DATE('now', '-1 day')`;
289
360
  case "week":
290
- return `started_at >= DATE('now', '-7 days')`;
361
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
291
362
  case "month":
292
- return `started_at >= DATE('now', '-30 days')`;
363
+ return `started_at >= DATE('now', 'start of month')`;
293
364
  case "year":
294
- return `started_at >= DATE('now', '-365 days')`;
365
+ return `started_at >= DATE('now', 'start of year')`;
295
366
  case "all":
296
367
  return "1=1";
297
368
  }
@@ -403,19 +474,39 @@ function queryModelBreakdown(db) {
403
474
  }
404
475
  function queryProjectBreakdown(db) {
405
476
  return db.prepare(`
477
+ WITH labeled AS (
478
+ SELECT
479
+ s.id,
480
+ s.project_path,
481
+ s.total_cost_usd,
482
+ s.started_at,
483
+ COALESCE(
484
+ NULLIF(s.project_name, ''),
485
+ CASE
486
+ WHEN s.project_path LIKE '%/%'
487
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
488
+ ELSE s.project_path
489
+ END
490
+ ) as label
491
+ FROM sessions s
492
+ WHERE s.project_path != '' OR s.project_name != ''
493
+ )
406
494
  SELECT
407
- s.project_path,
408
- COALESCE(p.name, s.project_name) as project_name,
409
- COUNT(DISTINCT s.id) as sessions,
410
- COUNT(r.id) as requests,
411
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
412
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
413
- MAX(s.started_at) as last_active
414
- FROM sessions s
415
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
416
- LEFT JOIN requests r ON r.session_id = s.id
417
- WHERE s.project_path != '' OR s.project_name != ''
418
- GROUP BY s.project_path
495
+ MIN(l.project_path) as project_path,
496
+ l.label as project_name,
497
+ COUNT(DISTINCT l.id) as sessions,
498
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
499
+ COALESCE(
500
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
501
+ SUM(l.total_cost_usd)
502
+ ) as cost_usd,
503
+ COALESCE(
504
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
505
+ 0
506
+ ) as total_tokens,
507
+ MAX(l.started_at) as last_active
508
+ FROM labeled l
509
+ GROUP BY l.label
419
510
  ORDER BY cost_usd DESC
420
511
  `).all();
421
512
  }
@@ -535,6 +626,26 @@ function setIngestState(db, source, key, value) {
535
626
  function queryRequestsSince(db, since) {
536
627
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
537
628
  }
629
+ function upsertBillingDaily(db, row) {
630
+ db.prepare(`
631
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
632
+ VALUES (?, ?, ?, ?, ?)
633
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
634
+ }
635
+ function clearBillingRange(db, provider, fromDate, toDate) {
636
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
637
+ }
638
+ function queryBillingSummary(db, period) {
639
+ 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";
640
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
641
+ const by_provider = {};
642
+ let total = 0;
643
+ for (const r of rows) {
644
+ by_provider[r.provider] = r.cost;
645
+ total += r.cost;
646
+ }
647
+ return { total_usd: total, by_provider };
648
+ }
538
649
  function listMachines(db) {
539
650
  return db.prepare(`
540
651
  SELECT
@@ -566,11 +677,11 @@ function deleteModelPricing(db, model) {
566
677
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
567
678
  }
568
679
  function seedModelPricing(db, defaults) {
569
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
570
- if (existing.count > 0)
571
- return;
680
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
572
681
  const now = new Date().toISOString();
573
682
  for (const [model, p] of Object.entries(defaults)) {
683
+ if (existing.has(model))
684
+ continue;
574
685
  upsertModelPricing(db, {
575
686
  model,
576
687
  input_per_1m: p.inputPer1M,
@@ -1915,6 +2026,119 @@ init_claude();
1915
2026
  init_codex();
1916
2027
  init_gemini();
1917
2028
 
2029
+ // src/ingest/billing.ts
2030
+ init_database();
2031
+ function getAnthropicAdminKey() {
2032
+ return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
2033
+ }
2034
+ function getOpenAIAdminKey() {
2035
+ return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
2036
+ }
2037
+ function toISODate(d) {
2038
+ return d.toISOString().substring(0, 10);
2039
+ }
2040
+ async function syncAnthropicBilling(db, opts = {}) {
2041
+ const key = getAnthropicAdminKey();
2042
+ if (!key)
2043
+ throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
2044
+ const now = new Date;
2045
+ const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
2046
+ const days = opts.days ?? 31;
2047
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2048
+ const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2049
+ const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2050
+ let totalUsd = 0;
2051
+ const buckets = [];
2052
+ let nextPage;
2053
+ do {
2054
+ const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
2055
+ url.searchParams.set("starting_at", startIso);
2056
+ url.searchParams.set("ending_at", endIso);
2057
+ url.searchParams.set("bucket_width", "1d");
2058
+ url.searchParams.set("limit", "31");
2059
+ url.searchParams.append("group_by[]", "description");
2060
+ if (nextPage)
2061
+ url.searchParams.set("page", nextPage);
2062
+ const res = await fetch(url.toString(), {
2063
+ headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
2064
+ });
2065
+ const data = await res.json();
2066
+ if (data.error)
2067
+ throw new Error(`Anthropic API: ${data.error.message}`);
2068
+ if (data.data)
2069
+ buckets.push(...data.data);
2070
+ nextPage = data.has_more ? data.next_page : undefined;
2071
+ } while (nextPage);
2072
+ const fromDateStr = toISODate(start);
2073
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
2074
+ clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
2075
+ const updatedAt = new Date().toISOString();
2076
+ for (const bucket of buckets) {
2077
+ const date = bucket.starting_at.substring(0, 10);
2078
+ for (const r of bucket.results) {
2079
+ const usd = Number(r.amount) / 100;
2080
+ if (usd === 0)
2081
+ continue;
2082
+ const desc = (r.description ?? "unknown").substring(0, 200);
2083
+ upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
2084
+ totalUsd += usd;
2085
+ }
2086
+ }
2087
+ return { days: buckets.length, totalUsd };
2088
+ }
2089
+ async function syncOpenAIBilling(db, opts = {}) {
2090
+ const key = getOpenAIAdminKey();
2091
+ if (!key)
2092
+ throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
2093
+ const now = new Date;
2094
+ const end = opts.toDate ? new Date(opts.toDate) : now;
2095
+ const days = opts.days ?? 31;
2096
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2097
+ const startSec = Math.floor(start.getTime() / 1000);
2098
+ const endSec = Math.floor(end.getTime() / 1000);
2099
+ let totalUsd = 0;
2100
+ const buckets = [];
2101
+ let nextPage;
2102
+ do {
2103
+ const url = new URL("https://api.openai.com/v1/organization/costs");
2104
+ url.searchParams.set("start_time", String(startSec));
2105
+ url.searchParams.set("end_time", String(endSec));
2106
+ url.searchParams.set("bucket_width", "1d");
2107
+ url.searchParams.set("limit", "31");
2108
+ url.searchParams.append("group_by[]", "line_item");
2109
+ if (nextPage)
2110
+ url.searchParams.set("page", nextPage);
2111
+ const res = await fetch(url.toString(), {
2112
+ headers: { Authorization: `Bearer ${key}` }
2113
+ });
2114
+ const data = await res.json();
2115
+ if (data.error)
2116
+ throw new Error(`OpenAI API: ${data.error.message}`);
2117
+ if (data.data)
2118
+ buckets.push(...data.data);
2119
+ nextPage = data.has_more ? data.next_page : undefined;
2120
+ } while (nextPage);
2121
+ const fromDateStr = toISODate(start);
2122
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
2123
+ clearBillingRange(db, "openai", fromDateStr, toDateStr);
2124
+ const updatedAt = new Date().toISOString();
2125
+ for (const bucket of buckets) {
2126
+ const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
2127
+ for (const r of bucket.results) {
2128
+ const usd = Number(r.amount?.value ?? 0);
2129
+ if (usd === 0)
2130
+ continue;
2131
+ const desc = (r.line_item ?? "unknown").substring(0, 200);
2132
+ upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
2133
+ totalUsd += usd;
2134
+ }
2135
+ }
2136
+ return { days: buckets.length, totalUsd };
2137
+ }
2138
+
2139
+ // src/cli/index.ts
2140
+ init_database();
2141
+
1918
2142
  // src/lib/package-metadata.ts
1919
2143
  import { readFileSync as readFileSync5 } from "fs";
1920
2144
  var cachedMetadata = null;
@@ -2054,7 +2278,7 @@ program.action(async () => {
2054
2278
  }
2055
2279
  console.log();
2056
2280
  });
2057
- program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").action(async (opts) => {
2281
+ program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
2058
2282
  const db = openDatabase();
2059
2283
  ensurePricingSeeded(db);
2060
2284
  if (opts.force) {
@@ -2093,6 +2317,29 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
2093
2317
  const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
2094
2318
  console.log(chalk4.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
2095
2319
  }
2320
+ if (opts.recalculate) {
2321
+ const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
2322
+ const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0)`).all();
2323
+ let fixed = 0;
2324
+ for (const r of zeroRows) {
2325
+ const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_create_tokens);
2326
+ if (cost > 0) {
2327
+ db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
2328
+ fixed++;
2329
+ }
2330
+ }
2331
+ if (fixed > 0) {
2332
+ const touchedSessions = new Set(zeroRows.map((r) => {
2333
+ const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
2334
+ return row?.session_id;
2335
+ }).filter(Boolean));
2336
+ const { rollupSession: rollupSession2 } = await Promise.resolve().then(() => (init_database(), exports_database));
2337
+ for (const sid of touchedSessions) {
2338
+ rollupSession2(db, sid);
2339
+ }
2340
+ }
2341
+ console.log(chalk4.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
2342
+ }
2096
2343
  try {
2097
2344
  const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
2098
2345
  await checkAndFireWebhooks2(db);
@@ -2956,5 +3203,46 @@ cloudCmd.command("status").description("Check cloud connection status").action(a
2956
3203
  }
2957
3204
  console.log();
2958
3205
  });
3206
+ var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
3207
+ 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) => {
3208
+ const db = openDatabase();
3209
+ const days = Number(opts.days ?? 31);
3210
+ const doBoth = !opts.anthropic && !opts.openai;
3211
+ if (opts.anthropic || doBoth) {
3212
+ process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
3213
+ try {
3214
+ const r = await syncAnthropicBilling(db, { days });
3215
+ console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3216
+ } catch (e) {
3217
+ console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3218
+ }
3219
+ }
3220
+ if (opts.openai || doBoth) {
3221
+ process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
3222
+ try {
3223
+ const r = await syncOpenAIBilling(db, { days });
3224
+ console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3225
+ } catch (e) {
3226
+ console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3227
+ }
3228
+ }
3229
+ });
3230
+ 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) => {
3231
+ const db = openDatabase();
3232
+ const period = opts.period ?? "month";
3233
+ const actual = queryBillingSummary(db, period);
3234
+ const estimated = querySummary(db, period);
3235
+ console.log();
3236
+ console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
3237
+ `));
3238
+ printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
3239
+ console.log();
3240
+ console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
3241
+ console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
3242
+ const diff = estimated.total_usd - actual.total_usd;
3243
+ const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
3244
+ console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
3245
+ console.log();
3246
+ });
2959
3247
  registerBrainsCommand(program);
2960
3248
  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":"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"}
package/dist/index.js CHANGED
@@ -77,6 +77,7 @@ var DEFAULT_PRICING;
77
77
  var init_pricing = __esm(() => {
78
78
  init_database();
79
79
  DEFAULT_PRICING = {
80
+ "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
80
81
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
81
82
  "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
83
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
@@ -87,20 +88,38 @@ var init_pricing = __esm(() => {
87
88
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
88
89
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
89
90
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
90
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
91
+ "gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
91
92
  "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
92
95
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
96
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, 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 },
94
100
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
101
+ "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
95
102
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
96
103
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
104
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
105
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
97
106
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
98
107
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
99
108
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
100
109
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
101
110
  o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
102
111
  "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
103
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
112
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
113
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
114
+ "qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
115
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
116
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
117
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
118
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
119
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
120
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
121
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
122
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
104
123
  };
105
124
  });
106
125
 
@@ -251,6 +270,18 @@ function initSchema(db) {
251
270
  machine_id TEXT,
252
271
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
253
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);
254
285
  `);
255
286
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
256
287
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -269,11 +300,11 @@ function periodWhere(period) {
269
300
  case "yesterday":
270
301
  return `DATE(timestamp) = DATE('now', '-1 day')`;
271
302
  case "week":
272
- return `timestamp >= DATE('now', '-7 days')`;
303
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
273
304
  case "month":
274
- return `timestamp >= DATE('now', '-30 days')`;
305
+ return `timestamp >= DATE('now', 'start of month')`;
275
306
  case "year":
276
- return `timestamp >= DATE('now', '-365 days')`;
307
+ return `timestamp >= DATE('now', 'start of year')`;
277
308
  case "all":
278
309
  return "1=1";
279
310
  }
@@ -285,11 +316,11 @@ function sessionPeriodWhere(period) {
285
316
  case "yesterday":
286
317
  return `DATE(started_at) = DATE('now', '-1 day')`;
287
318
  case "week":
288
- return `started_at >= DATE('now', '-7 days')`;
319
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
289
320
  case "month":
290
- return `started_at >= DATE('now', '-30 days')`;
321
+ return `started_at >= DATE('now', 'start of month')`;
291
322
  case "year":
292
- return `started_at >= DATE('now', '-365 days')`;
323
+ return `started_at >= DATE('now', 'start of year')`;
293
324
  case "all":
294
325
  return "1=1";
295
326
  }
@@ -401,19 +432,39 @@ function queryModelBreakdown(db) {
401
432
  }
402
433
  function queryProjectBreakdown(db) {
403
434
  return db.prepare(`
435
+ WITH labeled AS (
436
+ SELECT
437
+ s.id,
438
+ s.project_path,
439
+ s.total_cost_usd,
440
+ s.started_at,
441
+ COALESCE(
442
+ NULLIF(s.project_name, ''),
443
+ CASE
444
+ WHEN s.project_path LIKE '%/%'
445
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
446
+ ELSE s.project_path
447
+ END
448
+ ) as label
449
+ FROM sessions s
450
+ WHERE s.project_path != '' OR s.project_name != ''
451
+ )
404
452
  SELECT
405
- s.project_path,
406
- COALESCE(p.name, s.project_name) as project_name,
407
- COUNT(DISTINCT s.id) as sessions,
408
- COUNT(r.id) as requests,
409
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
410
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
411
- MAX(s.started_at) as last_active
412
- FROM sessions s
413
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
414
- LEFT JOIN requests r ON r.session_id = s.id
415
- WHERE s.project_path != '' OR s.project_name != ''
416
- GROUP BY s.project_path
453
+ MIN(l.project_path) as project_path,
454
+ l.label as project_name,
455
+ COUNT(DISTINCT l.id) as sessions,
456
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
457
+ COALESCE(
458
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
459
+ SUM(l.total_cost_usd)
460
+ ) as cost_usd,
461
+ COALESCE(
462
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
463
+ 0
464
+ ) as total_tokens,
465
+ MAX(l.started_at) as last_active
466
+ FROM labeled l
467
+ GROUP BY l.label
417
468
  ORDER BY cost_usd DESC
418
469
  `).all();
419
470
  }
@@ -533,6 +584,26 @@ function setIngestState(db, source, key, value) {
533
584
  function queryRequestsSince(db, since) {
534
585
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
535
586
  }
587
+ function upsertBillingDaily(db, row) {
588
+ db.prepare(`
589
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
590
+ VALUES (?, ?, ?, ?, ?)
591
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
592
+ }
593
+ function clearBillingRange(db, provider, fromDate, toDate) {
594
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
595
+ }
596
+ function queryBillingSummary(db, period) {
597
+ 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";
598
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
599
+ const by_provider = {};
600
+ let total = 0;
601
+ for (const r of rows) {
602
+ by_provider[r.provider] = r.cost;
603
+ total += r.cost;
604
+ }
605
+ return { total_usd: total, by_provider };
606
+ }
536
607
  function listMachines(db) {
537
608
  return db.prepare(`
538
609
  SELECT
@@ -564,11 +635,11 @@ function deleteModelPricing(db, model) {
564
635
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
565
636
  }
566
637
  function seedModelPricing(db, defaults) {
567
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
568
- if (existing.count > 0)
569
- return;
638
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
570
639
  const now = new Date().toISOString();
571
640
  for (const [model, p] of Object.entries(defaults)) {
641
+ if (existing.has(model))
642
+ continue;
572
643
  upsertModelPricing(db, {
573
644
  model,
574
645
  input_per_1m: p.inputPer1M,
@@ -1018,6 +1089,7 @@ export {
1018
1089
  upsertModelPricing,
1019
1090
  upsertGoal,
1020
1091
  upsertBudget,
1092
+ upsertBillingDaily,
1021
1093
  setIngestState,
1022
1094
  setActiveModel,
1023
1095
  seedModelPricing,
@@ -1030,6 +1102,7 @@ export {
1030
1102
  queryProjectBreakdown,
1031
1103
  queryModelBreakdown,
1032
1104
  queryDailyBreakdown,
1105
+ queryBillingSummary,
1033
1106
  openDatabase,
1034
1107
  normalizeModelName,
1035
1108
  listProjects,
@@ -1059,6 +1132,7 @@ export {
1059
1132
  deleteBudget,
1060
1133
  computeCostFromDb,
1061
1134
  computeCost,
1135
+ clearBillingRange,
1062
1136
  clearActiveModel,
1063
1137
  DEFAULT_PRICING,
1064
1138
  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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/lib/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA6BxD,CAAA;AAGD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKtD;AAGD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAGD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuBjF;AAGD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAO7D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,eAAe,SAAI,EACnB,gBAAgB,SAAI,GACnB,MAAM,CASR;AAED,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,eAAe,SAAI,EACnB,gBAAgB,SAAI,GACnB,MAAM,CASR"}
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/lib/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAqDxD,CAAA;AAGD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKtD;AAGD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAGD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuBjF;AAGD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAO7D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,eAAe,SAAI,EACnB,gBAAgB,SAAI,GACnB,MAAM,CASR;AAED,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,eAAe,SAAI,EACnB,gBAAgB,SAAI,GACnB,MAAM,CASR"}
package/dist/mcp/index.js CHANGED
@@ -78,6 +78,7 @@ var DEFAULT_PRICING;
78
78
  var init_pricing = __esm(() => {
79
79
  init_database();
80
80
  DEFAULT_PRICING = {
81
+ "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
81
82
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
83
  "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
84
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
@@ -88,20 +89,38 @@ var init_pricing = __esm(() => {
88
89
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
89
90
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
90
91
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
91
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
92
+ "gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
92
93
  "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
96
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
97
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, 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 },
95
101
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
102
+ "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
96
103
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
97
104
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
105
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
106
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
98
107
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
99
108
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
100
109
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
101
110
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
102
111
  o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
103
112
  "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
104
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
113
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
114
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
115
+ "qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
116
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
117
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
118
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
119
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
120
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
121
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
122
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
123
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
105
124
  };
106
125
  });
107
126
 
@@ -252,6 +271,18 @@ function initSchema(db) {
252
271
  machine_id TEXT,
253
272
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
254
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);
255
286
  `);
256
287
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
257
288
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -270,11 +301,11 @@ function periodWhere(period) {
270
301
  case "yesterday":
271
302
  return `DATE(timestamp) = DATE('now', '-1 day')`;
272
303
  case "week":
273
- return `timestamp >= DATE('now', '-7 days')`;
304
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
274
305
  case "month":
275
- return `timestamp >= DATE('now', '-30 days')`;
306
+ return `timestamp >= DATE('now', 'start of month')`;
276
307
  case "year":
277
- return `timestamp >= DATE('now', '-365 days')`;
308
+ return `timestamp >= DATE('now', 'start of year')`;
278
309
  case "all":
279
310
  return "1=1";
280
311
  }
@@ -286,11 +317,11 @@ function sessionPeriodWhere(period) {
286
317
  case "yesterday":
287
318
  return `DATE(started_at) = DATE('now', '-1 day')`;
288
319
  case "week":
289
- return `started_at >= DATE('now', '-7 days')`;
320
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
290
321
  case "month":
291
- return `started_at >= DATE('now', '-30 days')`;
322
+ return `started_at >= DATE('now', 'start of month')`;
292
323
  case "year":
293
- return `started_at >= DATE('now', '-365 days')`;
324
+ return `started_at >= DATE('now', 'start of year')`;
294
325
  case "all":
295
326
  return "1=1";
296
327
  }
@@ -402,19 +433,39 @@ function queryModelBreakdown(db) {
402
433
  }
403
434
  function queryProjectBreakdown(db) {
404
435
  return db.prepare(`
436
+ WITH labeled AS (
437
+ SELECT
438
+ s.id,
439
+ s.project_path,
440
+ s.total_cost_usd,
441
+ s.started_at,
442
+ COALESCE(
443
+ NULLIF(s.project_name, ''),
444
+ CASE
445
+ WHEN s.project_path LIKE '%/%'
446
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
447
+ ELSE s.project_path
448
+ END
449
+ ) as label
450
+ FROM sessions s
451
+ WHERE s.project_path != '' OR s.project_name != ''
452
+ )
405
453
  SELECT
406
- s.project_path,
407
- COALESCE(p.name, s.project_name) as project_name,
408
- COUNT(DISTINCT s.id) as sessions,
409
- COUNT(r.id) as requests,
410
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
411
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
412
- MAX(s.started_at) as last_active
413
- FROM sessions s
414
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
415
- LEFT JOIN requests r ON r.session_id = s.id
416
- WHERE s.project_path != '' OR s.project_name != ''
417
- GROUP BY s.project_path
454
+ MIN(l.project_path) as project_path,
455
+ l.label as project_name,
456
+ COUNT(DISTINCT l.id) as sessions,
457
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
458
+ COALESCE(
459
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
460
+ SUM(l.total_cost_usd)
461
+ ) as cost_usd,
462
+ COALESCE(
463
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
464
+ 0
465
+ ) as total_tokens,
466
+ MAX(l.started_at) as last_active
467
+ FROM labeled l
468
+ GROUP BY l.label
418
469
  ORDER BY cost_usd DESC
419
470
  `).all();
420
471
  }
@@ -528,11 +579,11 @@ function getModelPricing(db, model) {
528
579
  return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
529
580
  }
530
581
  function seedModelPricing(db, defaults) {
531
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
532
- if (existing.count > 0)
533
- return;
582
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
534
583
  const now = new Date().toISOString();
535
584
  for (const [model, p] of Object.entries(defaults)) {
585
+ if (existing.has(model))
586
+ continue;
536
587
  upsertModelPricing(db, {
537
588
  model,
538
589
  input_per_1m: p.inputPer1M,
@@ -79,6 +79,7 @@ var DEFAULT_PRICING;
79
79
  var init_pricing = __esm(() => {
80
80
  init_database();
81
81
  DEFAULT_PRICING = {
82
+ "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
83
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
84
  "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
84
85
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
@@ -89,20 +90,38 @@ var init_pricing = __esm(() => {
89
90
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
90
91
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
91
92
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
92
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
+ "gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
93
94
  "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
97
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
98
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, 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 },
96
102
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
103
+ "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
97
104
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
98
105
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
106
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
107
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
99
108
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
100
109
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
101
110
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
102
111
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
103
112
  o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
104
113
  "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
105
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
114
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
115
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
116
+ "qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
117
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
118
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
119
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
120
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
121
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
122
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
123
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
124
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
106
125
  };
107
126
  });
108
127
 
@@ -253,6 +272,18 @@ function initSchema(db) {
253
272
  machine_id TEXT,
254
273
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
255
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);
256
287
  `);
257
288
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
258
289
  if (!cols.some((c) => c.name === "machine_id")) {
@@ -271,11 +302,11 @@ function periodWhere(period) {
271
302
  case "yesterday":
272
303
  return `DATE(timestamp) = DATE('now', '-1 day')`;
273
304
  case "week":
274
- return `timestamp >= DATE('now', '-7 days')`;
305
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
275
306
  case "month":
276
- return `timestamp >= DATE('now', '-30 days')`;
307
+ return `timestamp >= DATE('now', 'start of month')`;
277
308
  case "year":
278
- return `timestamp >= DATE('now', '-365 days')`;
309
+ return `timestamp >= DATE('now', 'start of year')`;
279
310
  case "all":
280
311
  return "1=1";
281
312
  }
@@ -287,11 +318,11 @@ function sessionPeriodWhere(period) {
287
318
  case "yesterday":
288
319
  return `DATE(started_at) = DATE('now', '-1 day')`;
289
320
  case "week":
290
- return `started_at >= DATE('now', '-7 days')`;
321
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
291
322
  case "month":
292
- return `started_at >= DATE('now', '-30 days')`;
323
+ return `started_at >= DATE('now', 'start of month')`;
293
324
  case "year":
294
- return `started_at >= DATE('now', '-365 days')`;
325
+ return `started_at >= DATE('now', 'start of year')`;
295
326
  case "all":
296
327
  return "1=1";
297
328
  }
@@ -403,19 +434,39 @@ function queryModelBreakdown(db) {
403
434
  }
404
435
  function queryProjectBreakdown(db) {
405
436
  return db.prepare(`
437
+ WITH labeled AS (
438
+ SELECT
439
+ s.id,
440
+ s.project_path,
441
+ s.total_cost_usd,
442
+ s.started_at,
443
+ COALESCE(
444
+ NULLIF(s.project_name, ''),
445
+ CASE
446
+ WHEN s.project_path LIKE '%/%'
447
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
448
+ ELSE s.project_path
449
+ END
450
+ ) as label
451
+ FROM sessions s
452
+ WHERE s.project_path != '' OR s.project_name != ''
453
+ )
406
454
  SELECT
407
- s.project_path,
408
- COALESCE(p.name, s.project_name) as project_name,
409
- COUNT(DISTINCT s.id) as sessions,
410
- COUNT(r.id) as requests,
411
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
412
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
413
- MAX(s.started_at) as last_active
414
- FROM sessions s
415
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
416
- LEFT JOIN requests r ON r.session_id = s.id
417
- WHERE s.project_path != '' OR s.project_name != ''
418
- GROUP BY s.project_path
455
+ MIN(l.project_path) as project_path,
456
+ l.label as project_name,
457
+ COUNT(DISTINCT l.id) as sessions,
458
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
459
+ COALESCE(
460
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
461
+ SUM(l.total_cost_usd)
462
+ ) as cost_usd,
463
+ COALESCE(
464
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
465
+ 0
466
+ ) as total_tokens,
467
+ MAX(l.started_at) as last_active
468
+ FROM labeled l
469
+ GROUP BY l.label
419
470
  ORDER BY cost_usd DESC
420
471
  `).all();
421
472
  }
@@ -557,11 +608,11 @@ function deleteModelPricing(db, model) {
557
608
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
558
609
  }
559
610
  function seedModelPricing(db, defaults) {
560
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
561
- if (existing.count > 0)
562
- return;
611
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
563
612
  const now = new Date().toISOString();
564
613
  for (const [model, p] of Object.entries(defaults)) {
614
+ if (existing.has(model))
615
+ continue;
565
616
  upsertModelPricing(db, {
566
617
  model,
567
618
  input_per_1m: p.inputPer1M,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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",