@hasna/economy 0.2.19 → 0.2.20
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/commands/menubar.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts +0 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +707 -5200
- package/dist/db/database.d.ts +1 -41
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +135 -1202
- package/dist/ingest/claude.d.ts +2 -13
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -2
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -2
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/model-config.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +3 -3
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +487 -2749
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +196 -3090
- package/dist/server/serve.d.ts +2 -10
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -59
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/cli/commands/completion.d.ts +0 -2
- package/dist/cli/commands/completion.d.ts.map +0 -1
- package/dist/cli/commands/extras.d.ts +0 -4
- package/dist/cli/commands/extras.d.ts.map +0 -1
- package/dist/cli/commands/notification.d.ts +0 -8
- package/dist/cli/commands/notification.d.ts.map +0 -1
- package/dist/cli/commands/todos.d.ts +0 -26
- package/dist/cli/commands/todos.d.ts.map +0 -1
- package/dist/cli/commands/tui.d.ts +0 -10
- package/dist/cli/commands/tui.d.ts.map +0 -1
- package/dist/ingest/billing.d.ts +0 -27
- package/dist/ingest/billing.d.ts.map +0 -1
- package/dist/ingest/claude-quota.d.ts +0 -5
- package/dist/ingest/claude-quota.d.ts.map +0 -1
- package/dist/ingest/codex-quota.d.ts +0 -5
- package/dist/ingest/codex-quota.d.ts.map +0 -1
- package/dist/ingest/cursor.d.ts +0 -6
- package/dist/ingest/cursor.d.ts.map +0 -1
- package/dist/ingest/hermes.d.ts +0 -6
- package/dist/ingest/hermes.d.ts.map +0 -1
- package/dist/ingest/opencode.d.ts +0 -7
- package/dist/ingest/opencode.d.ts.map +0 -1
- package/dist/ingest/otel.d.ts +0 -20
- package/dist/ingest/otel.d.ts.map +0 -1
- package/dist/ingest/pi.d.ts +0 -7
- package/dist/ingest/pi.d.ts.map +0 -1
- package/dist/ingest/plugin.d.ts +0 -17
- package/dist/ingest/plugin.d.ts.map +0 -1
- package/dist/lib/agents.d.ts +0 -11
- package/dist/lib/agents.d.ts.map +0 -1
- package/dist/lib/billing-diff.d.ts +0 -22
- package/dist/lib/billing-diff.d.ts.map +0 -1
- package/dist/lib/cloud-sync.d.ts +0 -35
- package/dist/lib/cloud-sync.d.ts.map +0 -1
- package/dist/lib/open-projects.d.ts +0 -19
- package/dist/lib/open-projects.d.ts.map +0 -1
- package/dist/lib/package-metadata.d.ts +0 -8
- package/dist/lib/package-metadata.d.ts.map +0 -1
- package/dist/lib/paths.d.ts +0 -20
- package/dist/lib/paths.d.ts.map +0 -1
- package/dist/lib/savings.d.ts +0 -17
- package/dist/lib/savings.d.ts.map +0 -1
- package/dist/lib/serve-auth.d.ts +0 -4
- package/dist/lib/serve-auth.d.ts.map +0 -1
- package/dist/lib/spikes.d.ts +0 -18
- package/dist/lib/spikes.d.ts.map +0 -1
- package/dist/lib/sync-all.d.ts +0 -28
- package/dist/lib/sync-all.d.ts.map +0 -1
- package/dist/lib/watch-paths.d.ts +0 -3
- package/dist/lib/watch-paths.d.ts.map +0 -1
- package/dist/mcp/http.d.ts +0 -12
- package/dist/mcp/http.d.ts.map +0 -1
- package/dist/mcp/server.d.ts +0 -4
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/otel/index.d.ts +0 -3
- package/dist/otel/index.d.ts.map +0 -1
- package/dist/otel/index.js +0 -1372
package/dist/mcp/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
5
|
var __returnValue = (v) => v;
|
|
5
6
|
function __exportSetter(name, newValue) {
|
|
6
7
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -29,503 +30,88 @@ __export(exports_pricing, {
|
|
|
29
30
|
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
30
31
|
});
|
|
31
32
|
function normalizeModelName(raw) {
|
|
32
|
-
return raw.
|
|
33
|
-
}
|
|
34
|
-
function normalizeModelNamePreservingProvider(raw) {
|
|
35
|
-
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
36
|
-
}
|
|
37
|
-
function modelLookupKeys(raw) {
|
|
38
|
-
const withProvider = normalizeModelNamePreservingProvider(raw);
|
|
39
|
-
const withoutProvider = normalizeModelName(raw);
|
|
40
|
-
return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
|
|
41
|
-
}
|
|
42
|
-
function bestPrefixMatch(normalized, entries) {
|
|
43
|
-
let best = null;
|
|
44
|
-
for (const entry of entries) {
|
|
45
|
-
const [key] = entry;
|
|
46
|
-
if (normalized !== key && !normalized.startsWith(`${key}-`))
|
|
47
|
-
continue;
|
|
48
|
-
if (!best || key.length > best[0].length)
|
|
49
|
-
best = entry;
|
|
50
|
-
}
|
|
51
|
-
return best?.[1] ?? null;
|
|
52
|
-
}
|
|
53
|
-
function bestModelMatch(model, entries) {
|
|
54
|
-
for (const key of modelLookupKeys(model)) {
|
|
55
|
-
const match = bestPrefixMatch(key, entries);
|
|
56
|
-
if (match)
|
|
57
|
-
return match;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
function exactModelMatch(model, entries) {
|
|
62
|
-
for (const key of modelLookupKeys(model)) {
|
|
63
|
-
const match = entries.find(([entryKey]) => entryKey === key);
|
|
64
|
-
if (match)
|
|
65
|
-
return match[1];
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
33
|
+
return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
|
|
68
34
|
}
|
|
69
35
|
function ensurePricingSeeded(db) {
|
|
70
36
|
seedModelPricing(db, DEFAULT_PRICING);
|
|
71
|
-
repairLegacySeededPricing(db);
|
|
72
|
-
repairMissingDefaultCacheWrite1h(db);
|
|
73
|
-
repairMissingDefaultCacheStorage(db);
|
|
74
|
-
removeDeprecatedDefaultPricing(db);
|
|
75
|
-
}
|
|
76
|
-
function repairLegacySeededPricing(db) {
|
|
77
|
-
const now = new Date().toISOString();
|
|
78
|
-
const legacyModels = new Set([
|
|
79
|
-
...Object.keys(LEGACY_DEFAULT_PRICING),
|
|
80
|
-
...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
|
|
81
|
-
]);
|
|
82
|
-
for (const model of legacyModels) {
|
|
83
|
-
const current = getModelPricing(db, model);
|
|
84
|
-
const next = DEFAULT_PRICING[model];
|
|
85
|
-
if (!current || !next)
|
|
86
|
-
continue;
|
|
87
|
-
const legacy = LEGACY_DEFAULT_PRICING[model];
|
|
88
|
-
const legacyRows = [
|
|
89
|
-
...legacy ? [legacy] : [],
|
|
90
|
-
...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
|
|
91
|
-
];
|
|
92
|
-
if (!legacyRows.some((row) => samePricing(current, row)))
|
|
93
|
-
continue;
|
|
94
|
-
upsertModelPricing(db, {
|
|
95
|
-
model,
|
|
96
|
-
input_per_1m: next.inputPer1M,
|
|
97
|
-
output_per_1m: next.outputPer1M,
|
|
98
|
-
cache_read_per_1m: next.cacheReadPer1M,
|
|
99
|
-
cache_write_per_1m: next.cacheWritePer1M,
|
|
100
|
-
cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
|
|
101
|
-
cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
|
|
102
|
-
updated_at: now
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function repairMissingDefaultCacheWrite1h(db) {
|
|
107
|
-
const now = new Date().toISOString();
|
|
108
|
-
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
109
|
-
if (!next.cacheWrite1hPer1M)
|
|
110
|
-
continue;
|
|
111
|
-
const current = getModelPricing(db, model);
|
|
112
|
-
if (!current)
|
|
113
|
-
continue;
|
|
114
|
-
if ((current.cache_write_1h_per_1m ?? 0) !== 0)
|
|
115
|
-
continue;
|
|
116
|
-
if (!sameBasePricing(current, next))
|
|
117
|
-
continue;
|
|
118
|
-
upsertModelPricing(db, {
|
|
119
|
-
model,
|
|
120
|
-
input_per_1m: current.input_per_1m,
|
|
121
|
-
output_per_1m: current.output_per_1m,
|
|
122
|
-
cache_read_per_1m: current.cache_read_per_1m,
|
|
123
|
-
cache_write_per_1m: current.cache_write_per_1m,
|
|
124
|
-
cache_write_1h_per_1m: next.cacheWrite1hPer1M,
|
|
125
|
-
cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
|
|
126
|
-
updated_at: now
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function repairMissingDefaultCacheStorage(db) {
|
|
131
|
-
const now = new Date().toISOString();
|
|
132
|
-
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
133
|
-
if (!next.cacheStoragePer1MHour)
|
|
134
|
-
continue;
|
|
135
|
-
const current = getModelPricing(db, model);
|
|
136
|
-
if (!current)
|
|
137
|
-
continue;
|
|
138
|
-
if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
|
|
139
|
-
continue;
|
|
140
|
-
if (!sameBasePricing(current, next))
|
|
141
|
-
continue;
|
|
142
|
-
upsertModelPricing(db, {
|
|
143
|
-
model,
|
|
144
|
-
input_per_1m: current.input_per_1m,
|
|
145
|
-
output_per_1m: current.output_per_1m,
|
|
146
|
-
cache_read_per_1m: current.cache_read_per_1m,
|
|
147
|
-
cache_write_per_1m: current.cache_write_per_1m,
|
|
148
|
-
cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
|
|
149
|
-
cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
|
|
150
|
-
updated_at: now
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function removeDeprecatedDefaultPricing(db) {
|
|
155
|
-
for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
|
|
156
|
-
const current = getModelPricing(db, model);
|
|
157
|
-
if (!current)
|
|
158
|
-
continue;
|
|
159
|
-
if (!removedRows.some((row) => samePricing(current, row)))
|
|
160
|
-
continue;
|
|
161
|
-
deleteModelPricing(db, model);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function sameBasePricing(row, pricing) {
|
|
165
|
-
return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M;
|
|
166
|
-
}
|
|
167
|
-
function samePricing(row, pricing) {
|
|
168
|
-
return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M && (row.cache_write_1h_per_1m ?? 0) === (pricing.cacheWrite1hPer1M ?? 0) && (row.cache_storage_per_1m_hour ?? 0) === (pricing.cacheStoragePer1MHour ?? 0);
|
|
169
37
|
}
|
|
170
38
|
function getPricingFromDb(db, model) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
39
|
+
const normalized = normalizeModelName(model);
|
|
40
|
+
const row = getModelPricing(db, normalized);
|
|
41
|
+
if (row) {
|
|
42
|
+
return {
|
|
43
|
+
inputPer1M: row.input_per_1m,
|
|
44
|
+
outputPer1M: row.output_per_1m,
|
|
45
|
+
cacheReadPer1M: row.cache_read_per_1m,
|
|
46
|
+
cacheWritePer1M: row.cache_write_per_1m
|
|
47
|
+
};
|
|
177
48
|
}
|
|
178
49
|
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const seeded = DEFAULT_PRICING[row.model];
|
|
186
|
-
const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
|
|
187
|
-
return {
|
|
188
|
-
inputPer1M: row.input_per_1m,
|
|
189
|
-
outputPer1M: row.output_per_1m,
|
|
190
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
191
|
-
cacheWritePer1M: row.cache_write_per_1m,
|
|
192
|
-
cacheWrite1hPer1M,
|
|
193
|
-
cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
|
|
194
|
-
};
|
|
50
|
+
for (const r of allRows) {
|
|
51
|
+
if (normalized.startsWith(r.model)) {
|
|
52
|
+
return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
195
56
|
}
|
|
196
57
|
function getPricing(model) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
58
|
+
const normalized = normalizeModelName(model);
|
|
59
|
+
if (DEFAULT_PRICING[normalized])
|
|
60
|
+
return DEFAULT_PRICING[normalized] ?? null;
|
|
61
|
+
for (const key of Object.keys(DEFAULT_PRICING)) {
|
|
62
|
+
if (normalized.startsWith(key))
|
|
63
|
+
return DEFAULT_PRICING[key] ?? null;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
203
66
|
}
|
|
204
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0
|
|
67
|
+
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
205
68
|
const pricing = getPricing(model);
|
|
206
69
|
if (!pricing)
|
|
207
70
|
return 0;
|
|
208
|
-
return
|
|
71
|
+
return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
|
|
209
72
|
}
|
|
210
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0
|
|
73
|
+
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
211
74
|
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
212
75
|
if (!pricing)
|
|
213
76
|
return 0;
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
217
|
-
if (isFreeModel(model))
|
|
218
|
-
return 0;
|
|
219
|
-
let effective = pricing;
|
|
220
|
-
const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
|
|
221
|
-
if (promptTier) {
|
|
222
|
-
const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
|
|
223
|
-
if (billablePromptTokens > promptTier.threshold) {
|
|
224
|
-
effective = {
|
|
225
|
-
...pricing,
|
|
226
|
-
inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
|
|
227
|
-
outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
|
|
228
|
-
cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
|
|
229
|
-
cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
77
|
+
return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
|
|
234
78
|
}
|
|
235
|
-
var DEFAULT_PRICING
|
|
79
|
+
var DEFAULT_PRICING;
|
|
236
80
|
var init_pricing = __esm(() => {
|
|
237
81
|
init_database();
|
|
238
82
|
DEFAULT_PRICING = {
|
|
239
|
-
"claude-opus-4-
|
|
240
|
-
"claude-opus-4-
|
|
241
|
-
"claude-
|
|
242
|
-
"claude-
|
|
243
|
-
"claude-
|
|
244
|
-
"claude-
|
|
245
|
-
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
246
|
-
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
247
|
-
"claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
248
|
-
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
|
|
249
|
-
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
|
|
250
|
-
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
251
|
-
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
|
|
252
|
-
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
253
|
-
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
254
|
-
"gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
255
|
-
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
256
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
257
|
-
"gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
258
|
-
"gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
259
|
-
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
260
|
-
"gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
261
|
-
"google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
|
|
262
|
-
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
263
|
-
"google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
264
|
-
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
|
|
265
|
-
"google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
|
|
266
|
-
"google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
|
|
267
|
-
"google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
|
|
268
|
-
"gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
269
|
-
"gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
270
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
271
|
-
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
272
|
-
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
273
|
-
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
274
|
-
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
275
|
-
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
276
|
-
"gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
277
|
-
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
278
|
-
"gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
279
|
-
"gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
|
|
280
|
-
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
281
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
282
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
283
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
284
|
-
"o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
285
|
-
o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
286
|
-
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
287
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
288
|
-
"qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
289
|
-
"qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
290
|
-
"qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
291
|
-
"qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
292
|
-
"qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
293
|
-
"qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
294
|
-
"qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
295
|
-
"qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
296
|
-
"qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
297
|
-
"qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
298
|
-
"minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
299
|
-
"minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
300
|
-
"minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
301
|
-
"minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
302
|
-
"minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
303
|
-
"grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
304
|
-
"grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
305
|
-
"grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
306
|
-
"grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
307
|
-
"grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
308
|
-
"grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
309
|
-
"grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
310
|
-
"grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
311
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
312
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
|
|
313
|
-
"glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
|
|
314
|
-
"glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
315
|
-
"z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
|
|
316
|
-
"z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
|
|
317
|
-
"kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
|
|
318
|
-
"kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
|
|
319
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
320
|
-
"moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
321
|
-
"moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
|
|
322
|
-
"moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
323
|
-
};
|
|
324
|
-
LEGACY_DEFAULT_PRICING = {
|
|
83
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
84
|
+
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
85
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
86
|
+
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
87
|
+
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
88
|
+
"claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
325
89
|
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
326
|
-
"claude-opus
|
|
327
|
-
"
|
|
328
|
-
"
|
|
90
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
91
|
+
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
92
|
+
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
329
93
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
94
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
95
|
+
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
96
|
+
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
330
97
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
331
98
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
332
99
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
333
|
-
"gpt-
|
|
334
|
-
"gpt-
|
|
100
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
101
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
102
|
+
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
335
103
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
336
|
-
|
|
337
|
-
"
|
|
338
|
-
"
|
|
339
|
-
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
340
|
-
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
341
|
-
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
342
|
-
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
343
|
-
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
344
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
345
|
-
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
|
|
346
|
-
};
|
|
347
|
-
ADDITIONAL_LEGACY_DEFAULT_PRICING = {
|
|
348
|
-
"gemini-2.5-pro": [
|
|
349
|
-
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
350
|
-
],
|
|
351
|
-
"qwen3.6-plus": [
|
|
352
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
353
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
354
|
-
],
|
|
355
|
-
"qwen3.6-flash": [
|
|
356
|
-
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
357
|
-
],
|
|
358
|
-
"qwen3.6-max-preview": [
|
|
359
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
360
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
361
|
-
],
|
|
362
|
-
"qwen/qwen3.6-plus": [
|
|
363
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
364
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
365
|
-
],
|
|
366
|
-
"qwen/qwen3.6-flash": [
|
|
367
|
-
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
368
|
-
],
|
|
369
|
-
"qwen/qwen3.6-max-preview": [
|
|
370
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
371
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
372
|
-
]
|
|
373
|
-
};
|
|
374
|
-
REMOVED_DEFAULT_PRICING = {
|
|
375
|
-
"claude-3-5-sonnet": [
|
|
376
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
377
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
378
|
-
],
|
|
379
|
-
"claude-3-sonnet": [
|
|
380
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
381
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
382
|
-
],
|
|
383
|
-
"gemini-3.1-pro": [
|
|
384
|
-
{ inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
385
|
-
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
|
|
386
|
-
],
|
|
387
|
-
"gemini-1.5-pro": [
|
|
388
|
-
{ inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
389
|
-
],
|
|
390
|
-
"gemini-1.5-flash": [
|
|
391
|
-
{ inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
392
|
-
],
|
|
393
|
-
"gpt-5.3-chat": [
|
|
394
|
-
{ inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
395
|
-
{ inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
|
|
396
|
-
],
|
|
397
|
-
"qwen3.6": [
|
|
398
|
-
{ inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
399
|
-
]
|
|
400
|
-
};
|
|
401
|
-
FREE_PRICING = {
|
|
402
|
-
inputPer1M: 0,
|
|
403
|
-
outputPer1M: 0,
|
|
404
|
-
cacheReadPer1M: 0,
|
|
405
|
-
cacheWritePer1M: 0,
|
|
406
|
-
cacheWrite1hPer1M: 0,
|
|
407
|
-
cacheStoragePer1MHour: 0
|
|
408
|
-
};
|
|
409
|
-
GEMINI_PROMPT_TIERS = {
|
|
410
|
-
"gemini-3.1-pro-preview": {
|
|
411
|
-
threshold: 200000,
|
|
412
|
-
inputPer1M: 4,
|
|
413
|
-
outputPer1M: 18,
|
|
414
|
-
cacheReadPer1M: 0.4
|
|
415
|
-
},
|
|
416
|
-
"gemini-2.5-pro": {
|
|
417
|
-
threshold: 200000,
|
|
418
|
-
inputPer1M: 2.5,
|
|
419
|
-
outputPer1M: 15,
|
|
420
|
-
cacheReadPer1M: 0.25
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
OPENAI_PROMPT_TIERS = {
|
|
424
|
-
"gpt-5.5": {
|
|
425
|
-
threshold: 272000,
|
|
426
|
-
inputMultiplier: 2,
|
|
427
|
-
outputMultiplier: 1.5,
|
|
428
|
-
cacheReadMultiplier: 2
|
|
429
|
-
},
|
|
430
|
-
"gpt-5.4-pro": {
|
|
431
|
-
threshold: 272000,
|
|
432
|
-
inputMultiplier: 2,
|
|
433
|
-
outputMultiplier: 1.5,
|
|
434
|
-
cacheReadMultiplier: 2
|
|
435
|
-
},
|
|
436
|
-
"gpt-5.4": {
|
|
437
|
-
threshold: 272000,
|
|
438
|
-
inputMultiplier: 2,
|
|
439
|
-
outputMultiplier: 1.5,
|
|
440
|
-
cacheReadMultiplier: 2
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
QWEN_PROMPT_TIERS = {
|
|
444
|
-
"qwen3.6-plus": {
|
|
445
|
-
threshold: 256000,
|
|
446
|
-
inputPer1M: 1.3,
|
|
447
|
-
outputPer1M: 3.9,
|
|
448
|
-
cacheReadPer1M: 0.13,
|
|
449
|
-
cacheWritePer1M: 1.625
|
|
450
|
-
},
|
|
451
|
-
"qwen3.6-flash": {
|
|
452
|
-
threshold: 256000,
|
|
453
|
-
inputPer1M: 1,
|
|
454
|
-
outputPer1M: 4,
|
|
455
|
-
cacheReadPer1M: 0.1,
|
|
456
|
-
cacheWritePer1M: 1.25
|
|
457
|
-
},
|
|
458
|
-
"qwen3.6-max-preview": {
|
|
459
|
-
threshold: 128000,
|
|
460
|
-
inputPer1M: 1.6,
|
|
461
|
-
outputPer1M: 9.6,
|
|
462
|
-
cacheReadPer1M: 0.16,
|
|
463
|
-
cacheWritePer1M: 2
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
MINIMAX_PROMPT_TIERS = {
|
|
467
|
-
"minimax/minimax-m1": {
|
|
468
|
-
threshold: Number.POSITIVE_INFINITY
|
|
469
|
-
},
|
|
470
|
-
"minimax-m1": {
|
|
471
|
-
threshold: 200000,
|
|
472
|
-
inputPer1M: 1.3
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
XAI_PROMPT_TIERS = {
|
|
476
|
-
"grok-4.3": {
|
|
477
|
-
threshold: 200000,
|
|
478
|
-
inputPer1M: 2.5,
|
|
479
|
-
outputPer1M: 5,
|
|
480
|
-
cacheReadPer1M: 0.4
|
|
481
|
-
},
|
|
482
|
-
"grok-latest": {
|
|
483
|
-
threshold: 200000,
|
|
484
|
-
inputPer1M: 2.5,
|
|
485
|
-
outputPer1M: 5,
|
|
486
|
-
cacheReadPer1M: 0.4
|
|
487
|
-
},
|
|
488
|
-
"grok-4.20": {
|
|
489
|
-
threshold: 200000,
|
|
490
|
-
inputPer1M: 2.5,
|
|
491
|
-
outputPer1M: 5,
|
|
492
|
-
cacheReadPer1M: 0.4
|
|
493
|
-
},
|
|
494
|
-
"grok-4-1-fast": {
|
|
495
|
-
threshold: 128000,
|
|
496
|
-
inputPer1M: 0.4,
|
|
497
|
-
outputPer1M: 1,
|
|
498
|
-
cacheReadPer1M: 0
|
|
499
|
-
},
|
|
500
|
-
"grok-4-fast": {
|
|
501
|
-
threshold: 128000,
|
|
502
|
-
inputPer1M: 0.4,
|
|
503
|
-
outputPer1M: 1,
|
|
504
|
-
cacheReadPer1M: 0
|
|
505
|
-
},
|
|
506
|
-
"grok-4": {
|
|
507
|
-
threshold: 128000,
|
|
508
|
-
inputPer1M: 6,
|
|
509
|
-
outputPer1M: 30,
|
|
510
|
-
cacheReadPer1M: 0
|
|
511
|
-
}
|
|
104
|
+
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
|
|
105
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
106
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
512
107
|
};
|
|
513
108
|
});
|
|
514
109
|
|
|
515
110
|
// src/db/database.ts
|
|
516
111
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
517
112
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
518
|
-
import { hostname } from "os";
|
|
519
113
|
import { homedir } from "os";
|
|
520
114
|
import { join } from "path";
|
|
521
|
-
function getMachineId() {
|
|
522
|
-
if (process.env["ECONOMY_MACHINE_ID"])
|
|
523
|
-
return process.env["ECONOMY_MACHINE_ID"];
|
|
524
|
-
const h = hostname().toLowerCase();
|
|
525
|
-
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
526
|
-
return h.split(".")[0];
|
|
527
|
-
return h.split(".")[0];
|
|
528
|
-
}
|
|
529
115
|
function getDataDir() {
|
|
530
116
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
531
117
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -558,7 +144,6 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
558
144
|
}
|
|
559
145
|
const db = new Database(path);
|
|
560
146
|
db.exec("PRAGMA journal_mode = WAL");
|
|
561
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
562
147
|
db.exec("PRAGMA foreign_keys = ON");
|
|
563
148
|
initSchema(db);
|
|
564
149
|
if (!skipSeed) {
|
|
@@ -577,13 +162,10 @@ function initSchema(db) {
|
|
|
577
162
|
output_tokens INTEGER DEFAULT 0,
|
|
578
163
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
579
164
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
580
|
-
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
581
|
-
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
582
165
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
583
166
|
duration_ms INTEGER DEFAULT 0,
|
|
584
167
|
timestamp TEXT NOT NULL,
|
|
585
|
-
source_request_id TEXT
|
|
586
|
-
machine_id TEXT DEFAULT ''
|
|
168
|
+
source_request_id TEXT
|
|
587
169
|
);
|
|
588
170
|
|
|
589
171
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -595,8 +177,7 @@ function initSchema(db) {
|
|
|
595
177
|
ended_at TEXT,
|
|
596
178
|
total_cost_usd REAL DEFAULT 0,
|
|
597
179
|
total_tokens INTEGER DEFAULT 0,
|
|
598
|
-
request_count INTEGER DEFAULT 0
|
|
599
|
-
machine_id TEXT DEFAULT ''
|
|
180
|
+
request_count INTEGER DEFAULT 0
|
|
600
181
|
);
|
|
601
182
|
|
|
602
183
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -649,8 +230,6 @@ function initSchema(db) {
|
|
|
649
230
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
650
231
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
651
232
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
652
|
-
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
653
|
-
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
654
233
|
updated_at TEXT NOT NULL
|
|
655
234
|
);
|
|
656
235
|
|
|
@@ -663,115 +242,6 @@ function initSchema(db) {
|
|
|
663
242
|
machine_id TEXT,
|
|
664
243
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
665
244
|
);
|
|
666
|
-
|
|
667
|
-
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
668
|
-
date TEXT NOT NULL,
|
|
669
|
-
provider TEXT NOT NULL,
|
|
670
|
-
description TEXT DEFAULT '',
|
|
671
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
672
|
-
updated_at TEXT NOT NULL,
|
|
673
|
-
PRIMARY KEY (date, provider, description)
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
677
|
-
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
678
|
-
|
|
679
|
-
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
680
|
-
id TEXT PRIMARY KEY,
|
|
681
|
-
agent TEXT,
|
|
682
|
-
provider TEXT NOT NULL,
|
|
683
|
-
plan TEXT NOT NULL,
|
|
684
|
-
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
685
|
-
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
686
|
-
billing_cycle_start TEXT,
|
|
687
|
-
reset_policy TEXT DEFAULT 'monthly',
|
|
688
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
689
|
-
created_at TEXT NOT NULL,
|
|
690
|
-
updated_at TEXT NOT NULL
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
694
|
-
id TEXT PRIMARY KEY,
|
|
695
|
-
agent TEXT NOT NULL,
|
|
696
|
-
date TEXT NOT NULL,
|
|
697
|
-
metric TEXT NOT NULL,
|
|
698
|
-
value REAL NOT NULL DEFAULT 0,
|
|
699
|
-
unit TEXT DEFAULT '',
|
|
700
|
-
machine_id TEXT DEFAULT '',
|
|
701
|
-
updated_at TEXT NOT NULL
|
|
702
|
-
);
|
|
703
|
-
|
|
704
|
-
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
705
|
-
date TEXT NOT NULL,
|
|
706
|
-
agent TEXT DEFAULT '',
|
|
707
|
-
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
708
|
-
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
709
|
-
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
710
|
-
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
711
|
-
saved_usd REAL NOT NULL DEFAULT 0,
|
|
712
|
-
updated_at TEXT NOT NULL,
|
|
713
|
-
PRIMARY KEY (date, agent)
|
|
714
|
-
);
|
|
715
|
-
|
|
716
|
-
CREATE TABLE IF NOT EXISTS machines (
|
|
717
|
-
machine_id TEXT PRIMARY KEY,
|
|
718
|
-
hostname TEXT NOT NULL,
|
|
719
|
-
last_seen_at TEXT,
|
|
720
|
-
last_push_at TEXT,
|
|
721
|
-
last_pull_at TEXT,
|
|
722
|
-
economy_version TEXT,
|
|
723
|
-
updated_at TEXT NOT NULL
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
727
|
-
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
728
|
-
`);
|
|
729
|
-
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
730
|
-
if (!cols.some((c) => c.name === "machine_id")) {
|
|
731
|
-
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
732
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
733
|
-
}
|
|
734
|
-
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
735
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
736
|
-
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
737
|
-
}
|
|
738
|
-
if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
|
|
739
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
|
|
740
|
-
}
|
|
741
|
-
if (!cols.some((c) => c.name === "cost_basis")) {
|
|
742
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
743
|
-
}
|
|
744
|
-
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
745
|
-
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
746
|
-
}
|
|
747
|
-
if (!cols.some((c) => c.name === "updated_at")) {
|
|
748
|
-
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
749
|
-
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
750
|
-
}
|
|
751
|
-
if (!cols.some((c) => c.name === "synced_at")) {
|
|
752
|
-
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
753
|
-
}
|
|
754
|
-
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
755
|
-
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
756
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
757
|
-
}
|
|
758
|
-
if (!sessionCols.some((c) => c.name === "updated_at")) {
|
|
759
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
760
|
-
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
761
|
-
}
|
|
762
|
-
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
763
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
764
|
-
}
|
|
765
|
-
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
766
|
-
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
767
|
-
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
768
|
-
}
|
|
769
|
-
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
770
|
-
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
771
|
-
}
|
|
772
|
-
db.exec(`
|
|
773
|
-
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
774
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
775
245
|
`);
|
|
776
246
|
}
|
|
777
247
|
function periodWhere(period) {
|
|
@@ -781,11 +251,11 @@ function periodWhere(period) {
|
|
|
781
251
|
case "yesterday":
|
|
782
252
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
783
253
|
case "week":
|
|
784
|
-
return `timestamp >= DATE('now', '
|
|
254
|
+
return `timestamp >= DATE('now', '-7 days')`;
|
|
785
255
|
case "month":
|
|
786
|
-
return `timestamp >= DATE('now', '
|
|
256
|
+
return `timestamp >= DATE('now', '-30 days')`;
|
|
787
257
|
case "year":
|
|
788
|
-
return `timestamp >= DATE('now', '
|
|
258
|
+
return `timestamp >= DATE('now', '-365 days')`;
|
|
789
259
|
case "all":
|
|
790
260
|
return "1=1";
|
|
791
261
|
}
|
|
@@ -797,34 +267,31 @@ function sessionPeriodWhere(period) {
|
|
|
797
267
|
case "yesterday":
|
|
798
268
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
799
269
|
case "week":
|
|
800
|
-
return `started_at >= DATE('now', '
|
|
270
|
+
return `started_at >= DATE('now', '-7 days')`;
|
|
801
271
|
case "month":
|
|
802
|
-
return `started_at >= DATE('now', '
|
|
272
|
+
return `started_at >= DATE('now', '-30 days')`;
|
|
803
273
|
case "year":
|
|
804
|
-
return `started_at >= DATE('now', '
|
|
274
|
+
return `started_at >= DATE('now', '-365 days')`;
|
|
805
275
|
case "all":
|
|
806
276
|
return "1=1";
|
|
807
277
|
}
|
|
808
278
|
}
|
|
809
279
|
function upsertRequest(db, req) {
|
|
810
|
-
const now = req.updated_at ?? new Date().toISOString();
|
|
811
280
|
db.prepare(`
|
|
812
281
|
INSERT OR REPLACE INTO requests
|
|
813
282
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
814
|
-
cache_read_tokens, cache_create_tokens,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
|
|
283
|
+
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
284
|
+
timestamp, source_request_id)
|
|
285
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
286
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
|
|
819
287
|
}
|
|
820
288
|
function upsertSession(db, session) {
|
|
821
|
-
const now = session.updated_at ?? new Date().toISOString();
|
|
822
289
|
db.prepare(`
|
|
823
290
|
INSERT OR REPLACE INTO sessions
|
|
824
291
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
825
|
-
total_cost_usd, total_tokens, request_count
|
|
826
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
827
|
-
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count
|
|
292
|
+
total_cost_usd, total_tokens, request_count)
|
|
293
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
294
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
|
|
828
295
|
}
|
|
829
296
|
function rollupSession(db, sessionId) {
|
|
830
297
|
db.prepare(`
|
|
@@ -854,10 +321,6 @@ function querySessions(db, filter = {}) {
|
|
|
854
321
|
conditions.push("started_at >= ?");
|
|
855
322
|
params.push(filter.since);
|
|
856
323
|
}
|
|
857
|
-
if (filter.machine) {
|
|
858
|
-
conditions.push("machine_id = ?");
|
|
859
|
-
params.push(filter.machine);
|
|
860
|
-
}
|
|
861
324
|
if (filter.search) {
|
|
862
325
|
const q = `%${filter.search}%`;
|
|
863
326
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -876,25 +339,24 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
876
339
|
}
|
|
877
340
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
878
341
|
}
|
|
879
|
-
function querySummary(db, period
|
|
342
|
+
function querySummary(db, period) {
|
|
880
343
|
const rWhere = periodWhere(period);
|
|
881
344
|
const sWhere = sessionPeriodWhere(period);
|
|
882
|
-
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
883
345
|
const r = db.prepare(`
|
|
884
346
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
885
347
|
COUNT(*) as requests,
|
|
886
348
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
887
|
-
FROM requests WHERE ${rWhere}
|
|
349
|
+
FROM requests WHERE ${rWhere}
|
|
888
350
|
`).get();
|
|
889
351
|
const codexTotals = db.prepare(`
|
|
890
352
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
891
353
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
892
354
|
COUNT(*) as sessions
|
|
893
355
|
FROM sessions
|
|
894
|
-
WHERE ${sWhere}
|
|
356
|
+
WHERE ${sWhere}
|
|
895
357
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
896
358
|
`).get();
|
|
897
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}
|
|
359
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
898
360
|
return {
|
|
899
361
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
900
362
|
requests: r.requests,
|
|
@@ -914,66 +376,23 @@ function queryModelBreakdown(db) {
|
|
|
914
376
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
915
377
|
`).all();
|
|
916
378
|
}
|
|
917
|
-
function labelForPath(projectPath, projectName) {
|
|
918
|
-
if (projectName && projectName.trim() !== "")
|
|
919
|
-
return projectName;
|
|
920
|
-
if (!projectPath)
|
|
921
|
-
return "";
|
|
922
|
-
const segments = projectPath.split("/").filter(Boolean);
|
|
923
|
-
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
924
|
-
for (const seg of segments) {
|
|
925
|
-
if (projectPrefix.test(seg))
|
|
926
|
-
return seg;
|
|
927
|
-
}
|
|
928
|
-
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
929
|
-
for (let i = segments.length - 1;i >= 0; i--) {
|
|
930
|
-
if (!generic.has(segments[i].toLowerCase()))
|
|
931
|
-
return segments[i];
|
|
932
|
-
}
|
|
933
|
-
return segments[segments.length - 1] ?? projectPath;
|
|
934
|
-
}
|
|
935
379
|
function queryProjectBreakdown(db) {
|
|
936
|
-
|
|
937
|
-
SELECT
|
|
938
|
-
|
|
939
|
-
|
|
380
|
+
return db.prepare(`
|
|
381
|
+
SELECT
|
|
382
|
+
s.project_path,
|
|
383
|
+
COALESCE(p.name, s.project_name) as project_name,
|
|
384
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
385
|
+
COUNT(r.id) as requests,
|
|
386
|
+
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
387
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
388
|
+
MAX(s.started_at) as last_active
|
|
389
|
+
FROM sessions s
|
|
390
|
+
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
391
|
+
LEFT JOIN requests r ON r.session_id = s.id
|
|
392
|
+
WHERE s.project_path != '' OR s.project_name != ''
|
|
393
|
+
GROUP BY s.project_path
|
|
394
|
+
ORDER BY cost_usd DESC
|
|
940
395
|
`).all();
|
|
941
|
-
const groups = new Map;
|
|
942
|
-
for (const s of sessions) {
|
|
943
|
-
const label = labelForPath(s.project_path, s.project_name);
|
|
944
|
-
if (!label)
|
|
945
|
-
continue;
|
|
946
|
-
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
947
|
-
g.sessionIds.push(s.id);
|
|
948
|
-
g.totalCost += s.total_cost_usd || 0;
|
|
949
|
-
if (!g.lastActive || s.started_at > g.lastActive)
|
|
950
|
-
g.lastActive = s.started_at;
|
|
951
|
-
if (!g.samplePath)
|
|
952
|
-
g.samplePath = s.project_path;
|
|
953
|
-
groups.set(label, g);
|
|
954
|
-
}
|
|
955
|
-
const result = [];
|
|
956
|
-
for (const [label, g] of groups.entries()) {
|
|
957
|
-
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
958
|
-
const reqStats = placeholders.length ? db.prepare(`
|
|
959
|
-
SELECT
|
|
960
|
-
COUNT(*) as requests,
|
|
961
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
962
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
963
|
-
FROM requests WHERE session_id IN (${placeholders})
|
|
964
|
-
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
965
|
-
result.push({
|
|
966
|
-
project_path: g.samplePath,
|
|
967
|
-
project_name: label,
|
|
968
|
-
sessions: g.sessionIds.length,
|
|
969
|
-
requests: reqStats.requests,
|
|
970
|
-
total_tokens: reqStats.total_tokens,
|
|
971
|
-
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
972
|
-
last_active: g.lastActive
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
976
|
-
return result;
|
|
977
396
|
}
|
|
978
397
|
function queryDailyBreakdown(db, days = 30) {
|
|
979
398
|
return db.prepare(`
|
|
@@ -984,19 +403,9 @@ function queryDailyBreakdown(db, days = 30) {
|
|
|
984
403
|
ORDER BY date ASC
|
|
985
404
|
`).all(`-${days}`);
|
|
986
405
|
}
|
|
987
|
-
function upsertBudget(db, budget) {
|
|
988
|
-
db.prepare(`
|
|
989
|
-
INSERT OR REPLACE INTO budgets
|
|
990
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
991
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
992
|
-
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
993
|
-
}
|
|
994
406
|
function listBudgets(db) {
|
|
995
407
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
996
408
|
}
|
|
997
|
-
function deleteBudget(db, id) {
|
|
998
|
-
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
999
|
-
}
|
|
1000
409
|
function getBudgetStatuses(db) {
|
|
1001
410
|
const budgets = listBudgets(db);
|
|
1002
411
|
return budgets.map((b) => {
|
|
@@ -1070,424 +479,120 @@ function getIngestState(db, source, key) {
|
|
|
1070
479
|
function setIngestState(db, source, key, value) {
|
|
1071
480
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1072
481
|
}
|
|
1073
|
-
function queryBillingSummary(db, period) {
|
|
1074
|
-
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";
|
|
1075
|
-
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
1076
|
-
const by_provider = {};
|
|
1077
|
-
let total = 0;
|
|
1078
|
-
for (const r of rows) {
|
|
1079
|
-
by_provider[r.provider] = r.cost;
|
|
1080
|
-
total += r.cost;
|
|
1081
|
-
}
|
|
1082
|
-
return { total_usd: total, by_provider };
|
|
1083
|
-
}
|
|
1084
|
-
function listMachines(db) {
|
|
1085
|
-
return db.prepare(`
|
|
1086
|
-
SELECT
|
|
1087
|
-
s.machine_id,
|
|
1088
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
1089
|
-
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
1090
|
-
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
1091
|
-
MAX(s.started_at) as last_active
|
|
1092
|
-
FROM sessions s
|
|
1093
|
-
WHERE s.machine_id != ''
|
|
1094
|
-
GROUP BY s.machine_id
|
|
1095
|
-
ORDER BY total_cost_usd DESC
|
|
1096
|
-
`).all();
|
|
1097
|
-
}
|
|
1098
482
|
function upsertModelPricing(db, p) {
|
|
1099
483
|
db.prepare(`
|
|
1100
484
|
INSERT OR REPLACE INTO model_pricing
|
|
1101
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m,
|
|
1102
|
-
VALUES (?, ?, ?, ?, ?,
|
|
1103
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.
|
|
485
|
+
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
486
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
487
|
+
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
1104
488
|
}
|
|
1105
489
|
function getModelPricing(db, model) {
|
|
1106
490
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
1107
491
|
}
|
|
1108
|
-
function listModelPricing(db) {
|
|
1109
|
-
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
1110
|
-
}
|
|
1111
|
-
function deleteModelPricing(db, model) {
|
|
1112
|
-
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
1113
|
-
}
|
|
1114
492
|
function seedModelPricing(db, defaults) {
|
|
1115
|
-
const existing =
|
|
493
|
+
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
494
|
+
if (existing.count > 0)
|
|
495
|
+
return;
|
|
1116
496
|
const now = new Date().toISOString();
|
|
1117
497
|
for (const [model, p] of Object.entries(defaults)) {
|
|
1118
|
-
if (existing.has(model))
|
|
1119
|
-
continue;
|
|
1120
498
|
upsertModelPricing(db, {
|
|
1121
499
|
model,
|
|
1122
500
|
input_per_1m: p.inputPer1M,
|
|
1123
501
|
output_per_1m: p.outputPer1M,
|
|
1124
502
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
1125
503
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1126
|
-
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1127
|
-
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
1128
504
|
updated_at: now
|
|
1129
505
|
});
|
|
1130
506
|
}
|
|
1131
507
|
}
|
|
1132
|
-
function upsertSubscription(db, sub) {
|
|
1133
|
-
db.prepare(`
|
|
1134
|
-
INSERT OR REPLACE INTO subscriptions
|
|
1135
|
-
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1136
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1137
|
-
`).run(sub.id, sub.agent, sub.provider, sub.plan, sub.monthly_fee_usd, sub.included_usage_usd, sub.billing_cycle_start, sub.reset_policy, sub.active, sub.created_at, sub.updated_at);
|
|
1138
|
-
}
|
|
1139
|
-
function upsertUsageSnapshot(db, snap) {
|
|
1140
|
-
const now = snap.updated_at ?? new Date().toISOString();
|
|
1141
|
-
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1142
|
-
db.prepare(`
|
|
1143
|
-
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1144
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1145
|
-
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1146
|
-
}
|
|
1147
|
-
function queryUsageSnapshots(db, opts = {}) {
|
|
1148
|
-
const conditions = [];
|
|
1149
|
-
const params = [];
|
|
1150
|
-
if (opts.agent) {
|
|
1151
|
-
conditions.push("agent = ?");
|
|
1152
|
-
params.push(opts.agent);
|
|
1153
|
-
}
|
|
1154
|
-
if (opts.date) {
|
|
1155
|
-
conditions.push("date = ?");
|
|
1156
|
-
params.push(opts.date);
|
|
1157
|
-
}
|
|
1158
|
-
if (opts.since) {
|
|
1159
|
-
conditions.push("date >= ?");
|
|
1160
|
-
params.push(opts.since);
|
|
1161
|
-
}
|
|
1162
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1163
|
-
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1164
|
-
}
|
|
1165
|
-
function dedupeRequests(db) {
|
|
1166
|
-
const dupes = db.prepare(`
|
|
1167
|
-
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1168
|
-
FROM requests
|
|
1169
|
-
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1170
|
-
GROUP BY source_request_id, agent
|
|
1171
|
-
HAVING cnt > 1
|
|
1172
|
-
`).all();
|
|
1173
|
-
let removed = 0;
|
|
1174
|
-
for (const row of dupes) {
|
|
1175
|
-
const result = db.prepare(`
|
|
1176
|
-
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1177
|
-
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1178
|
-
removed += result.changes;
|
|
1179
|
-
}
|
|
1180
|
-
return removed;
|
|
1181
|
-
}
|
|
1182
508
|
var init_database = () => {};
|
|
1183
509
|
|
|
1184
|
-
//
|
|
1185
|
-
var
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
PRIMARY KEY (source, key)
|
|
1253
|
-
)`,
|
|
1254
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1255
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1256
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1257
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1258
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1259
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1260
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1261
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1262
|
-
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1263
|
-
model TEXT PRIMARY KEY,
|
|
1264
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1265
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1266
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1267
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1268
|
-
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
1269
|
-
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
1270
|
-
updated_at TEXT NOT NULL
|
|
1271
|
-
)`,
|
|
1272
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
|
|
1273
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
|
|
1274
|
-
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
|
|
1275
|
-
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
|
|
1276
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1277
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1278
|
-
message TEXT NOT NULL,
|
|
1279
|
-
email TEXT,
|
|
1280
|
-
category TEXT DEFAULT 'general',
|
|
1281
|
-
version TEXT,
|
|
1282
|
-
machine_id TEXT,
|
|
1283
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1284
|
-
)`,
|
|
1285
|
-
`CREATE TABLE IF NOT EXISTS billing_daily (
|
|
1286
|
-
date TEXT NOT NULL,
|
|
1287
|
-
provider TEXT NOT NULL,
|
|
1288
|
-
description TEXT DEFAULT '',
|
|
1289
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1290
|
-
updated_at TEXT NOT NULL,
|
|
1291
|
-
PRIMARY KEY (date, provider, description)
|
|
1292
|
-
)`,
|
|
1293
|
-
`CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
|
|
1294
|
-
`CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
|
|
1295
|
-
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
1296
|
-
id TEXT PRIMARY KEY,
|
|
1297
|
-
agent TEXT,
|
|
1298
|
-
provider TEXT NOT NULL,
|
|
1299
|
-
plan TEXT NOT NULL,
|
|
1300
|
-
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1301
|
-
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
1302
|
-
billing_cycle_start TEXT,
|
|
1303
|
-
reset_policy TEXT DEFAULT 'monthly',
|
|
1304
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
1305
|
-
created_at TEXT NOT NULL,
|
|
1306
|
-
updated_at TEXT NOT NULL
|
|
1307
|
-
)`,
|
|
1308
|
-
`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
1309
|
-
id TEXT PRIMARY KEY,
|
|
1310
|
-
agent TEXT NOT NULL,
|
|
1311
|
-
date TEXT NOT NULL,
|
|
1312
|
-
metric TEXT NOT NULL,
|
|
1313
|
-
value REAL NOT NULL DEFAULT 0,
|
|
1314
|
-
unit TEXT DEFAULT '',
|
|
1315
|
-
machine_id TEXT DEFAULT '',
|
|
1316
|
-
updated_at TEXT NOT NULL
|
|
1317
|
-
)`,
|
|
1318
|
-
`CREATE TABLE IF NOT EXISTS savings_daily (
|
|
1319
|
-
date TEXT NOT NULL,
|
|
1320
|
-
agent TEXT DEFAULT '',
|
|
1321
|
-
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
1322
|
-
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1323
|
-
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
1324
|
-
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
1325
|
-
saved_usd REAL NOT NULL DEFAULT 0,
|
|
1326
|
-
updated_at TEXT NOT NULL,
|
|
1327
|
-
PRIMARY KEY (date, agent)
|
|
1328
|
-
)`,
|
|
1329
|
-
`CREATE TABLE IF NOT EXISTS machines (
|
|
1330
|
-
machine_id TEXT PRIMARY KEY,
|
|
1331
|
-
hostname TEXT NOT NULL,
|
|
1332
|
-
last_seen_at TEXT,
|
|
1333
|
-
last_push_at TEXT,
|
|
1334
|
-
last_pull_at TEXT,
|
|
1335
|
-
economy_version TEXT,
|
|
1336
|
-
updated_at TEXT NOT NULL
|
|
1337
|
-
)`,
|
|
1338
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1339
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1340
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1341
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1342
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1343
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1344
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1345
|
-
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1346
|
-
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
|
|
1347
|
-
];
|
|
510
|
+
// package.json
|
|
511
|
+
var require_package = __commonJS((exports, module) => {
|
|
512
|
+
module.exports = {
|
|
513
|
+
name: "@hasna/economy",
|
|
514
|
+
version: "0.2.10",
|
|
515
|
+
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
|
|
516
|
+
type: "module",
|
|
517
|
+
main: "dist/index.js",
|
|
518
|
+
types: "dist/index.d.ts",
|
|
519
|
+
bin: {
|
|
520
|
+
economy: "dist/cli/index.js",
|
|
521
|
+
"economy-mcp": "dist/mcp/index.js",
|
|
522
|
+
"economy-serve": "dist/server/index.js"
|
|
523
|
+
},
|
|
524
|
+
exports: {
|
|
525
|
+
".": {
|
|
526
|
+
types: "./dist/index.d.ts",
|
|
527
|
+
import: "./dist/index.js"
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
files: [
|
|
531
|
+
"dist",
|
|
532
|
+
"LICENSE"
|
|
533
|
+
],
|
|
534
|
+
scripts: {
|
|
535
|
+
build: "cd dashboard && bun run build && cd .. && bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
|
|
536
|
+
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
537
|
+
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
538
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
539
|
+
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
540
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
541
|
+
typecheck: "tsc --noEmit",
|
|
542
|
+
test: "bun test",
|
|
543
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
544
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
545
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
546
|
+
postinstall: "mkdir -p $HOME/.hasna/economy/training 2>/dev/null || true"
|
|
547
|
+
},
|
|
548
|
+
keywords: [
|
|
549
|
+
"economy",
|
|
550
|
+
"cost",
|
|
551
|
+
"ai",
|
|
552
|
+
"claude",
|
|
553
|
+
"codex",
|
|
554
|
+
"gemini",
|
|
555
|
+
"mcp",
|
|
556
|
+
"cli",
|
|
557
|
+
"budget",
|
|
558
|
+
"tracking"
|
|
559
|
+
],
|
|
560
|
+
author: "hasna",
|
|
561
|
+
license: "Apache-2.0",
|
|
562
|
+
publishConfig: {
|
|
563
|
+
registry: "https://registry.npmjs.org",
|
|
564
|
+
access: "public"
|
|
565
|
+
},
|
|
566
|
+
dependencies: {
|
|
567
|
+
"@hasna/cloud": "^0.1.0",
|
|
568
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
569
|
+
chalk: "^5.4.1",
|
|
570
|
+
commander: "^13.1.0"
|
|
571
|
+
},
|
|
572
|
+
devDependencies: {
|
|
573
|
+
"@types/bun": "latest",
|
|
574
|
+
"bun-types": "latest",
|
|
575
|
+
typescript: "^5.7.2"
|
|
576
|
+
}
|
|
577
|
+
};
|
|
1348
578
|
});
|
|
1349
579
|
|
|
1350
580
|
// src/mcp/index.ts
|
|
1351
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1352
|
-
|
|
1353
|
-
// src/lib/package-metadata.ts
|
|
1354
|
-
import { readFileSync } from "fs";
|
|
1355
|
-
var cachedMetadata = null;
|
|
1356
|
-
function getPackageMetadata() {
|
|
1357
|
-
if (cachedMetadata)
|
|
1358
|
-
return cachedMetadata;
|
|
1359
|
-
const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
|
|
1360
|
-
const parsed = JSON.parse(raw);
|
|
1361
|
-
cachedMetadata = {
|
|
1362
|
-
name: parsed.name ?? "@hasna/economy",
|
|
1363
|
-
version: parsed.version ?? "0.0.0"
|
|
1364
|
-
};
|
|
1365
|
-
return cachedMetadata;
|
|
1366
|
-
}
|
|
1367
|
-
var packageMetadata = getPackageMetadata();
|
|
1368
|
-
|
|
1369
|
-
// src/mcp/server.ts
|
|
1370
581
|
init_database();
|
|
1371
|
-
|
|
1372
|
-
import {
|
|
1373
|
-
import {
|
|
1374
|
-
import { registerCloudTools } from "@hasna/cloud";
|
|
1375
|
-
import { z } from "zod";
|
|
582
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
583
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
584
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1376
585
|
|
|
1377
586
|
// src/ingest/claude.ts
|
|
1378
587
|
init_database();
|
|
1379
588
|
init_pricing();
|
|
1380
|
-
import { readdirSync as readdirSync2, readFileSync
|
|
589
|
+
import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
1381
590
|
import { homedir as homedir2 } from "os";
|
|
1382
591
|
import { join as join2, basename } from "path";
|
|
1383
|
-
|
|
1384
|
-
// src/lib/savings.ts
|
|
1385
|
-
function periodWhere2(period, column) {
|
|
1386
|
-
switch (period) {
|
|
1387
|
-
case "today":
|
|
1388
|
-
return `DATE(${column}) = DATE('now')`;
|
|
1389
|
-
case "yesterday":
|
|
1390
|
-
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
1391
|
-
case "week":
|
|
1392
|
-
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1393
|
-
case "month":
|
|
1394
|
-
return `${column} >= DATE('now', 'start of month')`;
|
|
1395
|
-
case "year":
|
|
1396
|
-
return `${column} >= DATE('now', 'start of year')`;
|
|
1397
|
-
case "all":
|
|
1398
|
-
return "1=1";
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
function prorateMonthlyFee(monthlyFee, period) {
|
|
1402
|
-
const now = new Date;
|
|
1403
|
-
const dayOfMonth = now.getDate();
|
|
1404
|
-
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1405
|
-
switch (period) {
|
|
1406
|
-
case "today":
|
|
1407
|
-
case "yesterday":
|
|
1408
|
-
return monthlyFee / daysInMonth;
|
|
1409
|
-
case "week":
|
|
1410
|
-
return monthlyFee / daysInMonth * 7;
|
|
1411
|
-
case "month":
|
|
1412
|
-
return monthlyFee;
|
|
1413
|
-
case "year":
|
|
1414
|
-
return monthlyFee * 12;
|
|
1415
|
-
case "all":
|
|
1416
|
-
return monthlyFee;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
1420
|
-
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
1421
|
-
}
|
|
1422
|
-
function querySavingsSummary(db, period, agent) {
|
|
1423
|
-
const where = periodWhere2(period, "timestamp");
|
|
1424
|
-
const agentClause = agent ? " AND agent = ?" : "";
|
|
1425
|
-
const params = agent ? [agent] : [];
|
|
1426
|
-
const apiRow = db.prepare(`
|
|
1427
|
-
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1428
|
-
FROM requests
|
|
1429
|
-
WHERE ${where}${agentClause}
|
|
1430
|
-
AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
|
|
1431
|
-
`).get(...params);
|
|
1432
|
-
const includedRow = db.prepare(`
|
|
1433
|
-
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1434
|
-
FROM requests
|
|
1435
|
-
WHERE ${where}${agentClause}
|
|
1436
|
-
AND cost_basis = 'subscription_included'
|
|
1437
|
-
`).get(...params);
|
|
1438
|
-
const subWhere = periodWhere2(period, "date");
|
|
1439
|
-
const onDemandRow = db.prepare(`
|
|
1440
|
-
SELECT COALESCE(SUM(value), 0) as total
|
|
1441
|
-
FROM usage_snapshots
|
|
1442
|
-
WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
|
|
1443
|
-
AND metric = 'on_demand_usd'
|
|
1444
|
-
`).get(...params);
|
|
1445
|
-
const subs = db.prepare(`
|
|
1446
|
-
SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
|
|
1447
|
-
FROM subscriptions
|
|
1448
|
-
WHERE active = 1${agent ? " AND agent = ?" : ""}
|
|
1449
|
-
`).get(...agent ? [agent] : []);
|
|
1450
|
-
const subscriptionFee = prorateMonthlyFee(subs.total, period);
|
|
1451
|
-
const apiEquivalent = apiRow.total + includedRow.total;
|
|
1452
|
-
const onDemand = onDemandRow.total;
|
|
1453
|
-
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
1454
|
-
const byAgent = {};
|
|
1455
|
-
if (!agent) {
|
|
1456
|
-
for (const row of db.prepare(`
|
|
1457
|
-
SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
|
|
1458
|
-
FROM requests WHERE ${where}
|
|
1459
|
-
GROUP BY agent
|
|
1460
|
-
`).all()) {
|
|
1461
|
-
byAgent[row.agent] = {
|
|
1462
|
-
api_equivalent_usd: row.api_eq,
|
|
1463
|
-
saved_usd: row.api_eq
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return {
|
|
1468
|
-
period,
|
|
1469
|
-
api_equivalent_usd: apiEquivalent,
|
|
1470
|
-
subscription_fee_usd: subscriptionFee,
|
|
1471
|
-
included_consumed_usd: includedRow.total,
|
|
1472
|
-
on_demand_usd: onDemand,
|
|
1473
|
-
saved_usd: saved,
|
|
1474
|
-
by_agent: byAgent
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
function defaultCostBasisForAgent(agent) {
|
|
1478
|
-
if (agent === "claude")
|
|
1479
|
-
return "metered_api";
|
|
1480
|
-
if (agent === "cursor")
|
|
1481
|
-
return "subscription_included";
|
|
1482
|
-
return "estimated";
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
// src/ingest/claude.ts
|
|
1486
592
|
function autoDetectProject(cwd, projects) {
|
|
1487
593
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
1488
594
|
}
|
|
1489
|
-
var
|
|
1490
|
-
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
595
|
+
var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
1491
596
|
function dirNameToPath(dirName) {
|
|
1492
597
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
1493
598
|
}
|
|
@@ -1506,42 +611,35 @@ function collectJsonlFiles(projectDir) {
|
|
|
1506
611
|
walk(projectDir);
|
|
1507
612
|
return files;
|
|
1508
613
|
}
|
|
1509
|
-
async function ingestClaude(db, verbose = false,
|
|
1510
|
-
|
|
1511
|
-
}
|
|
1512
|
-
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1513
|
-
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
1514
|
-
}
|
|
1515
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
1516
|
-
if (!existsSync2(projectsDir)) {
|
|
614
|
+
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
615
|
+
if (!existsSync2(PROJECTS_DIR)) {
|
|
1517
616
|
if (verbose)
|
|
1518
|
-
console.log(
|
|
617
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
1519
618
|
return { files: 0, requests: 0, sessions: 0 };
|
|
1520
619
|
}
|
|
1521
|
-
const machineId = getMachineId();
|
|
1522
620
|
let totalFiles = 0;
|
|
1523
621
|
let totalRequests = 0;
|
|
1524
622
|
const touchedSessions = new Set;
|
|
1525
623
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
1526
|
-
const projectDirs = readdirSync2(
|
|
624
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1527
625
|
for (const projectDirEntry of projectDirs) {
|
|
1528
|
-
const projectDirPath = join2(
|
|
626
|
+
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
1529
627
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
1530
628
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
1531
629
|
for (const filePath of jsonlFiles) {
|
|
1532
|
-
const stateKey = filePath.replace(
|
|
630
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
1533
631
|
let fileMtime = "0";
|
|
1534
632
|
try {
|
|
1535
633
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
1536
634
|
} catch {
|
|
1537
635
|
continue;
|
|
1538
636
|
}
|
|
1539
|
-
const processed = getIngestState(db,
|
|
637
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
1540
638
|
if (processed === fileMtime)
|
|
1541
639
|
continue;
|
|
1542
640
|
let lines;
|
|
1543
641
|
try {
|
|
1544
|
-
lines =
|
|
642
|
+
lines = readFileSync(filePath, "utf-8").split(`
|
|
1545
643
|
`).filter((l) => l.trim());
|
|
1546
644
|
} catch {
|
|
1547
645
|
continue;
|
|
@@ -1571,38 +669,26 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1571
669
|
continue;
|
|
1572
670
|
const inputTokens = usage.input_tokens ?? 0;
|
|
1573
671
|
const outputTokens = usage.output_tokens ?? 0;
|
|
1574
|
-
const
|
|
1575
|
-
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1576
|
-
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
672
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
1577
673
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
1578
674
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
1579
|
-
if (inputTokens + outputTokens + cacheWriteTokens
|
|
675
|
+
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1580
676
|
continue;
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const serverToolUse = usage.server_tool_use;
|
|
1584
|
-
if (serverToolUse?.web_search_requests) {
|
|
1585
|
-
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1586
|
-
}
|
|
1587
|
-
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1588
|
-
const reqId = `${agentName}-${sourceRequestId}`;
|
|
677
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
678
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
1589
679
|
upsertRequest(db, {
|
|
1590
680
|
id: reqId,
|
|
1591
|
-
agent:
|
|
681
|
+
agent: "claude",
|
|
1592
682
|
session_id: sessionId,
|
|
1593
683
|
model,
|
|
1594
684
|
input_tokens: inputTokens,
|
|
1595
685
|
output_tokens: outputTokens,
|
|
1596
686
|
cache_read_tokens: cacheReadTokens,
|
|
1597
687
|
cache_create_tokens: cacheWriteTokens,
|
|
1598
|
-
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
1599
|
-
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
1600
688
|
cost_usd: costUsd,
|
|
1601
|
-
cost_basis: defaultCostBasisForAgent(agentName),
|
|
1602
689
|
duration_ms: 0,
|
|
1603
690
|
timestamp,
|
|
1604
|
-
source_request_id:
|
|
1605
|
-
machine_id: machineId
|
|
691
|
+
source_request_id: reqId
|
|
1606
692
|
});
|
|
1607
693
|
if (!touchedSessions.has(sessionId)) {
|
|
1608
694
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -1611,15 +697,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1611
697
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
1612
698
|
const session = {
|
|
1613
699
|
id: sessionId,
|
|
1614
|
-
agent:
|
|
700
|
+
agent: "claude",
|
|
1615
701
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
1616
702
|
project_name: detectedProject ? detectedProject.name : "",
|
|
1617
703
|
started_at: timestamp,
|
|
1618
704
|
ended_at: null,
|
|
1619
705
|
total_cost_usd: 0,
|
|
1620
706
|
total_tokens: 0,
|
|
1621
|
-
request_count: 0
|
|
1622
|
-
machine_id: machineId
|
|
707
|
+
request_count: 0
|
|
1623
708
|
};
|
|
1624
709
|
upsertSession(db, session);
|
|
1625
710
|
}
|
|
@@ -1627,7 +712,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1627
712
|
}
|
|
1628
713
|
totalRequests++;
|
|
1629
714
|
}
|
|
1630
|
-
setIngestState(db,
|
|
715
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
1631
716
|
totalFiles++;
|
|
1632
717
|
}
|
|
1633
718
|
}
|
|
@@ -1636,254 +721,79 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1636
721
|
}
|
|
1637
722
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
1638
723
|
}
|
|
1639
|
-
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
1640
|
-
let multiplier = 1;
|
|
1641
|
-
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
1642
|
-
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
1643
|
-
multiplier *= 6;
|
|
1644
|
-
}
|
|
1645
|
-
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
1646
|
-
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
1647
|
-
multiplier *= 1.1;
|
|
1648
|
-
}
|
|
1649
|
-
return costUsd * multiplier;
|
|
1650
|
-
}
|
|
1651
|
-
function supportsClaudeDataResidencyPricing(model) {
|
|
1652
|
-
const normalized = normalizeModelName(model);
|
|
1653
|
-
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
1654
|
-
if (!match)
|
|
1655
|
-
return false;
|
|
1656
|
-
const major = Number(match[2]);
|
|
1657
|
-
const minor = match[3] ? Number(match[3]) : 0;
|
|
1658
|
-
return major > 4 || major === 4 && minor >= 6;
|
|
1659
|
-
}
|
|
1660
724
|
|
|
1661
725
|
// src/ingest/codex.ts
|
|
1662
726
|
init_database();
|
|
1663
|
-
|
|
1664
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
727
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1665
728
|
import { homedir as homedir3 } from "os";
|
|
1666
729
|
import { join as join3, basename as basename2 } from "path";
|
|
1667
|
-
import { Database as
|
|
1668
|
-
var
|
|
1669
|
-
var
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
1676
|
-
}
|
|
1677
|
-
function readCodexModel() {
|
|
1678
|
-
const configPath = codexConfigPath();
|
|
1679
|
-
if (!existsSync3(configPath))
|
|
1680
|
-
return "gpt-5-codex";
|
|
1681
|
-
try {
|
|
1682
|
-
const content = readFileSync3(configPath, "utf-8");
|
|
1683
|
-
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
1684
|
-
return match?.[1] ?? "gpt-5-codex";
|
|
1685
|
-
} catch {
|
|
1686
|
-
return "gpt-5-codex";
|
|
730
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
731
|
+
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
732
|
+
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
733
|
+
async function ingestCodex(db, verbose = false) {
|
|
734
|
+
if (!existsSync3(CODEX_DB_PATH)) {
|
|
735
|
+
if (verbose)
|
|
736
|
+
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
737
|
+
return { sessions: 0 };
|
|
1687
738
|
}
|
|
1688
|
-
}
|
|
1689
|
-
function buildThreadQuery(codexDb) {
|
|
1690
|
-
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
1691
|
-
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
1692
|
-
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
1693
|
-
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
1694
|
-
return `
|
|
1695
|
-
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
1696
|
-
${providerSelect}, ${modelSelect}
|
|
1697
|
-
FROM threads WHERE tokens_used > 0
|
|
1698
|
-
`;
|
|
1699
|
-
}
|
|
1700
|
-
function readTokenEvents(rolloutPath) {
|
|
1701
|
-
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
1702
|
-
return [];
|
|
1703
|
-
const events = [];
|
|
1704
|
-
const seen = new Set;
|
|
1705
|
-
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
1706
|
-
`)) {
|
|
1707
|
-
if (!line.trim())
|
|
1708
|
-
continue;
|
|
1709
|
-
let entry;
|
|
1710
|
-
try {
|
|
1711
|
-
entry = JSON.parse(line);
|
|
1712
|
-
} catch {
|
|
1713
|
-
continue;
|
|
1714
|
-
}
|
|
1715
|
-
if (!entry || typeof entry !== "object")
|
|
1716
|
-
continue;
|
|
1717
|
-
const payload = entry["payload"];
|
|
1718
|
-
if (!payload || payload["type"] !== "token_count")
|
|
1719
|
-
continue;
|
|
1720
|
-
const info = payload["info"];
|
|
1721
|
-
const usage = info?.["last_token_usage"];
|
|
1722
|
-
if (!usage)
|
|
1723
|
-
continue;
|
|
1724
|
-
const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
1725
|
-
if (total <= 0)
|
|
1726
|
-
continue;
|
|
1727
|
-
const key = JSON.stringify(usage);
|
|
1728
|
-
if (seen.has(key))
|
|
1729
|
-
continue;
|
|
1730
|
-
seen.add(key);
|
|
1731
|
-
const timestamp = entry["timestamp"];
|
|
1732
|
-
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
1733
|
-
}
|
|
1734
|
-
return events;
|
|
1735
|
-
}
|
|
1736
|
-
function fallbackEvents(totalTokens) {
|
|
1737
|
-
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
1738
|
-
return [{
|
|
1739
|
-
usage: {
|
|
1740
|
-
input_tokens: inputTokens,
|
|
1741
|
-
cached_input_tokens: 0,
|
|
1742
|
-
output_tokens: totalTokens - inputTokens,
|
|
1743
|
-
total_tokens: totalTokens
|
|
1744
|
-
}
|
|
1745
|
-
}];
|
|
1746
|
-
}
|
|
1747
|
-
async function ingestCodex(db, verbose = false) {
|
|
1748
|
-
const dbPath = codexDbPath();
|
|
1749
|
-
if (!existsSync3(dbPath)) {
|
|
1750
|
-
if (verbose)
|
|
1751
|
-
console.log("Codex DB not found:", dbPath);
|
|
1752
|
-
return { sessions: 0, requests: 0 };
|
|
1753
|
-
}
|
|
1754
|
-
const machineId = getMachineId();
|
|
1755
739
|
let codexDb = null;
|
|
1756
740
|
let ingested = 0;
|
|
1757
|
-
let requests = 0;
|
|
1758
741
|
try {
|
|
1759
|
-
codexDb = new
|
|
1760
|
-
const threads = codexDb.prepare(
|
|
742
|
+
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
743
|
+
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
1761
744
|
for (const thread of threads) {
|
|
1762
|
-
const
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
if (processed === stateValue)
|
|
745
|
+
const stateKey = thread.id;
|
|
746
|
+
const processed = getIngestState(db, "codex", stateKey);
|
|
747
|
+
if (processed === "done")
|
|
1766
748
|
continue;
|
|
749
|
+
const costUsd = 0;
|
|
1767
750
|
const projectPath = thread.cwd ?? "";
|
|
1768
751
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
1769
|
-
const sessionId = `codex-${thread.id}`;
|
|
1770
752
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
1771
753
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
1772
754
|
upsertSession(db, {
|
|
1773
|
-
id:
|
|
755
|
+
id: `codex-${thread.id}`,
|
|
1774
756
|
agent: "codex",
|
|
1775
757
|
project_path: projectPath,
|
|
1776
758
|
project_name: projectName,
|
|
1777
759
|
started_at: startedAt,
|
|
1778
760
|
ended_at: endedAt,
|
|
1779
|
-
total_cost_usd:
|
|
1780
|
-
total_tokens:
|
|
1781
|
-
request_count:
|
|
1782
|
-
machine_id: machineId
|
|
1783
|
-
});
|
|
1784
|
-
const events = readTokenEvents(thread.rollout_path);
|
|
1785
|
-
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
1786
|
-
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
1787
|
-
tokenEvents.forEach((event, index) => {
|
|
1788
|
-
const usage = event.usage;
|
|
1789
|
-
const inputTotal = usage.input_tokens ?? 0;
|
|
1790
|
-
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
1791
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1792
|
-
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
1793
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1794
|
-
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
1795
|
-
const requestId = `${sessionId}-${index}`;
|
|
1796
|
-
upsertRequest(db, {
|
|
1797
|
-
id: requestId,
|
|
1798
|
-
agent: "codex",
|
|
1799
|
-
session_id: sessionId,
|
|
1800
|
-
model,
|
|
1801
|
-
input_tokens: inputTokens,
|
|
1802
|
-
output_tokens: outputTokens,
|
|
1803
|
-
cache_read_tokens: cacheReadTokens,
|
|
1804
|
-
cache_create_tokens: 0,
|
|
1805
|
-
cost_usd: costUsd,
|
|
1806
|
-
cost_basis: defaultCostBasisForAgent("codex"),
|
|
1807
|
-
duration_ms: 0,
|
|
1808
|
-
timestamp,
|
|
1809
|
-
source_request_id: requestId,
|
|
1810
|
-
machine_id: machineId
|
|
1811
|
-
});
|
|
1812
|
-
requests++;
|
|
761
|
+
total_cost_usd: costUsd,
|
|
762
|
+
total_tokens: thread.tokens_used,
|
|
763
|
+
request_count: 1
|
|
1813
764
|
});
|
|
1814
|
-
|
|
1815
|
-
setIngestState(db, "codex", thread.id, stateValue);
|
|
765
|
+
setIngestState(db, "codex", stateKey, "done");
|
|
1816
766
|
ingested++;
|
|
1817
767
|
if (verbose)
|
|
1818
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens
|
|
768
|
+
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
|
|
1819
769
|
}
|
|
1820
770
|
} finally {
|
|
1821
771
|
codexDb?.close();
|
|
1822
772
|
}
|
|
1823
|
-
return { sessions: ingested
|
|
773
|
+
return { sessions: ingested };
|
|
1824
774
|
}
|
|
1825
775
|
|
|
1826
776
|
// src/ingest/gemini.ts
|
|
1827
777
|
init_database();
|
|
1828
|
-
|
|
1829
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
778
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
1830
779
|
import { homedir as homedir4 } from "os";
|
|
1831
|
-
import { join as join4
|
|
1832
|
-
var
|
|
1833
|
-
var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
|
|
1834
|
-
function geminiTmpDir() {
|
|
1835
|
-
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
1836
|
-
}
|
|
1837
|
-
function geminiHistoryDir() {
|
|
1838
|
-
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
1839
|
-
}
|
|
1840
|
-
function numberField(...values) {
|
|
1841
|
-
for (const value of values) {
|
|
1842
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
1843
|
-
return value;
|
|
1844
|
-
}
|
|
1845
|
-
return 0;
|
|
1846
|
-
}
|
|
1847
|
-
function listProjectDirs(...roots) {
|
|
1848
|
-
const dirs = new Set;
|
|
1849
|
-
for (const root of roots) {
|
|
1850
|
-
if (!existsSync4(root))
|
|
1851
|
-
continue;
|
|
1852
|
-
try {
|
|
1853
|
-
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
1854
|
-
if (entry.isDirectory())
|
|
1855
|
-
dirs.add(join4(root, entry.name));
|
|
1856
|
-
}
|
|
1857
|
-
} catch {}
|
|
1858
|
-
}
|
|
1859
|
-
return [...dirs];
|
|
1860
|
-
}
|
|
1861
|
-
function projectRoot(projectDir, chatData) {
|
|
1862
|
-
if (chatData.projectPath)
|
|
1863
|
-
return chatData.projectPath;
|
|
1864
|
-
if (chatData.project_path)
|
|
1865
|
-
return chatData.project_path;
|
|
1866
|
-
const rootFile = join4(projectDir, ".project_root");
|
|
1867
|
-
try {
|
|
1868
|
-
if (existsSync4(rootFile))
|
|
1869
|
-
return readFileSync4(rootFile, "utf-8").trim();
|
|
1870
|
-
} catch {}
|
|
1871
|
-
return "";
|
|
1872
|
-
}
|
|
780
|
+
import { join as join4 } from "path";
|
|
781
|
+
var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
1873
782
|
async function ingestGemini(db, verbose) {
|
|
1874
|
-
|
|
1875
|
-
const historyDir = geminiHistoryDir();
|
|
1876
|
-
if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
|
|
783
|
+
if (!existsSync4(GEMINI_TMP_DIR)) {
|
|
1877
784
|
if (verbose)
|
|
1878
|
-
console.log("Gemini tmp
|
|
1879
|
-
return { sessions: 0
|
|
785
|
+
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
786
|
+
return { sessions: 0 };
|
|
1880
787
|
}
|
|
1881
|
-
const machineId = getMachineId();
|
|
1882
788
|
let totalSessions = 0;
|
|
1883
|
-
let totalRequests = 0;
|
|
1884
789
|
const touchedSessions = new Set;
|
|
1885
|
-
|
|
1886
|
-
|
|
790
|
+
let projectHashDirs = [];
|
|
791
|
+
try {
|
|
792
|
+
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
|
|
793
|
+
} catch {
|
|
794
|
+
return { sessions: 0 };
|
|
795
|
+
}
|
|
796
|
+
for (const projectDir of projectHashDirs) {
|
|
1887
797
|
const chatsDir = join4(projectDir, "chats");
|
|
1888
798
|
if (!existsSync4(chatsDir))
|
|
1889
799
|
continue;
|
|
@@ -1906,1483 +816,311 @@ async function ingestGemini(db, verbose) {
|
|
|
1906
816
|
continue;
|
|
1907
817
|
let chatData;
|
|
1908
818
|
try {
|
|
1909
|
-
chatData = JSON.parse(
|
|
819
|
+
chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
1910
820
|
} catch {
|
|
1911
821
|
continue;
|
|
1912
822
|
}
|
|
1913
|
-
const sessionId = chatData.sessionId
|
|
823
|
+
const sessionId = chatData.sessionId;
|
|
1914
824
|
if (!sessionId)
|
|
1915
825
|
continue;
|
|
1916
826
|
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
1917
|
-
const projectPath = projectRoot(projectDir, chatData);
|
|
1918
|
-
const projectName = projectPath ? basename3(projectPath) : "";
|
|
1919
827
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
1920
828
|
if (!existing) {
|
|
1921
829
|
const session = {
|
|
1922
830
|
id: sessionId,
|
|
1923
831
|
agent: "gemini",
|
|
1924
|
-
project_path:
|
|
1925
|
-
project_name:
|
|
832
|
+
project_path: "",
|
|
833
|
+
project_name: "",
|
|
1926
834
|
started_at: startTime,
|
|
1927
835
|
ended_at: chatData.lastUpdated ?? null,
|
|
1928
836
|
total_cost_usd: 0,
|
|
1929
837
|
total_tokens: 0,
|
|
1930
|
-
request_count: 0
|
|
1931
|
-
machine_id: machineId
|
|
838
|
+
request_count: 0
|
|
1932
839
|
};
|
|
1933
840
|
upsertSession(db, session);
|
|
841
|
+
touchedSessions.add(sessionId);
|
|
1934
842
|
totalSessions++;
|
|
1935
843
|
}
|
|
1936
|
-
touchedSessions.add(sessionId);
|
|
1937
|
-
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
1938
|
-
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
1939
|
-
if (!usage)
|
|
1940
|
-
continue;
|
|
1941
|
-
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
1942
|
-
if (!model)
|
|
1943
|
-
continue;
|
|
1944
|
-
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
1945
|
-
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
1946
|
-
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
1947
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1948
|
-
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
1949
|
-
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
1950
|
-
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
1951
|
-
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
1952
|
-
continue;
|
|
1953
|
-
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1954
|
-
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
1955
|
-
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
1956
|
-
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
1957
|
-
upsertRequest(db, {
|
|
1958
|
-
id: requestId,
|
|
1959
|
-
agent: "gemini",
|
|
1960
|
-
session_id: sessionId,
|
|
1961
|
-
model,
|
|
1962
|
-
input_tokens: inputTokens,
|
|
1963
|
-
output_tokens: outputTokens,
|
|
1964
|
-
cache_read_tokens: cacheReadTokens,
|
|
1965
|
-
cache_create_tokens: 0,
|
|
1966
|
-
cost_usd: costUsd,
|
|
1967
|
-
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
1968
|
-
duration_ms: 0,
|
|
1969
|
-
timestamp,
|
|
1970
|
-
source_request_id: message.id ?? requestId,
|
|
1971
|
-
machine_id: machineId
|
|
1972
|
-
});
|
|
1973
|
-
totalRequests++;
|
|
1974
|
-
}
|
|
1975
844
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
1976
845
|
}
|
|
1977
846
|
}
|
|
1978
847
|
for (const sessionId of touchedSessions) {
|
|
1979
848
|
rollupSession(db, sessionId);
|
|
1980
849
|
}
|
|
1981
|
-
return { sessions: totalSessions
|
|
850
|
+
return { sessions: totalSessions };
|
|
1982
851
|
}
|
|
1983
852
|
|
|
1984
|
-
// src/
|
|
1985
|
-
init_database();
|
|
853
|
+
// src/mcp/index.ts
|
|
1986
854
|
init_pricing();
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
var
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const now = new Date().toISOString();
|
|
2021
|
-
for (const file of files) {
|
|
2022
|
-
const mtime = statSync4(file).mtimeMs;
|
|
2023
|
-
const stateKey = file;
|
|
2024
|
-
const prev = getIngestState(db, "opencode", stateKey);
|
|
2025
|
-
if (prev && Number(prev) >= mtime)
|
|
2026
|
-
continue;
|
|
2027
|
-
let parsed;
|
|
2028
|
-
try {
|
|
2029
|
-
parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
2030
|
-
} catch {
|
|
2031
|
-
continue;
|
|
2032
|
-
}
|
|
2033
|
-
if (parsed.role !== "assistant")
|
|
2034
|
-
continue;
|
|
2035
|
-
const usage = parsed.usage;
|
|
2036
|
-
if (!usage)
|
|
2037
|
-
continue;
|
|
2038
|
-
const sessionId = parseSessionIdFromPath(file) ?? `opencode-${statSync4(file).ino}`;
|
|
2039
|
-
const model = normalizeModelName(parsed.model ?? "unknown");
|
|
2040
|
-
const input = usage.inputTokens ?? 0;
|
|
2041
|
-
const output = usage.outputTokens ?? 0;
|
|
2042
|
-
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
2043
|
-
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
2044
|
-
if (input + output + cacheRead + cacheWrite === 0 && !usage.cost)
|
|
2045
|
-
continue;
|
|
2046
|
-
const timestamp = usage && parsed.time?.created ? new Date(parsed.time.created).toISOString() : new Date(statSync4(file).mtime).toISOString();
|
|
2047
|
-
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2048
|
-
const reqId = `opencode-${sourceId}`;
|
|
2049
|
-
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2050
|
-
upsertRequest(db, {
|
|
2051
|
-
id: reqId,
|
|
2052
|
-
agent: "opencode",
|
|
2053
|
-
session_id: sessionId,
|
|
2054
|
-
model,
|
|
2055
|
-
input_tokens: input,
|
|
2056
|
-
output_tokens: output,
|
|
2057
|
-
cache_read_tokens: cacheRead,
|
|
2058
|
-
cache_create_tokens: cacheWrite,
|
|
2059
|
-
cost_usd: costUsd,
|
|
2060
|
-
cost_basis: defaultCostBasisForAgent("opencode"),
|
|
2061
|
-
duration_ms: 0,
|
|
2062
|
-
timestamp,
|
|
2063
|
-
source_request_id: sourceId,
|
|
2064
|
-
machine_id: machineId,
|
|
2065
|
-
updated_at: now
|
|
2066
|
-
});
|
|
2067
|
-
requests++;
|
|
2068
|
-
if (!touched.has(sessionId)) {
|
|
2069
|
-
upsertSession(db, {
|
|
2070
|
-
id: sessionId,
|
|
2071
|
-
agent: "opencode",
|
|
2072
|
-
project_path: "",
|
|
2073
|
-
project_name: "",
|
|
2074
|
-
started_at: timestamp,
|
|
2075
|
-
ended_at: null,
|
|
2076
|
-
total_cost_usd: 0,
|
|
2077
|
-
total_tokens: 0,
|
|
2078
|
-
request_count: 0,
|
|
2079
|
-
machine_id: machineId,
|
|
2080
|
-
updated_at: now
|
|
2081
|
-
});
|
|
2082
|
-
touched.add(sessionId);
|
|
2083
|
-
}
|
|
2084
|
-
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
2085
|
-
if (verbose)
|
|
2086
|
-
console.log(` opencode: ${reqId} ${model} $${costUsd.toFixed(4)}`);
|
|
2087
|
-
}
|
|
2088
|
-
for (const sid of touched)
|
|
2089
|
-
rollupSession(db, sid);
|
|
2090
|
-
return { files: files.length, requests, sessions: touched.size };
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
// src/ingest/cursor.ts
|
|
2094
|
-
init_database();
|
|
2095
|
-
function getCursorSessionToken() {
|
|
2096
|
-
return process.env["CURSOR_SESSION_TOKEN"] ?? process.env["CURSOR_API_TOKEN"] ?? null;
|
|
2097
|
-
}
|
|
2098
|
-
async function cursorFetch(path, token) {
|
|
2099
|
-
try {
|
|
2100
|
-
const res = await fetch(`https://cursor.com${path}`, {
|
|
2101
|
-
headers: {
|
|
2102
|
-
Cookie: `WorkosCursorSessionToken=${token}`,
|
|
2103
|
-
Accept: "application/json"
|
|
2104
|
-
},
|
|
2105
|
-
signal: AbortSignal.timeout(1e4)
|
|
2106
|
-
});
|
|
2107
|
-
if (!res.ok)
|
|
2108
|
-
return null;
|
|
2109
|
-
return await res.json();
|
|
2110
|
-
} catch {
|
|
2111
|
-
return null;
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
async function ingestCursor(db, verbose = false) {
|
|
2115
|
-
const token = getCursorSessionToken();
|
|
2116
|
-
if (!token) {
|
|
2117
|
-
if (verbose)
|
|
2118
|
-
console.log(" cursor: skipped \u2014 set CURSOR_SESSION_TOKEN");
|
|
2119
|
-
return { requests: 0, snapshots: 0 };
|
|
2120
|
-
}
|
|
2121
|
-
const today = new Date().toISOString().substring(0, 10);
|
|
2122
|
-
const prev = getIngestState(db, "cursor", `sync-${today}`);
|
|
2123
|
-
if (prev)
|
|
2124
|
-
return { requests: 0, snapshots: 0 };
|
|
2125
|
-
const machineId = getMachineId();
|
|
2126
|
-
const now = new Date().toISOString();
|
|
2127
|
-
let snapshots = 0;
|
|
2128
|
-
const usage = await cursorFetch("/api/usage", token);
|
|
2129
|
-
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2130
|
-
upsertUsageSnapshot(db, {
|
|
2131
|
-
agent: "cursor",
|
|
2132
|
-
date: today,
|
|
2133
|
-
metric: "premium_requests_used",
|
|
2134
|
-
value: usage.premiumRequests,
|
|
2135
|
-
unit: "count",
|
|
2136
|
-
machine_id: machineId
|
|
2137
|
-
});
|
|
2138
|
-
upsertUsageSnapshot(db, {
|
|
2139
|
-
agent: "cursor",
|
|
2140
|
-
date: today,
|
|
2141
|
-
metric: "premium_requests_limit",
|
|
2142
|
-
value: usage.maxPremiumRequests,
|
|
2143
|
-
unit: "count",
|
|
2144
|
-
machine_id: machineId
|
|
2145
|
-
});
|
|
2146
|
-
snapshots += 2;
|
|
2147
|
-
}
|
|
2148
|
-
const summary = await cursorFetch("/api/usage-summary", token);
|
|
2149
|
-
const onDemand = summary?.individualUsage?.spend ?? summary?.teamUsage?.spend ?? 0;
|
|
2150
|
-
const included = summary?.individualUsage?.includedSpend ?? 0;
|
|
2151
|
-
if (onDemand > 0) {
|
|
2152
|
-
upsertUsageSnapshot(db, {
|
|
2153
|
-
agent: "cursor",
|
|
2154
|
-
date: today,
|
|
2155
|
-
metric: "on_demand_usd",
|
|
2156
|
-
value: onDemand,
|
|
2157
|
-
unit: "usd",
|
|
2158
|
-
machine_id: machineId
|
|
2159
|
-
});
|
|
2160
|
-
snapshots++;
|
|
2161
|
-
}
|
|
2162
|
-
if (included > 0) {
|
|
2163
|
-
upsertUsageSnapshot(db, {
|
|
2164
|
-
agent: "cursor",
|
|
2165
|
-
date: today,
|
|
2166
|
-
metric: "included_consumed_usd",
|
|
2167
|
-
value: included,
|
|
2168
|
-
unit: "usd",
|
|
2169
|
-
machine_id: machineId
|
|
2170
|
-
});
|
|
2171
|
-
snapshots++;
|
|
2172
|
-
}
|
|
2173
|
-
const sessionId = `cursor-${today}-${machineId}`;
|
|
2174
|
-
if (onDemand + included > 0) {
|
|
2175
|
-
upsertSession(db, {
|
|
2176
|
-
id: sessionId,
|
|
2177
|
-
agent: "cursor",
|
|
2178
|
-
project_path: "",
|
|
2179
|
-
project_name: "Cursor subscription",
|
|
2180
|
-
started_at: `${today}T00:00:00.000Z`,
|
|
2181
|
-
ended_at: now,
|
|
2182
|
-
total_cost_usd: onDemand + included,
|
|
2183
|
-
total_tokens: 0,
|
|
2184
|
-
request_count: 1,
|
|
2185
|
-
machine_id: machineId,
|
|
2186
|
-
updated_at: now
|
|
2187
|
-
});
|
|
2188
|
-
upsertRequest(db, {
|
|
2189
|
-
id: `cursor-${today}-${machineId}-usage`,
|
|
2190
|
-
agent: "cursor",
|
|
2191
|
-
session_id: sessionId,
|
|
2192
|
-
model: "cursor-subscription",
|
|
2193
|
-
input_tokens: 0,
|
|
2194
|
-
output_tokens: 0,
|
|
2195
|
-
cache_read_tokens: 0,
|
|
2196
|
-
cache_create_tokens: 0,
|
|
2197
|
-
cost_usd: onDemand + included,
|
|
2198
|
-
cost_basis: "subscription_included",
|
|
2199
|
-
duration_ms: 0,
|
|
2200
|
-
timestamp: now,
|
|
2201
|
-
source_request_id: today,
|
|
2202
|
-
machine_id: machineId,
|
|
2203
|
-
updated_at: now
|
|
2204
|
-
});
|
|
2205
|
-
rollupSession(db, sessionId);
|
|
2206
|
-
}
|
|
2207
|
-
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
2208
|
-
if (verbose)
|
|
2209
|
-
console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
|
|
2210
|
-
return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
// src/ingest/pi.ts
|
|
2214
|
-
init_database();
|
|
2215
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
2216
|
-
import { homedir as homedir6 } from "os";
|
|
2217
|
-
import { join as join6 } from "path";
|
|
2218
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
|
|
2219
|
-
function walkSessions(dir, acc = []) {
|
|
2220
|
-
if (!existsSync6(dir))
|
|
2221
|
-
return acc;
|
|
2222
|
-
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2223
|
-
const p = join6(dir, entry.name);
|
|
2224
|
-
if (entry.isDirectory())
|
|
2225
|
-
walkSessions(p, acc);
|
|
2226
|
-
else if (entry.name.endsWith(".json"))
|
|
2227
|
-
acc.push(p);
|
|
2228
|
-
}
|
|
2229
|
-
return acc;
|
|
2230
|
-
}
|
|
2231
|
-
async function ingestPi(db, verbose = false) {
|
|
2232
|
-
const files = walkSessions(PI_SESSION_DIR);
|
|
2233
|
-
let requests = 0;
|
|
2234
|
-
const touched = new Set;
|
|
2235
|
-
const machineId = getMachineId();
|
|
2236
|
-
const now = new Date().toISOString();
|
|
2237
|
-
for (const file of files) {
|
|
2238
|
-
const mtime = statSync5(file).mtimeMs;
|
|
2239
|
-
const prev = getIngestState(db, "pi", file);
|
|
2240
|
-
if (prev && Number(prev) >= mtime)
|
|
2241
|
-
continue;
|
|
2242
|
-
let data;
|
|
2243
|
-
try {
|
|
2244
|
-
data = JSON.parse(readFileSync6(file, "utf-8"));
|
|
2245
|
-
} catch {
|
|
2246
|
-
continue;
|
|
2247
|
-
}
|
|
2248
|
-
const sessionId = data.id ?? file.replace(/\.json$/, "").split("/").pop() ?? `pi-${statSync5(file).ino}`;
|
|
2249
|
-
const turns = data.turns ?? data.messages?.filter((m) => m.role === "assistant") ?? [];
|
|
2250
|
-
for (let i = 0;i < turns.length; i++) {
|
|
2251
|
-
const turn = turns[i];
|
|
2252
|
-
const usage = turn.usage;
|
|
2253
|
-
if (!usage)
|
|
2254
|
-
continue;
|
|
2255
|
-
const input = usage.input ?? 0;
|
|
2256
|
-
const output = usage.output ?? 0;
|
|
2257
|
-
if (input + output === 0 && !usage.cost)
|
|
2258
|
-
continue;
|
|
2259
|
-
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2260
|
-
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2261
|
-
const reqId = `pi-${sessionId}-${i}`;
|
|
2262
|
-
upsertRequest(db, {
|
|
2263
|
-
id: reqId,
|
|
2264
|
-
agent: "pi",
|
|
2265
|
-
session_id: sessionId,
|
|
2266
|
-
model,
|
|
2267
|
-
input_tokens: input,
|
|
2268
|
-
output_tokens: output,
|
|
2269
|
-
cache_read_tokens: usage.cacheRead ?? 0,
|
|
2270
|
-
cache_create_tokens: usage.cacheWrite ?? 0,
|
|
2271
|
-
cost_usd: usage.cost ?? 0,
|
|
2272
|
-
cost_basis: defaultCostBasisForAgent("pi"),
|
|
2273
|
-
duration_ms: 0,
|
|
2274
|
-
timestamp,
|
|
2275
|
-
source_request_id: `${sessionId}-${i}`,
|
|
2276
|
-
machine_id: machineId,
|
|
2277
|
-
updated_at: now
|
|
2278
|
-
});
|
|
2279
|
-
requests++;
|
|
2280
|
-
}
|
|
2281
|
-
if (turns.length > 0) {
|
|
2282
|
-
upsertSession(db, {
|
|
2283
|
-
id: sessionId,
|
|
2284
|
-
agent: "pi",
|
|
2285
|
-
project_path: "",
|
|
2286
|
-
project_name: "",
|
|
2287
|
-
started_at: turns[0]?.timestamp ?? now,
|
|
2288
|
-
ended_at: null,
|
|
2289
|
-
total_cost_usd: 0,
|
|
2290
|
-
total_tokens: 0,
|
|
2291
|
-
request_count: 0,
|
|
2292
|
-
machine_id: machineId,
|
|
2293
|
-
updated_at: now
|
|
2294
|
-
});
|
|
2295
|
-
touched.add(sessionId);
|
|
2296
|
-
}
|
|
2297
|
-
setIngestState(db, "pi", file, String(mtime));
|
|
2298
|
-
if (verbose)
|
|
2299
|
-
console.log(` pi: ${sessionId} (${turns.length} turns)`);
|
|
2300
|
-
}
|
|
2301
|
-
for (const sid of touched)
|
|
2302
|
-
rollupSession(db, sid);
|
|
2303
|
-
return { files: files.length, requests, sessions: touched.size };
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
// src/ingest/hermes.ts
|
|
2307
|
-
init_database();
|
|
2308
|
-
import { existsSync as existsSync7, statSync as statSync6 } from "fs";
|
|
2309
|
-
import { homedir as homedir7 } from "os";
|
|
2310
|
-
import { join as join7 } from "path";
|
|
2311
|
-
var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
|
|
2312
|
-
function mapCostBasis(billingMode) {
|
|
2313
|
-
if (billingMode === "subscription")
|
|
2314
|
-
return "subscription_included";
|
|
2315
|
-
if (billingMode === "api")
|
|
2316
|
-
return "metered_api";
|
|
2317
|
-
return defaultCostBasisForAgent("hermes");
|
|
2318
|
-
}
|
|
2319
|
-
async function ingestHermes(db, verbose = false) {
|
|
2320
|
-
if (!existsSync7(HERMES_DB)) {
|
|
2321
|
-
return { sessions: 0, requests: 0 };
|
|
2322
|
-
}
|
|
2323
|
-
const { Database: Sqlite } = await import("bun:sqlite");
|
|
2324
|
-
const hermes = new Sqlite(HERMES_DB, { readonly: true });
|
|
2325
|
-
const rows = hermes.prepare(`
|
|
2326
|
-
SELECT id, source, model, started_at, ended_at,
|
|
2327
|
-
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
2328
|
-
reasoning_tokens, estimated_cost_usd, actual_cost_usd, billing_mode, parent_session_id
|
|
2329
|
-
FROM sessions
|
|
2330
|
-
ORDER BY started_at DESC
|
|
2331
|
-
`).all();
|
|
2332
|
-
const stateKey = "state.db";
|
|
2333
|
-
const mtime = statSyncSafe(HERMES_DB);
|
|
2334
|
-
const prev = getIngestState(db, "hermes", stateKey);
|
|
2335
|
-
if (prev && Number(prev) >= mtime && rows.length === 0) {
|
|
2336
|
-
hermes.close();
|
|
2337
|
-
return { sessions: 0, requests: 0 };
|
|
2338
|
-
}
|
|
2339
|
-
const machineId = getMachineId();
|
|
2340
|
-
const now = new Date().toISOString();
|
|
2341
|
-
let requests = 0;
|
|
2342
|
-
for (const row of rows) {
|
|
2343
|
-
const sessionId = `hermes-${row.id}`;
|
|
2344
|
-
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2345
|
-
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2346
|
-
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2347
|
-
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2348
|
-
upsertSession(db, {
|
|
2349
|
-
id: sessionId,
|
|
2350
|
-
agent: "hermes",
|
|
2351
|
-
project_path: row.source ?? "",
|
|
2352
|
-
project_name: row.source ?? "",
|
|
2353
|
-
started_at: startedAt,
|
|
2354
|
-
ended_at: endedAt,
|
|
2355
|
-
total_cost_usd: cost,
|
|
2356
|
-
total_tokens: tokens,
|
|
2357
|
-
request_count: 1,
|
|
2358
|
-
machine_id: machineId,
|
|
2359
|
-
updated_at: now
|
|
2360
|
-
});
|
|
2361
|
-
const reqId = `hermes-${row.id}-rollup`;
|
|
2362
|
-
upsertRequest(db, {
|
|
2363
|
-
id: reqId,
|
|
2364
|
-
agent: "hermes",
|
|
2365
|
-
session_id: sessionId,
|
|
2366
|
-
model: row.model ?? "unknown",
|
|
2367
|
-
input_tokens: row.input_tokens,
|
|
2368
|
-
output_tokens: row.output_tokens + row.reasoning_tokens,
|
|
2369
|
-
cache_read_tokens: row.cache_read_tokens,
|
|
2370
|
-
cache_create_tokens: row.cache_write_tokens,
|
|
2371
|
-
cost_usd: cost,
|
|
2372
|
-
cost_basis: mapCostBasis(row.billing_mode),
|
|
2373
|
-
duration_ms: 0,
|
|
2374
|
-
timestamp: endedAt ?? startedAt,
|
|
2375
|
-
source_request_id: row.id,
|
|
2376
|
-
machine_id: machineId,
|
|
2377
|
-
updated_at: now
|
|
2378
|
-
});
|
|
2379
|
-
requests++;
|
|
2380
|
-
rollupSession(db, sessionId);
|
|
2381
|
-
if (verbose)
|
|
2382
|
-
console.log(` hermes: ${sessionId} $${cost.toFixed(4)}`);
|
|
2383
|
-
}
|
|
2384
|
-
setIngestState(db, "hermes", stateKey, String(mtime));
|
|
2385
|
-
hermes.close();
|
|
2386
|
-
return { sessions: rows.length, requests };
|
|
2387
|
-
}
|
|
2388
|
-
function statSyncSafe(path) {
|
|
2389
|
-
try {
|
|
2390
|
-
return statSync6(path).mtimeMs;
|
|
2391
|
-
} catch {
|
|
2392
|
-
return 0;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
// src/ingest/claude-quota.ts
|
|
2397
|
-
init_database();
|
|
2398
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
2399
|
-
|
|
2400
|
-
// src/lib/paths.ts
|
|
2401
|
-
import { homedir as homedir8 } from "os";
|
|
2402
|
-
import { join as join8 } from "path";
|
|
2403
|
-
function getHomeDir() {
|
|
2404
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
|
|
2405
|
-
}
|
|
2406
|
-
function agentPaths() {
|
|
2407
|
-
const home = getHomeDir();
|
|
2408
|
-
return {
|
|
2409
|
-
claudeProjects: join8(home, ".claude", "projects"),
|
|
2410
|
-
claudeCredentials: join8(home, ".claude", ".credentials.json"),
|
|
2411
|
-
takumiProjects: join8(home, ".takumi", "projects"),
|
|
2412
|
-
codexDir: join8(home, ".codex"),
|
|
2413
|
-
codexDb: join8(home, ".codex", "state_5.sqlite"),
|
|
2414
|
-
codexAuth: join8(home, ".codex", "auth.json"),
|
|
2415
|
-
codexConfig: join8(home, ".codex", "config.toml"),
|
|
2416
|
-
geminiTmp: join8(home, ".gemini", "tmp"),
|
|
2417
|
-
geminiHistory: join8(home, ".gemini", "history"),
|
|
2418
|
-
opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
|
|
2419
|
-
piSessions: join8(home, ".pi", "agent", "sessions"),
|
|
2420
|
-
hermesDir: join8(home, ".hermes"),
|
|
2421
|
-
hermesDb: join8(home, ".hermes", "state.db")
|
|
2422
|
-
};
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// src/ingest/claude-quota.ts
|
|
2426
|
-
var CREDENTIALS_PATH = agentPaths().claudeCredentials;
|
|
2427
|
-
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
2428
|
-
var OAUTH_BETA = "oauth-2025-04-20";
|
|
2429
|
-
function readClaudeToken() {
|
|
2430
|
-
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
2431
|
-
if (fromEnv)
|
|
2432
|
-
return { token: fromEnv };
|
|
2433
|
-
if (!existsSync8(CREDENTIALS_PATH))
|
|
2434
|
-
return null;
|
|
2435
|
-
try {
|
|
2436
|
-
const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
|
|
2437
|
-
const oauth = creds.claudeAiOauth;
|
|
2438
|
-
if (!oauth?.accessToken)
|
|
2439
|
-
return null;
|
|
2440
|
-
return {
|
|
2441
|
-
token: oauth.accessToken,
|
|
2442
|
-
subscriptionType: oauth.subscriptionType,
|
|
2443
|
-
rateLimitTier: oauth.rateLimitTier
|
|
2444
|
-
};
|
|
2445
|
-
} catch {
|
|
2446
|
-
return null;
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
function inferMonthlyFee(subscriptionType, rateLimitTier) {
|
|
2450
|
-
const tier = `${subscriptionType ?? ""} ${rateLimitTier ?? ""}`.toLowerCase();
|
|
2451
|
-
if (tier.includes("max") && tier.includes("20"))
|
|
2452
|
-
return 200;
|
|
2453
|
-
if (tier.includes("max"))
|
|
2454
|
-
return 100;
|
|
2455
|
-
if (tier.includes("pro"))
|
|
2456
|
-
return 20;
|
|
2457
|
-
if (tier.includes("team"))
|
|
2458
|
-
return 30;
|
|
2459
|
-
return 20;
|
|
2460
|
-
}
|
|
2461
|
-
async function fetchClaudeOAuthUsage(token) {
|
|
2462
|
-
try {
|
|
2463
|
-
const res = await fetch(USAGE_URL, {
|
|
2464
|
-
headers: {
|
|
2465
|
-
Authorization: `Bearer ${token}`,
|
|
2466
|
-
"anthropic-beta": OAUTH_BETA,
|
|
2467
|
-
Accept: "application/json"
|
|
2468
|
-
},
|
|
2469
|
-
signal: AbortSignal.timeout(1e4)
|
|
2470
|
-
});
|
|
2471
|
-
if (!res.ok)
|
|
2472
|
-
return null;
|
|
2473
|
-
return await res.json();
|
|
2474
|
-
} catch {
|
|
2475
|
-
return null;
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
async function ingestClaudeQuota(db, verbose = false) {
|
|
2479
|
-
const auth = readClaudeToken();
|
|
2480
|
-
if (!auth) {
|
|
2481
|
-
if (verbose)
|
|
2482
|
-
console.log(" claude quota: skipped \u2014 no OAuth token (~/.claude/.credentials.json or CLAUDE_OAUTH_TOKEN)");
|
|
2483
|
-
return { snapshots: 0 };
|
|
2484
|
-
}
|
|
2485
|
-
const today = new Date().toISOString().substring(0, 10);
|
|
2486
|
-
const prev = getIngestState(db, "claude", `quota-${today}`);
|
|
2487
|
-
if (prev)
|
|
2488
|
-
return { snapshots: 0 };
|
|
2489
|
-
const usage = await fetchClaudeOAuthUsage(auth.token);
|
|
2490
|
-
if (!usage) {
|
|
2491
|
-
if (verbose)
|
|
2492
|
-
console.log(" claude quota: OAuth usage endpoint unavailable");
|
|
2493
|
-
return { snapshots: 0 };
|
|
2494
|
-
}
|
|
2495
|
-
const machineId = getMachineId();
|
|
2496
|
-
let snapshots = 0;
|
|
2497
|
-
const windows = [
|
|
2498
|
-
["five_hour_utilization", usage.five_hour],
|
|
2499
|
-
["seven_day_utilization", usage.seven_day],
|
|
2500
|
-
["seven_day_sonnet_utilization", usage.seven_day_sonnet],
|
|
2501
|
-
["seven_day_opus_utilization", usage.seven_day_opus]
|
|
2502
|
-
];
|
|
2503
|
-
for (const [metric, window] of windows) {
|
|
2504
|
-
if (window?.utilization == null)
|
|
2505
|
-
continue;
|
|
2506
|
-
upsertUsageSnapshot(db, {
|
|
2507
|
-
agent: "claude",
|
|
2508
|
-
date: today,
|
|
2509
|
-
metric,
|
|
2510
|
-
value: Math.round(window.utilization * 1000) / 10,
|
|
2511
|
-
unit: "percent",
|
|
2512
|
-
machine_id: machineId
|
|
2513
|
-
});
|
|
2514
|
-
snapshots++;
|
|
2515
|
-
if (window.resets_at) {
|
|
2516
|
-
upsertUsageSnapshot(db, {
|
|
2517
|
-
agent: "claude",
|
|
2518
|
-
date: today,
|
|
2519
|
-
metric: `${metric}_resets_at`,
|
|
2520
|
-
value: Date.parse(window.resets_at),
|
|
2521
|
-
unit: "epoch_ms",
|
|
2522
|
-
machine_id: machineId
|
|
2523
|
-
});
|
|
2524
|
-
snapshots++;
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
if (usage.extra_usage?.spend != null) {
|
|
2528
|
-
upsertUsageSnapshot(db, {
|
|
2529
|
-
agent: "claude",
|
|
2530
|
-
date: today,
|
|
2531
|
-
metric: "on_demand_usd",
|
|
2532
|
-
value: usage.extra_usage.spend,
|
|
2533
|
-
unit: "usd",
|
|
2534
|
-
machine_id: machineId
|
|
2535
|
-
});
|
|
2536
|
-
snapshots++;
|
|
2537
|
-
}
|
|
2538
|
-
if (usage.extra_usage?.limit != null) {
|
|
2539
|
-
upsertUsageSnapshot(db, {
|
|
2540
|
-
agent: "claude",
|
|
2541
|
-
date: today,
|
|
2542
|
-
metric: "on_demand_limit_usd",
|
|
2543
|
-
value: usage.extra_usage.limit,
|
|
2544
|
-
unit: "usd",
|
|
2545
|
-
machine_id: machineId
|
|
2546
|
-
});
|
|
2547
|
-
snapshots++;
|
|
2548
|
-
}
|
|
2549
|
-
const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
|
|
2550
|
-
const now = new Date().toISOString();
|
|
2551
|
-
upsertSubscription(db, {
|
|
2552
|
-
id: "anthropic-claude-oauth",
|
|
2553
|
-
provider: "anthropic",
|
|
2554
|
-
agent: "claude",
|
|
2555
|
-
plan: auth.rateLimitTier ?? auth.subscriptionType ?? "claude_pro",
|
|
2556
|
-
monthly_fee_usd: monthlyFee,
|
|
2557
|
-
included_usage_usd: monthlyFee,
|
|
2558
|
-
billing_cycle_start: null,
|
|
2559
|
-
reset_policy: "monthly",
|
|
2560
|
-
active: 1,
|
|
2561
|
-
created_at: now,
|
|
2562
|
-
updated_at: now
|
|
2563
|
-
});
|
|
2564
|
-
setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
|
|
2565
|
-
if (verbose)
|
|
2566
|
-
console.log(` claude quota: ${snapshots} snapshots`);
|
|
2567
|
-
return { snapshots };
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
// src/ingest/codex-quota.ts
|
|
2571
|
-
init_database();
|
|
2572
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
2573
|
-
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2574
|
-
function readCodexAuth() {
|
|
2575
|
-
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
2576
|
-
if (fromEnv)
|
|
2577
|
-
return { token: fromEnv, authMode: "chatgpt" };
|
|
2578
|
-
const authPath = agentPaths().codexAuth;
|
|
2579
|
-
if (!existsSync9(authPath))
|
|
2580
|
-
return null;
|
|
2581
|
-
try {
|
|
2582
|
-
const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
|
|
2583
|
-
const token = auth.tokens?.access_token;
|
|
2584
|
-
if (!token)
|
|
2585
|
-
return null;
|
|
2586
|
-
return {
|
|
2587
|
-
token,
|
|
2588
|
-
accountId: auth.tokens?.account_id,
|
|
2589
|
-
authMode: auth.auth_mode ?? "chatgpt"
|
|
2590
|
-
};
|
|
2591
|
-
} catch {
|
|
2592
|
-
return null;
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
function planMonthlyFee(planType) {
|
|
2596
|
-
const plan = (planType ?? "").toLowerCase();
|
|
2597
|
-
if (plan.includes("pro"))
|
|
2598
|
-
return 200;
|
|
2599
|
-
if (plan.includes("plus"))
|
|
2600
|
-
return 20;
|
|
2601
|
-
if (plan.includes("team"))
|
|
2602
|
-
return 30;
|
|
2603
|
-
return 20;
|
|
2604
|
-
}
|
|
2605
|
-
async function fetchCodexUsage(token, accountId) {
|
|
2606
|
-
try {
|
|
2607
|
-
const headers = {
|
|
2608
|
-
Authorization: `Bearer ${token}`,
|
|
2609
|
-
Accept: "application/json",
|
|
2610
|
-
"User-Agent": "economy-cli"
|
|
2611
|
-
};
|
|
2612
|
-
if (accountId)
|
|
2613
|
-
headers["ChatGPT-Account-Id"] = accountId;
|
|
2614
|
-
const res = await fetch(WHAM_USAGE_URL, {
|
|
2615
|
-
headers,
|
|
2616
|
-
signal: AbortSignal.timeout(1e4)
|
|
2617
|
-
});
|
|
2618
|
-
if (!res.ok)
|
|
2619
|
-
return null;
|
|
2620
|
-
return await res.json();
|
|
2621
|
-
} catch {
|
|
2622
|
-
return null;
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
async function ingestCodexQuota(db, verbose = false) {
|
|
2626
|
-
const auth = readCodexAuth();
|
|
2627
|
-
if (!auth) {
|
|
2628
|
-
if (verbose)
|
|
2629
|
-
console.log(" codex quota: skipped \u2014 no ~/.codex/auth.json or CODEX_OAUTH_TOKEN");
|
|
2630
|
-
return { snapshots: 0 };
|
|
2631
|
-
}
|
|
2632
|
-
if (auth.authMode === "api_key" || auth.authMode === "api") {
|
|
2633
|
-
if (verbose)
|
|
2634
|
-
console.log(" codex quota: skipped \u2014 API key mode (no subscription quota)");
|
|
2635
|
-
return { snapshots: 0 };
|
|
2636
|
-
}
|
|
2637
|
-
const today = new Date().toISOString().substring(0, 10);
|
|
2638
|
-
const prev = getIngestState(db, "codex", `quota-${today}`);
|
|
2639
|
-
if (prev)
|
|
2640
|
-
return { snapshots: 0 };
|
|
2641
|
-
const usage = await fetchCodexUsage(auth.token, auth.accountId);
|
|
2642
|
-
if (!usage) {
|
|
2643
|
-
if (verbose)
|
|
2644
|
-
console.log(" codex quota: wham/usage endpoint unavailable");
|
|
2645
|
-
return { snapshots: 0 };
|
|
2646
|
-
}
|
|
2647
|
-
const machineId = getMachineId();
|
|
2648
|
-
let snapshots = 0;
|
|
2649
|
-
const now = new Date().toISOString();
|
|
2650
|
-
const windows = [
|
|
2651
|
-
["five_hour_utilization", usage.rate_limit?.primary_window],
|
|
2652
|
-
["seven_day_utilization", usage.rate_limit?.secondary_window]
|
|
2653
|
-
];
|
|
2654
|
-
for (const [metric, window] of windows) {
|
|
2655
|
-
if (window?.used_percent == null)
|
|
2656
|
-
continue;
|
|
2657
|
-
upsertUsageSnapshot(db, {
|
|
2658
|
-
agent: "codex",
|
|
2659
|
-
date: today,
|
|
2660
|
-
metric,
|
|
2661
|
-
value: window.used_percent,
|
|
2662
|
-
unit: "percent",
|
|
2663
|
-
machine_id: machineId
|
|
2664
|
-
});
|
|
2665
|
-
snapshots++;
|
|
2666
|
-
if (window.reset_at) {
|
|
2667
|
-
upsertUsageSnapshot(db, {
|
|
2668
|
-
agent: "codex",
|
|
2669
|
-
date: today,
|
|
2670
|
-
metric: `${metric}_resets_at`,
|
|
2671
|
-
value: window.reset_at * 1000,
|
|
2672
|
-
unit: "epoch_ms",
|
|
2673
|
-
machine_id: machineId
|
|
2674
|
-
});
|
|
2675
|
-
snapshots++;
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
if (usage.credits?.balance != null) {
|
|
2679
|
-
upsertUsageSnapshot(db, {
|
|
2680
|
-
agent: "codex",
|
|
2681
|
-
date: today,
|
|
2682
|
-
metric: "credits_balance_usd",
|
|
2683
|
-
value: usage.credits.balance,
|
|
2684
|
-
unit: "usd",
|
|
2685
|
-
machine_id: machineId
|
|
2686
|
-
});
|
|
2687
|
-
snapshots++;
|
|
2688
|
-
}
|
|
2689
|
-
const monthlyFee = planMonthlyFee(usage.plan_type);
|
|
2690
|
-
upsertSubscription(db, {
|
|
2691
|
-
id: "openai-codex-oauth",
|
|
2692
|
-
provider: "openai",
|
|
2693
|
-
agent: "codex",
|
|
2694
|
-
plan: usage.plan_type ?? "chatgpt_plus",
|
|
2695
|
-
monthly_fee_usd: monthlyFee,
|
|
2696
|
-
included_usage_usd: monthlyFee,
|
|
2697
|
-
billing_cycle_start: null,
|
|
2698
|
-
reset_policy: "monthly",
|
|
2699
|
-
active: 1,
|
|
2700
|
-
created_at: now,
|
|
2701
|
-
updated_at: now
|
|
2702
|
-
});
|
|
2703
|
-
setIngestState(db, "codex", `quota-${today}`, now);
|
|
2704
|
-
if (verbose)
|
|
2705
|
-
console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
|
|
2706
|
-
return { snapshots };
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
// src/lib/sync-all.ts
|
|
2710
|
-
init_database();
|
|
2711
|
-
|
|
2712
|
-
// src/lib/cloud-sync.ts
|
|
2713
|
-
init_database();
|
|
2714
|
-
var CLOUD_TABLES = [
|
|
2715
|
-
"requests",
|
|
2716
|
-
"sessions",
|
|
2717
|
-
"projects",
|
|
2718
|
-
"budgets",
|
|
2719
|
-
"goals",
|
|
2720
|
-
"model_pricing",
|
|
2721
|
-
"billing_daily",
|
|
2722
|
-
"subscriptions",
|
|
2723
|
-
"usage_snapshots",
|
|
2724
|
-
"savings_daily",
|
|
2725
|
-
"machines",
|
|
2726
|
-
"ingest_state"
|
|
855
|
+
var db = openDatabase();
|
|
856
|
+
ensurePricingSeeded(db);
|
|
857
|
+
var server = new Server({ name: "economy", version: "0.2.2" }, { capabilities: { tools: {} } });
|
|
858
|
+
var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
859
|
+
var fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
860
|
+
function fmtSession(s) {
|
|
861
|
+
const id = String(s["id"] ?? "").slice(0, 8);
|
|
862
|
+
const agent = String(s["agent"] ?? "");
|
|
863
|
+
const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
|
|
864
|
+
const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
|
|
865
|
+
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
866
|
+
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
867
|
+
}
|
|
868
|
+
var TOOLS = [
|
|
869
|
+
{ name: "get_cost_summary", description: "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["today", "week", "month", "year", "all"] } } } },
|
|
870
|
+
{ name: "get_sessions", description: "List sessions. Returns compact table. Params: agent, project, limit(20)", inputSchema: { type: "object", properties: { agent: { type: "string" }, project: { type: "string" }, limit: { type: "number" } } } },
|
|
871
|
+
{ name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
|
|
872
|
+
{ name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
|
|
873
|
+
{ name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
|
|
874
|
+
{ name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
|
|
875
|
+
{ name: "get_daily", description: "Daily cost table by agent. Params: days(30)", inputSchema: { type: "object", properties: { days: { type: "number" } } } },
|
|
876
|
+
{ name: "get_session_detail", description: "Per-request breakdown of a single session. Params: session_id (prefix ok)", inputSchema: { type: "object", properties: { session_id: { type: "string" } }, required: ["session_id"] } },
|
|
877
|
+
{ name: "sync", description: "Ingest new cost data. sources: all|claude|codex|gemini", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex", "gemini"] } } } },
|
|
878
|
+
{ name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
|
|
879
|
+
{ name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } },
|
|
880
|
+
{ name: "get_goals", description: "All spending goals with current progress. No params.", inputSchema: { type: "object", properties: {} } },
|
|
881
|
+
{ name: "set_goal", description: "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", inputSchema: { type: "object", properties: { period: { type: "string" }, limit_usd: { type: "number" }, project_path: { type: "string" }, agent: { type: "string" } }, required: ["period", "limit_usd"] } },
|
|
882
|
+
{ name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
883
|
+
{ name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
|
|
884
|
+
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
885
|
+
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
|
|
886
|
+
{ name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
|
|
887
|
+
{ name: "send_feedback", description: "Send feedback about this service.", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
|
|
2727
888
|
];
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
}
|
|
2746
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
2747
|
-
return new PgAdapterAsync(url);
|
|
2748
|
-
}
|
|
2749
|
-
async function runCloudMigrations(cloud) {
|
|
2750
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2751
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
2752
|
-
await cloud.run(sql);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
function isCloudIncrementalEnabled() {
|
|
2756
|
-
return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
|
|
2757
|
-
}
|
|
2758
|
-
async function cloudPush(opts) {
|
|
2759
|
-
const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
|
|
2760
|
-
const cloud = await getCloudPg();
|
|
2761
|
-
const local = new SqliteAdapter(getDbPath());
|
|
2762
|
-
await runCloudMigrations(cloud);
|
|
2763
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
2764
|
-
let rows = 0;
|
|
2765
|
-
if (isCloudIncrementalEnabled()) {
|
|
2766
|
-
ensureSyncMetaTable(local);
|
|
2767
|
-
const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
|
|
2768
|
-
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
2769
|
-
} else {
|
|
2770
|
-
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
2771
|
-
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2772
|
-
}
|
|
2773
|
-
touchMachineRegistry(local, "push");
|
|
2774
|
-
local.close();
|
|
2775
|
-
await cloud.close();
|
|
2776
|
-
return { rows, machine: getMachineId() };
|
|
2777
|
-
}
|
|
2778
|
-
async function cloudPull(opts) {
|
|
2779
|
-
const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
|
|
2780
|
-
const cloud = await getCloudPg();
|
|
2781
|
-
const local = new SqliteAdapter(getDbPath());
|
|
2782
|
-
await runCloudMigrations(cloud);
|
|
2783
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
2784
|
-
let rows = 0;
|
|
2785
|
-
if (isCloudIncrementalEnabled()) {
|
|
2786
|
-
ensureSyncMetaTable(local);
|
|
2787
|
-
const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
|
|
2788
|
-
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
2789
|
-
} else {
|
|
2790
|
-
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
2791
|
-
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2792
|
-
}
|
|
2793
|
-
touchMachineRegistry(local, "pull");
|
|
2794
|
-
local.close();
|
|
2795
|
-
await cloud.close();
|
|
2796
|
-
setLastCloudPull();
|
|
2797
|
-
return { rows, machine: getMachineId() };
|
|
2798
|
-
}
|
|
2799
|
-
function setLastCloudPull(at = new Date().toISOString()) {
|
|
2800
|
-
const db = openDatabase();
|
|
2801
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
2802
|
-
}
|
|
2803
|
-
function getLastCloudPull() {
|
|
2804
|
-
const db = openDatabase();
|
|
2805
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
2806
|
-
return row?.value ?? null;
|
|
2807
|
-
}
|
|
2808
|
-
function shouldPullFromCloud() {
|
|
2809
|
-
if (!getCloudDatabaseUrl())
|
|
2810
|
-
return false;
|
|
2811
|
-
const last = getLastCloudPull();
|
|
2812
|
-
if (!last)
|
|
2813
|
-
return true;
|
|
2814
|
-
const ageMs = Date.now() - new Date(last).getTime();
|
|
2815
|
-
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
2816
|
-
}
|
|
2817
|
-
async function maybePullFromCloud() {
|
|
2818
|
-
if (!shouldPullFromCloud())
|
|
2819
|
-
return false;
|
|
2820
|
-
try {
|
|
2821
|
-
await cloudPull();
|
|
2822
|
-
return true;
|
|
2823
|
-
} catch {
|
|
2824
|
-
return false;
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
async function maybePushAfterIngest() {
|
|
2828
|
-
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
2829
|
-
return false;
|
|
889
|
+
var TOOL_DESCRIPTIONS = {
|
|
890
|
+
get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
|
|
891
|
+
get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
|
|
892
|
+
get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
|
|
893
|
+
get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
|
|
894
|
+
get_project_breakdown: "no params \u2192 project_name, sessions, cost",
|
|
895
|
+
get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
|
|
896
|
+
get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
|
|
897
|
+
get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
|
|
898
|
+
sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
|
|
899
|
+
get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
|
|
900
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
|
|
901
|
+
remove_goal: "id \u2192 deletes goal"
|
|
902
|
+
};
|
|
903
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
904
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
905
|
+
const { name, arguments: args } = req.params;
|
|
906
|
+
const a = args ?? {};
|
|
2830
907
|
try {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
2842
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2843
|
-
ON CONFLICT(machine_id) DO UPDATE SET
|
|
2844
|
-
hostname = excluded.hostname,
|
|
2845
|
-
last_seen_at = excluded.last_seen_at,
|
|
2846
|
-
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
2847
|
-
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
2848
|
-
economy_version = excluded.economy_version,
|
|
2849
|
-
updated_at = excluded.updated_at
|
|
2850
|
-
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
// src/lib/sync-all.ts
|
|
2854
|
-
async function syncAll(db, opts = {}) {
|
|
2855
|
-
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
2856
|
-
const all = !anySpecific;
|
|
2857
|
-
await maybePullFromCloud();
|
|
2858
|
-
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
2859
|
-
if (all || opts.claude) {
|
|
2860
|
-
result.claude = await ingestClaude(db, opts.verbose);
|
|
2861
|
-
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
2862
|
-
}
|
|
2863
|
-
if (all || opts.takumi)
|
|
2864
|
-
result.takumi = await ingestTakumi(db, opts.verbose);
|
|
2865
|
-
if (all || opts.codex) {
|
|
2866
|
-
result.codex = await ingestCodex(db, opts.verbose);
|
|
2867
|
-
result.codexQuota = await ingestCodexQuota(db, opts.verbose);
|
|
2868
|
-
}
|
|
2869
|
-
if (all || opts.gemini)
|
|
2870
|
-
result.gemini = await ingestGemini(db, opts.verbose);
|
|
2871
|
-
if (all || opts.opencode)
|
|
2872
|
-
result.opencode = await ingestOpenCode(db, opts.verbose);
|
|
2873
|
-
if (all || opts.cursor)
|
|
2874
|
-
result.cursor = await ingestCursor(db, opts.verbose);
|
|
2875
|
-
if (all || opts.pi)
|
|
2876
|
-
result.pi = await ingestPi(db, opts.verbose);
|
|
2877
|
-
if (all || opts.hermes)
|
|
2878
|
-
result.hermes = await ingestHermes(db, opts.verbose);
|
|
2879
|
-
result.deduped = dedupeRequests(db);
|
|
2880
|
-
result.cloudPushed = await maybePushAfterIngest();
|
|
2881
|
-
return result;
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
// src/lib/agents.ts
|
|
2885
|
-
var AGENTS = [
|
|
2886
|
-
"claude",
|
|
2887
|
-
"takumi",
|
|
2888
|
-
"codex",
|
|
2889
|
-
"gemini",
|
|
2890
|
-
"opencode",
|
|
2891
|
-
"cursor",
|
|
2892
|
-
"pi",
|
|
2893
|
-
"hermes"
|
|
2894
|
-
];
|
|
2895
|
-
|
|
2896
|
-
// src/mcp/server.ts
|
|
2897
|
-
init_database();
|
|
2898
|
-
init_pricing();
|
|
2899
|
-
init_pricing();
|
|
2900
|
-
var MCP_NAME = "economy";
|
|
2901
|
-
var DEFAULT_MCP_HTTP_PORT = 8815;
|
|
2902
|
-
function buildServer() {
|
|
2903
|
-
const db = openDatabase();
|
|
2904
|
-
ensurePricingSeeded(db);
|
|
2905
|
-
const server = new McpServer({
|
|
2906
|
-
name: MCP_NAME,
|
|
2907
|
-
version: packageMetadata.version
|
|
2908
|
-
});
|
|
2909
|
-
const _econAgents = new Map;
|
|
2910
|
-
const TOOL_NAMES = [
|
|
2911
|
-
"get_cost_summary",
|
|
2912
|
-
"get_sessions",
|
|
2913
|
-
"get_top_sessions",
|
|
2914
|
-
"get_model_breakdown",
|
|
2915
|
-
"get_project_breakdown",
|
|
2916
|
-
"get_budget_status",
|
|
2917
|
-
"set_budget",
|
|
2918
|
-
"remove_budget",
|
|
2919
|
-
"get_pricing",
|
|
2920
|
-
"set_pricing",
|
|
2921
|
-
"remove_pricing",
|
|
2922
|
-
"get_daily",
|
|
2923
|
-
"get_billing_summary",
|
|
2924
|
-
"get_session_detail",
|
|
2925
|
-
"sync",
|
|
2926
|
-
"search_tools",
|
|
2927
|
-
"describe_tools",
|
|
2928
|
-
"get_goals",
|
|
2929
|
-
"set_goal",
|
|
2930
|
-
"remove_goal",
|
|
2931
|
-
"list_machines",
|
|
2932
|
-
"register_agent",
|
|
2933
|
-
"heartbeat",
|
|
2934
|
-
"set_focus",
|
|
2935
|
-
"list_agents",
|
|
2936
|
-
"send_feedback"
|
|
2937
|
-
];
|
|
2938
|
-
const TOOL_DESCRIPTIONS = {
|
|
2939
|
-
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
2940
|
-
get_sessions: "agent(claude|takumi|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
|
|
2941
|
-
get_top_sessions: "n(10), agent(claude|takumi|codex|gemini) -> top sessions by cost",
|
|
2942
|
-
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
2943
|
-
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
2944
|
-
get_project_breakdown: "no params -> project_name, sessions, cost",
|
|
2945
|
-
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
2946
|
-
set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
|
|
2947
|
-
remove_budget: "id -> delete budget",
|
|
2948
|
-
get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
|
|
2949
|
-
set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
|
|
2950
|
-
remove_pricing: "model -> delete pricing row",
|
|
2951
|
-
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
2952
|
-
get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
|
|
2953
|
-
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
2954
|
-
sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
|
|
2955
|
-
search_tools: "query substring -> tool name list",
|
|
2956
|
-
describe_tools: "names[] -> one-line parameter hints",
|
|
2957
|
-
get_goals: "no params -> goal progress summary",
|
|
2958
|
-
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
2959
|
-
remove_goal: "id -> delete goal",
|
|
2960
|
-
register_agent: "name, session_id? -> register agent session",
|
|
2961
|
-
heartbeat: "agent_id -> update last_seen_at",
|
|
2962
|
-
set_focus: "agent_id, project_id? -> set active project context",
|
|
2963
|
-
list_agents: "no params -> registered agent list",
|
|
2964
|
-
send_feedback: "message, email?, category? -> save feedback locally"
|
|
2965
|
-
};
|
|
2966
|
-
const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2967
|
-
const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
2968
|
-
function fmtSession(s) {
|
|
2969
|
-
const id = String(s["id"] ?? "").slice(0, 8);
|
|
2970
|
-
const agent = String(s["agent"] ?? "");
|
|
2971
|
-
const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
|
|
2972
|
-
const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
|
|
2973
|
-
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
2974
|
-
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
2975
|
-
}
|
|
2976
|
-
function text(text2) {
|
|
2977
|
-
return { content: [{ type: "text", text: text2 }] };
|
|
2978
|
-
}
|
|
2979
|
-
function textError(message) {
|
|
2980
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
2981
|
-
}
|
|
2982
|
-
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
|
|
2983
|
-
const q = query?.toLowerCase();
|
|
2984
|
-
const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
|
|
2985
|
-
return text(matches.join(", "));
|
|
2986
|
-
});
|
|
2987
|
-
server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
|
|
2988
|
-
const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
|
|
908
|
+
switch (name) {
|
|
909
|
+
case "search_tools": {
|
|
910
|
+
const q = a["query"]?.toLowerCase();
|
|
911
|
+
const names = TOOLS.map((t) => t.name);
|
|
912
|
+
const matches = q ? names.filter((n) => n.includes(q)) : names;
|
|
913
|
+
return { content: [{ type: "text", text: matches.join(", ") }] };
|
|
914
|
+
}
|
|
915
|
+
case "describe_tools": {
|
|
916
|
+
const names = a["names"] ?? [];
|
|
917
|
+
const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
|
|
2989
918
|
`);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
`
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
`)
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
`
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
const summary = querySummary(db, p, undefined, true);
|
|
3191
|
-
return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
|
|
3192
|
-
});
|
|
3193
|
-
server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
3194
|
-
return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
|
|
3195
|
-
});
|
|
3196
|
-
server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
|
|
3197
|
-
const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
|
|
3198
|
-
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
3199
|
-
});
|
|
3200
|
-
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
3201
|
-
const goals = getGoalStatuses(db);
|
|
3202
|
-
if (goals.length === 0)
|
|
3203
|
-
return text("No goals set.");
|
|
3204
|
-
const lines = ["period scope limit spent used% status"];
|
|
3205
|
-
for (const goal of goals) {
|
|
3206
|
-
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
3207
|
-
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
3208
|
-
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
3209
|
-
lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
3210
|
-
}
|
|
3211
|
-
return text(lines.join(`
|
|
3212
|
-
`));
|
|
3213
|
-
});
|
|
3214
|
-
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
3215
|
-
period: z.enum(["day", "week", "month", "year"]),
|
|
3216
|
-
limit_usd: z.number().positive(),
|
|
3217
|
-
project_path: z.string().optional(),
|
|
3218
|
-
agent: z.string().optional()
|
|
3219
|
-
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
3220
|
-
const now = new Date().toISOString();
|
|
3221
|
-
upsertGoal(db, {
|
|
3222
|
-
id: randomUUID(),
|
|
3223
|
-
period,
|
|
3224
|
-
project_path: project_path ?? null,
|
|
3225
|
-
agent: agent ?? null,
|
|
3226
|
-
limit_usd,
|
|
3227
|
-
created_at: now,
|
|
3228
|
-
updated_at: now
|
|
3229
|
-
});
|
|
3230
|
-
return text(`Goal set: ${period} $${limit_usd}`);
|
|
3231
|
-
});
|
|
3232
|
-
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
3233
|
-
deleteGoal(db, id);
|
|
3234
|
-
return text("Goal removed.");
|
|
3235
|
-
});
|
|
3236
|
-
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
3237
|
-
const machines = listMachines(db);
|
|
3238
|
-
if (machines.length === 0)
|
|
3239
|
-
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
3240
|
-
const lines = ["machine sessions requests cost last_active"];
|
|
3241
|
-
for (const m of machines) {
|
|
3242
|
-
lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
|
|
3243
|
-
}
|
|
3244
|
-
lines.push(`
|
|
3245
|
-
current machine: ${getMachineId()}`);
|
|
3246
|
-
return text(lines.join(`
|
|
3247
|
-
`));
|
|
3248
|
-
});
|
|
3249
|
-
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
3250
|
-
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
3251
|
-
if (existing) {
|
|
3252
|
-
existing.last_seen_at = new Date().toISOString();
|
|
3253
|
-
return text(JSON.stringify(existing));
|
|
3254
|
-
}
|
|
3255
|
-
const id = Math.random().toString(36).slice(2, 10);
|
|
3256
|
-
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
3257
|
-
_econAgents.set(id, agent);
|
|
3258
|
-
return text(JSON.stringify(agent));
|
|
3259
|
-
});
|
|
3260
|
-
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
3261
|
-
const agent = _econAgents.get(agent_id);
|
|
3262
|
-
if (!agent)
|
|
3263
|
-
return textError("Agent not found");
|
|
3264
|
-
agent.last_seen_at = new Date().toISOString();
|
|
3265
|
-
return text(`\u2665 ${agent.name}`);
|
|
3266
|
-
});
|
|
3267
|
-
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
3268
|
-
const agent = _econAgents.get(agent_id);
|
|
3269
|
-
if (!agent)
|
|
3270
|
-
return textError("Agent not found");
|
|
3271
|
-
agent.project_id = project_id ?? undefined;
|
|
3272
|
-
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
3273
|
-
});
|
|
3274
|
-
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
3275
|
-
server.tool("send_feedback", "Send feedback about this service.", {
|
|
3276
|
-
message: z.string(),
|
|
3277
|
-
email: z.string().optional(),
|
|
3278
|
-
category: z.enum(["bug", "feature", "general"]).optional()
|
|
3279
|
-
}, async ({ message, email, category }) => {
|
|
3280
|
-
try {
|
|
3281
|
-
db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
|
|
3282
|
-
return text("Feedback saved. Thank you!");
|
|
3283
|
-
} catch (error) {
|
|
3284
|
-
return textError(String(error));
|
|
3285
|
-
}
|
|
3286
|
-
});
|
|
3287
|
-
registerCloudTools(server, MCP_NAME, {
|
|
3288
|
-
dbPath: getDbPath(),
|
|
3289
|
-
migrations: PG_MIGRATIONS
|
|
3290
|
-
});
|
|
3291
|
-
return server;
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
// src/mcp/http.ts
|
|
3295
|
-
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3296
|
-
function isHttpMode(argv = process.argv.slice(2)) {
|
|
3297
|
-
return argv.includes("--http") || process.env["MCP_HTTP"] === "1";
|
|
3298
|
-
}
|
|
3299
|
-
function resolveHttpPort(argv = process.argv.slice(2)) {
|
|
3300
|
-
for (let i = 0;i < argv.length; i++) {
|
|
3301
|
-
const arg = argv[i];
|
|
3302
|
-
if (arg === "--port" || arg === "-p") {
|
|
3303
|
-
const raw = argv[i + 1];
|
|
3304
|
-
if (!raw)
|
|
3305
|
-
throw new Error(`Invalid port: ${raw ?? ""}`);
|
|
3306
|
-
return parsePort(raw, "port");
|
|
919
|
+
return { content: [{ type: "text", text: result }] };
|
|
920
|
+
}
|
|
921
|
+
case "get_cost_summary": {
|
|
922
|
+
const period = a["period"] ?? "today";
|
|
923
|
+
const s = querySummary(db, period);
|
|
924
|
+
const text = [
|
|
925
|
+
`period: ${period}`,
|
|
926
|
+
`cost: ${fmtUsd(s.total_usd)}`,
|
|
927
|
+
`sessions: ${s.sessions}`,
|
|
928
|
+
`requests: ${s.requests.toLocaleString()}`,
|
|
929
|
+
`tokens: ${fmtTok(s.tokens)}`,
|
|
930
|
+
`summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
931
|
+
].join(`
|
|
932
|
+
`);
|
|
933
|
+
return { content: [{ type: "text", text }] };
|
|
934
|
+
}
|
|
935
|
+
case "get_sessions": {
|
|
936
|
+
const sessions = querySessions(db, {
|
|
937
|
+
agent: a["agent"],
|
|
938
|
+
project: a["project"],
|
|
939
|
+
limit: Number(a["limit"] ?? 20)
|
|
940
|
+
});
|
|
941
|
+
const lines = ["id agent cost tokens project"];
|
|
942
|
+
for (const s of sessions)
|
|
943
|
+
lines.push(fmtSession(s));
|
|
944
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
945
|
+
`) }] };
|
|
946
|
+
}
|
|
947
|
+
case "get_top_sessions": {
|
|
948
|
+
const sessions = queryTopSessions(db, Number(a["n"] ?? 10), a["agent"]);
|
|
949
|
+
const lines = ["rank id agent cost tokens project"];
|
|
950
|
+
sessions.forEach((s, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(s)}`));
|
|
951
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
952
|
+
`) }] };
|
|
953
|
+
}
|
|
954
|
+
case "get_model_breakdown": {
|
|
955
|
+
const rows = queryModelBreakdown(db);
|
|
956
|
+
const lines = ["model reqs tokens cost"];
|
|
957
|
+
for (const r of rows) {
|
|
958
|
+
lines.push(`${String(r["model"]).slice(0, 30).padEnd(31)}${String(r["requests"]).padEnd(8)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
|
|
959
|
+
}
|
|
960
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
961
|
+
`) }] };
|
|
962
|
+
}
|
|
963
|
+
case "get_project_breakdown": {
|
|
964
|
+
const rows = queryProjectBreakdown(db);
|
|
965
|
+
const lines = ["project sessions tokens cost"];
|
|
966
|
+
for (const r of rows) {
|
|
967
|
+
const name2 = String(r["project_name"] || r["project_path"] || "\u2014").slice(0, 20);
|
|
968
|
+
lines.push(`${name2.padEnd(21)}${String(r["sessions"]).padEnd(9)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
|
|
969
|
+
}
|
|
970
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
971
|
+
`) }] };
|
|
972
|
+
}
|
|
973
|
+
case "get_budget_status": {
|
|
974
|
+
const budgets = getBudgetStatuses(db);
|
|
975
|
+
if (budgets.length === 0)
|
|
976
|
+
return { content: [{ type: "text", text: "No budgets set." }] };
|
|
977
|
+
const lines = ["scope period spent limit used% status"];
|
|
978
|
+
for (const b of budgets) {
|
|
979
|
+
const scope = String(b["project_path"] ?? "global").slice(0, 20);
|
|
980
|
+
const pct = Number(b["percent_used"]).toFixed(1);
|
|
981
|
+
const status = b["is_over_limit"] ? "OVER" : b["is_over_alert"] ? "ALERT" : "OK";
|
|
982
|
+
lines.push(`${scope.padEnd(21)}${String(b["period"]).padEnd(9)}${fmtUsd(Number(b["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(b["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
|
|
983
|
+
}
|
|
984
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
985
|
+
`) }] };
|
|
986
|
+
}
|
|
987
|
+
case "get_daily": {
|
|
988
|
+
const days = Number(a["days"] ?? 30);
|
|
989
|
+
const rows = queryDailyBreakdown(db, days);
|
|
990
|
+
const lines = ["date claude codex gemini total"];
|
|
991
|
+
const byDate = new Map;
|
|
992
|
+
for (const r of rows) {
|
|
993
|
+
const d = String(r["date"]);
|
|
994
|
+
const entry = byDate.get(d) ?? { claude: 0, codex: 0, gemini: 0 };
|
|
995
|
+
if (r["agent"] === "claude")
|
|
996
|
+
entry.claude += Number(r["cost_usd"]);
|
|
997
|
+
else if (r["agent"] === "codex")
|
|
998
|
+
entry.codex += Number(r["cost_usd"]);
|
|
999
|
+
else if (r["agent"] === "gemini")
|
|
1000
|
+
entry.gemini += Number(r["cost_usd"]);
|
|
1001
|
+
byDate.set(d, entry);
|
|
1002
|
+
}
|
|
1003
|
+
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
1004
|
+
const total = costs.claude + costs.codex + costs.gemini;
|
|
1005
|
+
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
1006
|
+
}
|
|
1007
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
1008
|
+
`) }] };
|
|
1009
|
+
}
|
|
1010
|
+
case "get_session_detail": {
|
|
1011
|
+
const sid = String(a["session_id"] ?? "");
|
|
1012
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sid, `${sid}%`);
|
|
1013
|
+
if (!session)
|
|
1014
|
+
return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
|
|
1015
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
1016
|
+
const lines = [
|
|
1017
|
+
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
1018
|
+
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
1019
|
+
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
1020
|
+
"",
|
|
1021
|
+
"time model input output cost"
|
|
1022
|
+
];
|
|
1023
|
+
for (const r of requests) {
|
|
1024
|
+
lines.push(`${String(r["timestamp"]).slice(11, 19)} ${String(r["model"]).slice(0, 22).padEnd(23)}${fmtTok(Number(r["input_tokens"])).padEnd(9)}${fmtTok(Number(r["output_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
|
|
1025
|
+
}
|
|
1026
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
1027
|
+
`) }] };
|
|
1028
|
+
}
|
|
1029
|
+
case "sync": {
|
|
1030
|
+
const sources = a["sources"] ?? "all";
|
|
1031
|
+
const parts = [];
|
|
1032
|
+
if (sources === "all" || sources === "claude") {
|
|
1033
|
+
const r = await ingestClaude(db);
|
|
1034
|
+
parts.push(`claude: ${r["files"]} files, ${r["requests"]} requests, ${r["sessions"]} sessions`);
|
|
1035
|
+
}
|
|
1036
|
+
if (sources === "all" || sources === "codex") {
|
|
1037
|
+
const r = await ingestCodex(db);
|
|
1038
|
+
parts.push(`codex: ${r["sessions"]} sessions`);
|
|
1039
|
+
}
|
|
1040
|
+
if (sources === "all" || sources === "gemini") {
|
|
1041
|
+
const r = await ingestGemini(db);
|
|
1042
|
+
parts.push(`gemini: ${r["sessions"]} sessions`);
|
|
1043
|
+
}
|
|
1044
|
+
return { content: [{ type: "text", text: parts.join(`
|
|
1045
|
+
`) || "done" }] };
|
|
1046
|
+
}
|
|
1047
|
+
case "get_goals": {
|
|
1048
|
+
const goals = getGoalStatuses(db);
|
|
1049
|
+
if (goals.length === 0)
|
|
1050
|
+
return { content: [{ type: "text", text: "No goals set." }] };
|
|
1051
|
+
const lines = ["period scope limit spent used% status"];
|
|
1052
|
+
for (const g of goals) {
|
|
1053
|
+
const scope = String(g["project_path"] ?? g["agent"] ?? "global").slice(0, 20);
|
|
1054
|
+
const pct = Number(g["percent_used"]).toFixed(1);
|
|
1055
|
+
const status = g["is_over"] ? "OVER" : g["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
1056
|
+
lines.push(`${String(g["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(g["limit_usd"])).padEnd(11)}${fmtUsd(Number(g["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
1057
|
+
}
|
|
1058
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
1059
|
+
`) }] };
|
|
1060
|
+
}
|
|
1061
|
+
case "set_goal": {
|
|
1062
|
+
const { randomUUID } = await import("crypto");
|
|
1063
|
+
const now = new Date().toISOString();
|
|
1064
|
+
upsertGoal(db, {
|
|
1065
|
+
id: randomUUID(),
|
|
1066
|
+
period: String(a["period"] ?? "month"),
|
|
1067
|
+
project_path: a["project_path"] ?? null,
|
|
1068
|
+
agent: a["agent"] ?? null,
|
|
1069
|
+
limit_usd: Number(a["limit_usd"]),
|
|
1070
|
+
created_at: now,
|
|
1071
|
+
updated_at: now
|
|
1072
|
+
});
|
|
1073
|
+
return { content: [{ type: "text", text: `Goal set: ${a["period"]} $${a["limit_usd"]}` }] };
|
|
1074
|
+
}
|
|
1075
|
+
case "remove_goal": {
|
|
1076
|
+
deleteGoal(db, String(a["id"] ?? ""));
|
|
1077
|
+
return { content: [{ type: "text", text: "Goal removed." }] };
|
|
1078
|
+
}
|
|
1079
|
+
case "register_agent": {
|
|
1080
|
+
const n = String(args["name"] ?? "");
|
|
1081
|
+
const ex = [..._econAgents.values()].find((x) => x.name === n);
|
|
1082
|
+
if (ex) {
|
|
1083
|
+
ex.last_seen_at = new Date().toISOString();
|
|
1084
|
+
return { content: [{ type: "text", text: JSON.stringify(ex) }] };
|
|
1085
|
+
}
|
|
1086
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
1087
|
+
const ag = { id, name: n, last_seen_at: new Date().toISOString() };
|
|
1088
|
+
_econAgents.set(id, ag);
|
|
1089
|
+
return { content: [{ type: "text", text: JSON.stringify(ag) }] };
|
|
1090
|
+
}
|
|
1091
|
+
case "heartbeat": {
|
|
1092
|
+
const ag = _econAgents.get(String(args["agent_id"] ?? ""));
|
|
1093
|
+
if (!ag)
|
|
1094
|
+
return { content: [{ type: "text", text: `Agent not found` }], isError: true };
|
|
1095
|
+
ag.last_seen_at = new Date().toISOString();
|
|
1096
|
+
return { content: [{ type: "text", text: `\u2665 ${ag.name}` }] };
|
|
1097
|
+
}
|
|
1098
|
+
case "set_focus": {
|
|
1099
|
+
const ag = _econAgents.get(String(args["agent_id"] ?? ""));
|
|
1100
|
+
if (!ag)
|
|
1101
|
+
return { content: [{ type: "text", text: `Agent not found` }], isError: true };
|
|
1102
|
+
ag["project_id"] = args["project_id"];
|
|
1103
|
+
return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
|
|
1104
|
+
}
|
|
1105
|
+
case "list_agents": {
|
|
1106
|
+
return { content: [{ type: "text", text: JSON.stringify([..._econAgents.values()]) }] };
|
|
1107
|
+
}
|
|
1108
|
+
case "send_feedback": {
|
|
1109
|
+
try {
|
|
1110
|
+
const pkg = require_package();
|
|
1111
|
+
db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(String(a["message"]), a["email"] || null, a["category"] || "general", pkg.version);
|
|
1112
|
+
return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
return { content: [{ type: "text", text: String(e) }], isError: true };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
default:
|
|
1118
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
3307
1119
|
}
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
|
|
3308
1122
|
}
|
|
3309
|
-
const fromEnv = process.env["MCP_HTTP_PORT"];
|
|
3310
|
-
if (fromEnv)
|
|
3311
|
-
return parsePort(fromEnv, "MCP_HTTP_PORT");
|
|
3312
|
-
return DEFAULT_MCP_HTTP_PORT;
|
|
3313
|
-
}
|
|
3314
|
-
function parsePort(raw, label) {
|
|
3315
|
-
const value = Number(raw);
|
|
3316
|
-
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
3317
|
-
throw new Error(`Invalid ${label}: ${raw}`);
|
|
3318
|
-
}
|
|
3319
|
-
return value;
|
|
3320
|
-
}
|
|
3321
|
-
async function handleMcpHttpRequest(req) {
|
|
3322
|
-
const url = new URL(req.url);
|
|
3323
|
-
if (url.pathname === "/health" && req.method === "GET") {
|
|
3324
|
-
return Response.json({ status: "ok", name: MCP_NAME });
|
|
3325
|
-
}
|
|
3326
|
-
if (url.pathname === "/mcp") {
|
|
3327
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
3328
|
-
sessionIdGenerator: undefined
|
|
3329
|
-
});
|
|
3330
|
-
const server = buildServer();
|
|
3331
|
-
await server.connect(transport);
|
|
3332
|
-
return transport.handleRequest(req);
|
|
3333
|
-
}
|
|
3334
|
-
return new Response("Not Found", { status: 404 });
|
|
3335
|
-
}
|
|
3336
|
-
function startHttpServer(options = {}) {
|
|
3337
|
-
const port = options.port ?? DEFAULT_MCP_HTTP_PORT;
|
|
3338
|
-
const hostname2 = options.hostname ?? "127.0.0.1";
|
|
3339
|
-
const log = options.log ?? console.error;
|
|
3340
|
-
const server = Bun.serve({
|
|
3341
|
-
port,
|
|
3342
|
-
hostname: hostname2,
|
|
3343
|
-
fetch: handleMcpHttpRequest
|
|
3344
|
-
});
|
|
3345
|
-
const address = `http://${hostname2}:${server.port}`;
|
|
3346
|
-
log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
|
|
3347
|
-
return server;
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
// src/mcp/index.ts
|
|
3351
|
-
function printHelp() {
|
|
3352
|
-
console.log(`Usage: economy-mcp [options]
|
|
3353
|
-
|
|
3354
|
-
Runs the ${packageMetadata.name} MCP server (stdio by default).
|
|
3355
|
-
|
|
3356
|
-
Options:
|
|
3357
|
-
--http Serve MCP over Streamable HTTP on 127.0.0.1
|
|
3358
|
-
-p, --port <port> HTTP port (default: MCP_HTTP_PORT or 8815)
|
|
3359
|
-
-V, --version output the version number
|
|
3360
|
-
-h, --help display help for command
|
|
3361
|
-
|
|
3362
|
-
Environment:
|
|
3363
|
-
MCP_HTTP=1 Enable HTTP mode
|
|
3364
|
-
MCP_HTTP_PORT Override default HTTP port (8815)`);
|
|
3365
|
-
}
|
|
3366
|
-
var args = process.argv.slice(2);
|
|
3367
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
3368
|
-
printHelp();
|
|
3369
|
-
process.exit(0);
|
|
3370
|
-
}
|
|
3371
|
-
if (args.includes("--version") || args.includes("-V")) {
|
|
3372
|
-
console.log(packageMetadata.version);
|
|
3373
|
-
process.exit(0);
|
|
3374
|
-
}
|
|
3375
|
-
async function main() {
|
|
3376
|
-
if (isHttpMode(args)) {
|
|
3377
|
-
startHttpServer({ port: resolveHttpPort(args) });
|
|
3378
|
-
await new Promise(() => {});
|
|
3379
|
-
return;
|
|
3380
|
-
}
|
|
3381
|
-
const server = buildServer();
|
|
3382
|
-
const transport = new StdioServerTransport;
|
|
3383
|
-
await server.connect(transport);
|
|
3384
|
-
}
|
|
3385
|
-
main().catch((error) => {
|
|
3386
|
-
console.error("MCP server error:", error);
|
|
3387
|
-
process.exit(1);
|
|
3388
1123
|
});
|
|
1124
|
+
var _econAgents = new Map;
|
|
1125
|
+
var transport = new StdioServerTransport;
|
|
1126
|
+
await server.connect(transport);
|