@hasna/economy 0.2.23 → 0.2.25

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.
@@ -1057,23 +1057,20 @@ function labelForPath(projectPath, projectName) {
1057
1057
  return segments[segments.length - 1] ?? projectPath;
1058
1058
  }
1059
1059
  function queryProjectBreakdown(db, period = "all") {
1060
- const where = sessionPeriodWhere(period);
1060
+ const requestWhere = requestPeriodWhere(period);
1061
+ const sessionWhere = sessionPeriodWhere(period);
1061
1062
  const sessions = db.prepare(`
1062
1063
  SELECT id, project_path, project_name, total_cost_usd, started_at
1063
1064
  FROM sessions
1064
- WHERE ${where}
1065
- AND (project_path != '' OR project_name != '')
1065
+ WHERE project_path != '' OR project_name != ''
1066
1066
  `).all();
1067
1067
  const groups = new Map;
1068
1068
  for (const s of sessions) {
1069
1069
  const label = labelForPath(s.project_path, s.project_name);
1070
1070
  if (!label)
1071
1071
  continue;
1072
- const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
1072
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path };
1073
1073
  g.sessionIds.push(s.id);
1074
- g.totalCost += s.total_cost_usd || 0;
1075
- if (!g.lastActive || s.started_at > g.lastActive)
1076
- g.lastActive = s.started_at;
1077
1074
  if (!g.samplePath)
1078
1075
  g.samplePath = s.project_path;
1079
1076
  groups.set(label, g);
@@ -1083,32 +1080,52 @@ function queryProjectBreakdown(db, period = "all") {
1083
1080
  const placeholders = g.sessionIds.map(() => "?").join(",");
1084
1081
  const reqStats = placeholders.length ? db.prepare(`
1085
1082
  SELECT
1083
+ COUNT(DISTINCT session_id) as sessions,
1086
1084
  COUNT(*) as requests,
1087
1085
  COALESCE(SUM(cost_usd), 0) as cost_usd,
1088
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
1089
- FROM requests WHERE session_id IN (${placeholders})
1090
- `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
1086
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1087
+ MAX(timestamp) as last_active
1088
+ FROM requests
1089
+ WHERE session_id IN (${placeholders})
1090
+ AND ${requestWhere}
1091
+ `).get(...g.sessionIds) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1092
+ const sessionOnlyStats = placeholders.length ? db.prepare(`
1093
+ SELECT
1094
+ COUNT(*) as sessions,
1095
+ COALESCE(SUM(request_count), 0) as requests,
1096
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1097
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1098
+ MAX(started_at) as last_active
1099
+ FROM sessions
1100
+ WHERE id IN (${placeholders})
1101
+ AND ${sessionWhere}
1102
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1103
+ `).get(...g.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1104
+ const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1105
+ if (totalSessions === 0)
1106
+ continue;
1107
+ const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1091
1108
  result.push({
1092
1109
  project_path: g.samplePath,
1093
1110
  project_name: label,
1094
- sessions: g.sessionIds.length,
1095
- requests: reqStats.requests,
1096
- total_tokens: reqStats.total_tokens,
1097
- cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
1098
- last_active: g.lastActive
1111
+ sessions: totalSessions,
1112
+ requests: reqStats.requests + sessionOnlyStats.requests,
1113
+ total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1114
+ cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
1115
+ last_active: lastActive
1099
1116
  });
1100
1117
  }
1101
1118
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1102
1119
  return result;
1103
1120
  }
1104
1121
  function queryAccountBreakdown(db, period = "all") {
1105
- const sWhere = sessionPeriodWhere(period);
1122
+ const requestWhere = requestPeriodWhere(period);
1123
+ const sessionWhere = sessionPeriodWhere(period);
1106
1124
  const sessions = db.prepare(`
1107
1125
  SELECT id, account_key, account_tool, account_name, account_email, account_source,
1108
1126
  total_cost_usd, total_tokens, request_count, started_at
1109
1127
  FROM sessions
1110
- WHERE ${sWhere}
1111
- AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
1128
+ WHERE account_key != '' OR account_tool != '' OR account_name != '' OR account_email != ''
1112
1129
  `).all();
1113
1130
  const groups = new Map;
1114
1131
  for (const session of sessions) {
@@ -1120,18 +1137,9 @@ function queryAccountBreakdown(db, period = "all") {
1120
1137
  account_tool: session.account_tool,
1121
1138
  account_name: session.account_name,
1122
1139
  account_email: session.account_email || null,
1123
- account_source: session.account_source || "unknown",
1124
- totalCost: 0,
1125
- totalTokens: 0,
1126
- requests: 0,
1127
- lastActive: ""
1140
+ account_source: session.account_source || "unknown"
1128
1141
  };
1129
1142
  group.sessionIds.push(session.id);
1130
- group.totalCost += session.total_cost_usd || 0;
1131
- group.totalTokens += session.total_tokens || 0;
1132
- group.requests += session.request_count || 0;
1133
- if (!group.lastActive || session.started_at > group.lastActive)
1134
- group.lastActive = session.started_at;
1135
1143
  groups.set(key, group);
1136
1144
  }
1137
1145
  const result = [];
@@ -1139,36 +1147,57 @@ function queryAccountBreakdown(db, period = "all") {
1139
1147
  const placeholders = group.sessionIds.map(() => "?").join(",");
1140
1148
  const reqStats = placeholders ? db.prepare(`
1141
1149
  SELECT
1150
+ COUNT(DISTINCT session_id) as sessions,
1142
1151
  COUNT(*) as requests,
1143
1152
  COALESCE(SUM(cost_usd), 0) as cost_usd,
1144
1153
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1145
1154
  COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1146
1155
  COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1147
1156
  COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1148
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
1149
- FROM requests WHERE session_id IN (${placeholders})
1157
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1158
+ MAX(timestamp) as last_active
1159
+ FROM requests
1160
+ WHERE session_id IN (${placeholders})
1161
+ AND ${requestWhere}
1150
1162
  `).get(...group.sessionIds) : {
1163
+ sessions: 0,
1151
1164
  requests: 0,
1152
1165
  cost_usd: 0,
1153
1166
  total_tokens: 0,
1154
1167
  metered_api_usd: 0,
1155
1168
  subscription_included_usd: 0,
1156
1169
  estimated_usd: 0,
1157
- unknown_usd: 0
1170
+ unknown_usd: 0,
1171
+ last_active: null
1158
1172
  };
1159
- const hasRequestCosts = reqStats.requests > 0;
1160
- const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
1161
- const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
1173
+ const sessionOnlyStats = placeholders ? db.prepare(`
1174
+ SELECT
1175
+ COUNT(*) as sessions,
1176
+ COALESCE(SUM(request_count), 0) as requests,
1177
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1178
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1179
+ MAX(started_at) as last_active
1180
+ FROM sessions
1181
+ WHERE id IN (${placeholders})
1182
+ AND ${sessionWhere}
1183
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1184
+ `).get(...group.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1185
+ const sessionsTotal = reqStats.sessions + sessionOnlyStats.sessions;
1186
+ if (sessionsTotal === 0)
1187
+ continue;
1188
+ const apiEquivalentUsd = reqStats.cost_usd + sessionOnlyStats.cost_usd;
1189
+ const estimatedUsd = reqStats.estimated_usd + sessionOnlyStats.cost_usd;
1162
1190
  const billableUsd = reqStats.metered_api_usd;
1191
+ const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1163
1192
  result.push({
1164
1193
  account_key: key,
1165
1194
  account_tool: group.account_tool,
1166
1195
  account_name: group.account_name,
1167
1196
  account_email: group.account_email,
1168
1197
  account_source: group.account_source,
1169
- sessions: group.sessionIds.length,
1170
- requests: reqStats.requests || group.requests,
1171
- total_tokens: reqStats.total_tokens || group.totalTokens,
1198
+ sessions: sessionsTotal,
1199
+ requests: reqStats.requests + sessionOnlyStats.requests,
1200
+ total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1172
1201
  api_equivalent_usd: apiEquivalentUsd,
1173
1202
  billable_usd: billableUsd,
1174
1203
  metered_api_usd: reqStats.metered_api_usd,
@@ -1176,7 +1205,7 @@ function queryAccountBreakdown(db, period = "all") {
1176
1205
  estimated_usd: estimatedUsd,
1177
1206
  unknown_usd: reqStats.unknown_usd,
1178
1207
  cost_usd: apiEquivalentUsd,
1179
- last_active: group.lastActive
1208
+ last_active: lastActive
1180
1209
  });
1181
1210
  }
1182
1211
  result.sort((a, b) => b.cost_usd - a.cost_usd);
@@ -2059,6 +2088,10 @@ function prorateMonthlyFee(monthlyFee, period) {
2059
2088
  return monthlyFee;
2060
2089
  }
2061
2090
  }
2091
+ function proratedIncludedConsumed(includedUsage, includedCap, period) {
2092
+ const cap = prorateMonthlyFee(includedCap, period);
2093
+ return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
2094
+ }
2062
2095
  function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
2063
2096
  return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
2064
2097
  }
@@ -2086,24 +2119,74 @@ function querySavingsSummary(db, period, agent) {
2086
2119
  AND metric = 'on_demand_usd'
2087
2120
  `).get(...params);
2088
2121
  const subs = db.prepare(`
2089
- SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
2122
+ SELECT
2123
+ COALESCE(SUM(monthly_fee_usd), 0) as fee,
2124
+ COALESCE(SUM(included_usage_usd), 0) as included
2090
2125
  FROM subscriptions
2091
- WHERE active = 1${agent ? " AND agent = ?" : ""}
2126
+ WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
2092
2127
  `).get(...agent ? [agent] : []);
2093
- const subscriptionFee = prorateMonthlyFee(subs.total, period);
2128
+ const subscriptionFee = prorateMonthlyFee(subs.fee, period);
2094
2129
  const apiEquivalent = apiRow.total + includedRow.total;
2130
+ const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
2095
2131
  const onDemand = onDemandRow.total;
2096
2132
  const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
2097
2133
  const byAgent = {};
2098
2134
  if (!agent) {
2135
+ const onDemandByAgent = new Map;
2136
+ for (const row of db.prepare(`
2137
+ SELECT agent, COALESCE(SUM(value), 0) as total
2138
+ FROM usage_snapshots
2139
+ WHERE ${subWhere}
2140
+ AND metric = 'on_demand_usd'
2141
+ GROUP BY agent
2142
+ `).all()) {
2143
+ onDemandByAgent.set(row.agent, row.total);
2144
+ }
2145
+ const subscriptionByAgent = new Map;
2099
2146
  for (const row of db.prepare(`
2100
- SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
2147
+ SELECT agent,
2148
+ COALESCE(SUM(monthly_fee_usd), 0) as fee,
2149
+ COALESCE(SUM(included_usage_usd), 0) as included
2150
+ FROM subscriptions
2151
+ WHERE active = 1 AND agent IS NOT NULL
2152
+ GROUP BY agent
2153
+ `).all()) {
2154
+ subscriptionByAgent.set(row.agent, row);
2155
+ }
2156
+ const globalSubs = db.prepare(`
2157
+ SELECT
2158
+ COALESCE(SUM(monthly_fee_usd), 0) as fee,
2159
+ COALESCE(SUM(included_usage_usd), 0) as included
2160
+ FROM subscriptions
2161
+ WHERE active = 1 AND agent IS NULL
2162
+ `).get();
2163
+ const rows = db.prepare(`
2164
+ SELECT agent,
2165
+ COALESCE(SUM(cost_usd), 0) as api_eq,
2166
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
2167
+ FROM requests WHERE ${where}
2168
+ GROUP BY agent
2169
+ `).all();
2170
+ const totalAgentApiEq = rows.reduce((sum, row) => sum + row.api_eq, 0);
2171
+ for (const row of db.prepare(`
2172
+ SELECT agent,
2173
+ COALESCE(SUM(cost_usd), 0) as api_eq,
2174
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
2101
2175
  FROM requests WHERE ${where}
2102
2176
  GROUP BY agent
2103
2177
  `).all()) {
2178
+ const agentSubs = subscriptionByAgent.get(row.agent) ?? { fee: 0, included: 0 };
2179
+ const globalShare = totalAgentApiEq > 0 ? row.api_eq / totalAgentApiEq : 0;
2180
+ const agentFee = prorateMonthlyFee(agentSubs.fee + globalSubs.fee * globalShare, period);
2181
+ const agentIncludedCap = agentSubs.included + globalSubs.included * globalShare;
2182
+ const agentIncludedConsumed = proratedIncludedConsumed(row.included, agentIncludedCap, period);
2183
+ const agentOnDemand = onDemandByAgent.get(row.agent) ?? 0;
2104
2184
  byAgent[row.agent] = {
2105
2185
  api_equivalent_usd: row.api_eq,
2106
- saved_usd: row.api_eq
2186
+ subscription_fee_usd: agentFee,
2187
+ included_consumed_usd: agentIncludedConsumed,
2188
+ on_demand_usd: agentOnDemand,
2189
+ saved_usd: computeSavedUsd(row.api_eq, agentOnDemand, agentFee)
2107
2190
  };
2108
2191
  }
2109
2192
  }
@@ -2111,7 +2194,7 @@ function querySavingsSummary(db, period, agent) {
2111
2194
  period,
2112
2195
  api_equivalent_usd: apiEquivalent,
2113
2196
  subscription_fee_usd: subscriptionFee,
2114
- included_consumed_usd: includedRow.total,
2197
+ included_consumed_usd: includedConsumed,
2115
2198
  on_demand_usd: onDemand,
2116
2199
  saved_usd: saved,
2117
2200
  by_agent: byAgent
@@ -3687,6 +3770,34 @@ async function syncAll(db, opts = {}) {
3687
3770
  return result;
3688
3771
  }
3689
3772
 
3773
+ // src/lib/periods.ts
3774
+ function ymd(date) {
3775
+ return date.toISOString().substring(0, 10);
3776
+ }
3777
+ function usageSnapshotFilterForPeriod(period) {
3778
+ const now = new Date;
3779
+ switch (period) {
3780
+ case "today":
3781
+ return { date: ymd(now) };
3782
+ case "yesterday": {
3783
+ const yesterday = new Date(now);
3784
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
3785
+ return { date: ymd(yesterday) };
3786
+ }
3787
+ case "week": {
3788
+ const weekAgo = new Date(now);
3789
+ weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
3790
+ return { since: ymd(weekAgo) };
3791
+ }
3792
+ case "month":
3793
+ return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
3794
+ case "year":
3795
+ return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
3796
+ case "all":
3797
+ return {};
3798
+ }
3799
+ }
3800
+
3690
3801
  // src/lib/billing-diff.ts
3691
3802
  init_database();
3692
3803
  var PROVIDER_TO_AGENT = {
@@ -4107,9 +4218,11 @@ function createHandler(db) {
4107
4218
  if (path === "/api/usage" && method === "GET") {
4108
4219
  const period = url.searchParams.get("period") ?? "month";
4109
4220
  const agent = url.searchParams.get("agent") ?? undefined;
4110
- const since = period === "month" ? new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().substring(0, 10) : undefined;
4111
4221
  return ok({
4112
- snapshots: queryUsageSnapshots(db, { agent: agent && isAgent(agent) ? agent : undefined, since }),
4222
+ snapshots: queryUsageSnapshots(db, {
4223
+ agent: agent && isAgent(agent) ? agent : undefined,
4224
+ ...usageSnapshotFilterForPeriod(period)
4225
+ }),
4113
4226
  summary: querySummary(db, period, undefined, true)
4114
4227
  });
4115
4228
  }
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAsC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAgW/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAkW/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",