@juliantanx/aiusage 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +8 -18
  2. package/dist/index.js +1982 -662
  3. package/dist/web/_app/immutable/assets/12.C1f-IaHK.css +1 -0
  4. package/dist/web/_app/immutable/assets/7.CahA9iix.css +1 -0
  5. package/dist/web/_app/immutable/assets/_page.CahA9iix.css +1 -0
  6. package/dist/web/_app/immutable/assets/_page.DO7aPEA-.css +1 -0
  7. package/dist/web/_app/immutable/chunks/{ToolSelector.CofvKVw2.js → ToolSelector.0iI1cXVj.js} +2 -2
  8. package/dist/web/_app/immutable/chunks/api.DVLAklIn.js +1 -0
  9. package/dist/web/_app/immutable/chunks/{entry.BdArkS4U.js → entry.DhBBmiYe.js} +2 -2
  10. package/dist/web/_app/immutable/chunks/i18n.DApJIXOT.js +1 -0
  11. package/dist/web/_app/immutable/chunks/{index.66Vu9Sio.js → index.BfJlbjrK.js} +1 -1
  12. package/dist/web/_app/immutable/chunks/{stores.COyyqc3i.js → stores.CSRSkpU3.js} +1 -1
  13. package/dist/web/_app/immutable/entry/{app.fIWuRyD7.js → app.C_5glR-X.js} +2 -2
  14. package/dist/web/_app/immutable/entry/start.B0qYhR-d.js +1 -0
  15. package/dist/web/_app/immutable/nodes/{0.D19pLRbx.js → 0.DJfx41KV.js} +1 -1
  16. package/dist/web/_app/immutable/nodes/{1.Cr_OSeqa.js → 1.BLZj3WNW.js} +1 -1
  17. package/dist/web/_app/immutable/nodes/{10.DTaETIM6.js → 10.CnTuhaua.js} +1 -1
  18. package/dist/web/_app/immutable/nodes/{11.BbAcHr8g.js → 11.Bu1W8Jx8.js} +1 -1
  19. package/dist/web/_app/immutable/nodes/12.BCT8Dv1G.js +1 -0
  20. package/dist/web/_app/immutable/nodes/{13.BFaBQy6e.js → 13.Cu-n0wZ9.js} +1 -1
  21. package/dist/web/_app/immutable/nodes/{14.BgJEC28r.js → 14.u96Ucsue.js} +1 -1
  22. package/dist/web/_app/immutable/nodes/{15.DjuCO6OE.js → 15.CMhy_NGl.js} +1 -1
  23. package/dist/web/_app/immutable/nodes/{2.CoxR54dV.js → 2.JQynNxpe.js} +1 -1
  24. package/dist/web/_app/immutable/nodes/{3.Bua2oewW.js → 3.Chtgl9hh.js} +1 -1
  25. package/dist/web/_app/immutable/nodes/{4.DJwmYYKI.js → 4.BIau4H47.js} +1 -1
  26. package/dist/web/_app/immutable/nodes/{5.CXmtjVJL.js → 5.DtYG5yJy.js} +1 -1
  27. package/dist/web/_app/immutable/nodes/{6.kpxBlrHt.js → 6.B-ud4zCO.js} +1 -1
  28. package/dist/web/_app/immutable/nodes/7.7Fm6yzqA.js +9 -0
  29. package/dist/web/_app/immutable/nodes/{8.ByAI01dT.js → 8.Chp3dlzK.js} +1 -1
  30. package/dist/web/_app/immutable/nodes/{9.Ubt4Bfxd.js → 9.PMJrdCxq.js} +1 -1
  31. package/dist/web/_app/version.json +1 -1
  32. package/dist/web/index.html +7 -7
  33. package/package.json +7 -5
  34. package/dist/web/_app/immutable/assets/12.nNHFRI35.css +0 -1
  35. package/dist/web/_app/immutable/assets/7.oxd6PqWk.css +0 -1
  36. package/dist/web/_app/immutable/assets/_page.ZSDsIDry.css +0 -1
  37. package/dist/web/_app/immutable/assets/_page.oxd6PqWk.css +0 -1
  38. package/dist/web/_app/immutable/chunks/api.Mv_KcGhp.js +0 -1
  39. package/dist/web/_app/immutable/chunks/i18n.CwV3KceS.js +0 -1
  40. package/dist/web/_app/immutable/entry/start.B0xmplla.js +0 -1
  41. package/dist/web/_app/immutable/nodes/12.DeLNKhlw.js +0 -1
  42. package/dist/web/_app/immutable/nodes/7.LSfdx4vz.js +0 -9
package/dist/index.js CHANGED
@@ -8,19 +8,21 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
 
9
9
  // src/cli.ts
10
10
  import { Command } from "commander";
11
- import { writeFileSync as writeFileSync6 } from "fs";
11
+ import { writeFileSync as writeFileSync7 } from "fs";
12
12
  import { execSync as execSync5 } from "child_process";
13
13
 
14
14
  // src/commands/serve.ts
15
15
  import http2 from "http";
16
- import { readFileSync as readFileSync8, existsSync as existsSync9, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
17
- import { join as join10, extname as extname2, dirname as dirname3 } from "path";
16
+ import { readFileSync as readFileSync9, existsSync as existsSync10, statSync as statSync3, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
17
+ import { join as join11, extname as extname2, dirname as dirname4 } from "path";
18
18
  import { fileURLToPath } from "url";
19
19
 
20
20
  // src/api/server.ts
21
21
  import http from "http";
22
- import { hostname, platform as platform3 } from "os";
22
+ import path2 from "path";
23
+ import { hostname, platform as platform3, tmpdir } from "os";
23
24
  import { randomBytes as randomBytes3 } from "crypto";
25
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
24
26
 
25
27
  // ../core/dist/index.js
26
28
  import { createHash } from "crypto";
@@ -33,6 +35,7 @@ var TOOLS = [
33
35
  "qoder",
34
36
  "cursor",
35
37
  "kilocode",
38
+ "kelivo",
36
39
  "copilot",
37
40
  "gemini",
38
41
  "kimi",
@@ -109,204 +112,22 @@ async function fetchExchangeRate() {
109
112
  function convertToUSD(cny, rate) {
110
113
  return cny * rate;
111
114
  }
112
- var DEFAULT_PRICE_TABLE = {
113
- // Anthropic — https://platform.claude.com/docs/en/about-claude/pricing
114
- "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
115
- "claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
116
- "claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
117
- "claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
118
- "claude-opus-4-1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
119
- "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
120
- "claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
121
- "claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
122
- "claude-haiku-3-5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
123
- "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
124
- "claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
125
- "claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
126
- "claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
127
- // OpenAI — https://developers.openai.com/api/docs/pricing
128
- "gpt-5.5-pro": { input: 30, output: 180 },
129
- "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5 },
130
- "gpt-5.4-pro": { input: 30, output: 180 },
131
- "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075 },
132
- "gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02 },
133
- "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25 },
134
- "gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
135
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
136
- "gpt-4o": { input: 2.5, output: 10 },
137
- "gpt-4.1": { input: 2, output: 8 },
138
- "o4-mini": { input: 1.1, output: 4.4 },
139
- "o4-mini-deep-research": { input: 1, output: 4 },
140
- "o3-pro": { input: 20, output: 80 },
141
- "o3": { input: 2, output: 8 },
142
- "o3-mini": { input: 0.55, output: 2.2, cacheRead: 0.55 },
143
- "o3-deep-research": { input: 5, output: 20 },
144
- "o1": { input: 15, output: 60, cacheRead: 7.5 },
145
- "o1-mini": { input: 0.55, output: 2.2, cacheRead: 0.55 },
146
- "codex-mini": { input: 0.75, output: 3, cacheRead: 0.025 },
147
- "gpt-5.2-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
148
- "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
149
- "gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
150
- "gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125 },
151
- "gpt-5-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
152
- "gpt-5": { input: 1.25, output: 10, cacheRead: 0.125 },
153
- "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
154
- "gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 5e-3 },
155
- "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
156
- "gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
157
- // Google Gemini — https://ai.google.dev/gemini-api/docs/pricing
158
- "gemini-3.5-flash": { input: 1.5, output: 9 },
159
- "gemini-3.1-pro": { input: 2, output: 12 },
160
- "gemini-3.1-flash-lite": { input: 0.25, output: 1.5 },
161
- "gemini-3-pro-preview": { input: 2, output: 12 },
162
- "gemini-3-flash-preview": { input: 0.5, output: 3 },
163
- "gemini-3-flash": { input: 0.5, output: 3 },
164
- "gemini-3-pro-image": { input: 2, output: 12 },
165
- "gemini-3.1-pro-preview": { input: 2, output: 12 },
166
- "gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5 },
167
- "gemini-2.5-pro": { input: 1.25, output: 10 },
168
- "gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
169
- "gemini-2.5-flash": { input: 0.3, output: 2.5 },
170
- "gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
171
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
172
- // DeepSeek — https://api-docs.deepseek.com/quick_start/pricing
173
- "deepseek-v4-pro": { input: 3.13, output: 6.26, cacheRead: 0.026, currency: "CNY" },
174
- "deepseek-v4-flash": { input: 1.01, output: 2.02, cacheRead: 0.02, currency: "CNY" },
175
- "deepseek-v3.2": { input: 2.01, output: 3.01, cacheRead: 0.2, currency: "CNY" },
176
- "deepseek-v3.1": { input: 3.94, output: 11.9, cacheRead: 0.39, currency: "CNY" },
177
- "deepseek-v3": { input: 2.01, output: 7.94, cacheRead: 0.2, currency: "CNY" },
178
- "deepseek-reasoner": { input: 3.94, output: 15.6, cacheRead: 1, currency: "CNY" },
179
- "deepseek-chat": { input: 1.93, output: 7.87, cacheRead: 0.5, currency: "CNY" },
180
- // Kimi (Moonshot AI) — https://platform.kimi.ai/docs/pricing/chat
181
- "kimi-k2.6": { input: 6.9, output: 29, cacheRead: 1.15, currency: "CNY" },
182
- "kimi-k2.5": { input: 4.3, output: 21.6, cacheRead: 0.72, currency: "CNY" },
183
- "kimi-k2-turbo": { input: 8.3, output: 57.6, cacheRead: 1.08, currency: "CNY" },
184
- "kimi-k2": { input: 4.3, output: 18, cacheRead: 1.08, currency: "CNY" },
185
- "kimi-k2-thinking": { input: 3.94, output: 15.7, cacheRead: 0.71, currency: "CNY" },
186
- "kimi-k2-0905": { input: 3.94, output: 15.7, cacheRead: 0.71, currency: "CNY" },
187
- "moonshot-v1-128k": { input: 14.4, output: 36, currency: "CNY" },
188
- "moonshot-v1-32k": { input: 7.2, output: 21.6, currency: "CNY" },
189
- "moonshot-v1-8k": { input: 1.44, output: 14.4, currency: "CNY" },
190
- // GLM (Z.ai / Zhipu AI) — https://docs.z.ai/guides/overview/pricing — TODO: verify against official pricing
191
- "glm-5.1": { input: 10, output: 32, currency: "CNY" },
192
- "glm-5p1": { input: 10, output: 32, currency: "CNY" },
193
- // Fireworks alias (accounts/fireworks/models/glm-5p1)
194
- "glm-5-turbo": { input: 8.5, output: 29, currency: "CNY" },
195
- "glm-5": { input: 7, output: 23, currency: "CNY" },
196
- "glm-4.7-flashx": { input: 0.5, output: 2.8, currency: "CNY" },
197
- "glm-4.7": { input: 4, output: 16, currency: "CNY" },
198
- "glm-4.6": { input: 4, output: 16, currency: "CNY" },
199
- "glm-4.5-x": { input: 16, output: 64, currency: "CNY" },
200
- "glm-4.5-airx": { input: 8, output: 32, currency: "CNY" },
201
- "glm-4.5-air": { input: 1.4, output: 8, currency: "CNY" },
202
- "glm-4.5": { input: 4, output: 16, currency: "CNY" },
203
- // Qwen (Alibaba Cloud) — https://www.alibabacloud.com/help/en/model-studio/model-pricing
204
- "qwen3.6-plus": { input: 2, output: 12, currency: "CNY" },
205
- "qwen3-235b": { input: 5, output: 20, currency: "CNY" },
206
- "qwen3-32b": { input: 1.12, output: 4.48, currency: "CNY" },
207
- "qwen3-30b": { input: 1.4, output: 5.6, currency: "CNY" },
208
- "qwen-max": { input: 11.5, output: 46, currency: "CNY" },
209
- "qwen-plus": { input: 2.8, output: 8.4, currency: "CNY" },
210
- "qwen-turbo": { input: 0.35, output: 1.4, currency: "CNY" },
211
- "qwen-long": { input: 0.5, output: 2, currency: "CNY" },
212
- "qwen2.5-72b": { input: 10, output: 40, currency: "CNY" },
213
- "qwen2.5-7b": { input: 1.26, output: 5.04, currency: "CNY" },
214
- "qwen3.5-plus": { input: 1.86, output: 11.2, currency: "CNY" },
215
- "qwen3-max": { input: 5.59, output: 27.9, currency: "CNY" },
216
- "qwen3-235b-a22b": { input: 5.01, output: 60.1, currency: "CNY" },
217
- "qwen3-coder-plus": { input: 4.65, output: 23.3, currency: "CNY" },
218
- "qwen3-coder-flash": { input: 1.4, output: 6.98, currency: "CNY" },
219
- "qwen3-coder-next": { input: 0.86, output: 5.37, currency: "CNY" },
220
- "qwq-plus": { input: 5.73, output: 17.2, currency: "CNY" },
221
- "qwq-32b": { input: 1.43, output: 4.3, currency: "CNY" },
222
- // Xiaomi MiMo — https://platform.xiaomimimo.com/docs/zh-CN/pricing
223
- "mimo-v2.5-pro": { input: 3, output: 6, cacheRead: 0.025, currency: "CNY" },
224
- "mimo-v2.5": { input: 1, output: 2, cacheRead: 0.02, currency: "CNY" },
225
- "mimo-v2-flash": { input: 0.64, output: 2.08, cacheRead: 0.064, currency: "CNY" },
226
- "mimo-v2-pro": { input: 7.17, output: 21.5, currency: "CNY" },
227
- // MiniMax — https://platform.minimaxi.com/docs/guides/pricing-paygo
228
- "minimax-m2.7": { input: 2.15, output: 8.59, cacheRead: 0.43, cacheWrite: 2.68, currency: "CNY" },
229
- "minimax-m2.7-highspeed": { input: 4.3, output: 17.2, cacheRead: 0.43, cacheWrite: 2.68, currency: "CNY" },
230
- "minimax-m2.5-lightning": { input: 2.15, output: 17.2, cacheRead: 0.21, currency: "CNY" },
231
- "minimax-m2.5": { input: 0.86, output: 6.81, cacheRead: 0.21, currency: "CNY" },
232
- "minimax-m2.1": { input: 1.93, output: 6.81, cacheRead: 0.21, currency: "CNY" },
233
- "minimax-m2.1-lightning": { input: 1.93, output: 16.7, cacheRead: 0.21, currency: "CNY" },
234
- "minimax-m2": { input: 1.93, output: 6.81, cacheRead: 0.21, currency: "CNY" },
235
- // Mistral AI — https://mistral.ai/pricing
236
- "mistral-large": { input: 0.5, output: 1.5 },
237
- "mistral-large-3": { input: 0.5, output: 1.5 },
238
- "mistral-medium": { input: 1.5, output: 7.5 },
239
- "mistral-small": { input: 0.1, output: 0.3 },
240
- "magistral-medium": { input: 2, output: 5 },
241
- "magistral-small": { input: 0.5, output: 1.5 },
242
- "devstral-medium": { input: 0.4, output: 2 },
243
- "devstral-small": { input: 0.1, output: 0.3 },
244
- "codestral": { input: 0.3, output: 0.9 },
245
- "open-mistral-nemo": { input: 0.15, output: 0.15 },
246
- "open-mixtral-8x22b": { input: 2, output: 6 },
247
- "ministral-14b": { input: 0.2, output: 0.2 },
248
- "ministral-8b": { input: 0.15, output: 0.15 },
249
- "ministral-3b": { input: 0.1, output: 0.1 },
250
- "pixtral-12b": { input: 0.1, output: 0.1 },
251
- // xAI Grok — https://docs.x.ai/developers/models
252
- "grok-4-1-fast": { input: 0.2, output: 0.5 },
253
- "grok-4.3": { input: 1.25, output: 2.5 },
254
- "grok-4.20-0309-reasoning": { input: 2, output: 6 },
255
- "grok-4.20-0309-non-reasoning": { input: 2, output: 6 },
256
- "grok-4": { input: 1.25, output: 2.5 },
257
- "grok-build-0.1": { input: 1, output: 2 },
258
- "grok-3": { input: 3, output: 15 },
259
- "grok-3-mini": { input: 0.25, output: 0.5 },
260
- // Cohere — https://cohere.com/pricing
261
- "command-a": { input: 2.5, output: 10 },
262
- "command-r-plus": { input: 2.5, output: 10 },
263
- "command-r": { input: 0.15, output: 0.6 },
264
- // Doubao (ByteDance) — https://www.volcengine.com/docs/82379/1544106
265
- "doubao-seed-code": { input: 1.22, output: 7.95, currency: "CNY" },
266
- "doubao-seed-2.0-pro": { input: 3.7, output: 18.5, currency: "CNY" },
267
- "doubao-seed-2.0-code": { input: 3.36, output: 16.8, currency: "CNY" },
268
- "doubao-seed-2.0-lite": { input: 0.63, output: 3.78, currency: "CNY" },
269
- "doubao-seed-2.0-mini": { input: 0.21, output: 2.1, currency: "CNY" },
270
- "doubao-seed-1.6-flash": { input: 0.16, output: 1.58, currency: "CNY" },
271
- "doubao-seed-1.6-lite": { input: 0.32, output: 2.52, currency: "CNY" },
272
- "doubao-seed-1.6": { input: 0.84, output: 8.4, currency: "CNY" },
273
- "doubao-1.5-pro": { input: 0.84, output: 2.1, currency: "CNY" },
274
- "doubao-1.5-lite": { input: 0.32, output: 0.63, currency: "CNY" },
275
- // Hunyuan (Tencent) — https://cloud.tencent.com/document/product/1729 — TODO: verify against official pricing
276
- "hunyuan-t1": { input: 0.48, output: 1.87, cacheRead: 0.21, currency: "CNY" },
277
- "hunyuan-a13b": { input: 1, output: 4.1, currency: "CNY" },
278
- // ERNIE (Baidu) — https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf — TODO: verify against official pricing
279
- "ernie-4.5-300b": { input: 2, output: 6.5, currency: "CNY" },
280
- "ernie-4.5-21b": { input: 0.5, output: 2, currency: "CNY" },
281
- "ernie-x1": { input: 2, output: 8, currency: "CNY" },
282
- // StepFun (阶跃星辰)
283
- "step-3.5-flash": { input: 0.72, output: 2.15, currency: "CNY" },
284
- // Qoder credits — https://docs.qoder.com/user-guide/chat/model-tier-selector
285
- // Qoder publishes credit multipliers and a $0.02 / credit pack price, but
286
- // not a full per-token tariff. Estimate 1.0x as 1 credit / 10K tokens.
287
- // Ultimate uses the current official limited-time 0.8x discount.
288
- "qoder-auto": { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 },
289
- "qoder-ultimate": { input: 1.6, output: 1.6, cacheRead: 1.6, cacheWrite: 1.6 },
290
- "qoder-performance": { input: 2.2, output: 2.2, cacheRead: 2.2, cacheWrite: 2.2 },
291
- "qoder-efficient": { input: 0.6, output: 0.6, cacheRead: 0.6, cacheWrite: 0.6 },
292
- "qoder-lite": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
293
- };
115
+ var basePriceTable = {};
294
116
  var userOverrides = {};
295
- var PRICE_TABLE = { ...DEFAULT_PRICE_TABLE };
296
- function getPriceTable() {
297
- return { ...DEFAULT_PRICE_TABLE, ...userOverrides };
117
+ var PRICE_TABLE = {};
118
+ function setRuntimePriceTable(base, overrides = {}) {
119
+ basePriceTable = { ...base };
120
+ userOverrides = { ...overrides };
121
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
298
122
  }
299
123
  function setPriceOverride(model, entry) {
300
124
  userOverrides = { ...userOverrides, [model]: entry };
301
- PRICE_TABLE = { ...DEFAULT_PRICE_TABLE, ...userOverrides };
125
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
302
126
  }
303
127
  function removePriceOverride(model) {
304
128
  const { [model]: _, ...rest } = userOverrides;
305
129
  userOverrides = rest;
306
- PRICE_TABLE = { ...DEFAULT_PRICE_TABLE, ...userOverrides };
307
- }
308
- function getUserOverrides() {
309
- return { ...userOverrides };
130
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
310
131
  }
311
132
  var PROVIDER_PREFIXES = [
312
133
  "accounts/fireworks/models/",
@@ -363,6 +184,15 @@ function calculateCostWithResolver(model, tokens, exchangeRate, resolver) {
363
184
  function calculateCost(model, tokens, exchangeRate) {
364
185
  return calculateCostWithResolver(model, tokens, exchangeRate, resolvePrice);
365
186
  }
187
+ function calculateCostForPrice(price, tokens, exchangeRate) {
188
+ const inputCost = tokens.inputTokens / 1e6 * price.input;
189
+ const outputCost = tokens.outputTokens / 1e6 * price.output;
190
+ const cacheReadCost = tokens.cacheReadTokens / 1e6 * (price.cacheRead ?? 0);
191
+ const cacheWriteCost = tokens.cacheWriteTokens / 1e6 * (price.cacheWrite ?? 0);
192
+ const thinkingCost = tokens.thinkingTokens / 1e6 * price.output;
193
+ const rawCost = inputCost + outputCost + cacheReadCost + cacheWriteCost + thinkingCost;
194
+ return price.currency === "CNY" ? convertToUSD(rawCost, exchangeRate ?? FALLBACK_RATE) : rawCost;
195
+ }
366
196
  var QODER_TIER_MODELS = /* @__PURE__ */ new Set([
367
197
  "auto",
368
198
  "ultimate",
@@ -1048,14 +878,14 @@ function usageFromAny(parsed) {
1048
878
  const cachedFromDetails = num(details.cached_tokens);
1049
879
  const rawInput = usage.input_tokens ?? usage.input ?? usage.prompt_tokens ?? usage.input_other ?? usage.tokensIn ?? 0;
1050
880
  const cacheRead = num(
1051
- usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.input_cache_read ?? usage.cacheReads ?? cachedFromDetails
881
+ usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.input_cache_read ?? usage.cacheReads ?? usage.cacheRead ?? cachedFromDetails
1052
882
  );
1053
883
  return {
1054
884
  inputTokens: Math.max(0, num(rawInput) - cachedFromDetails),
1055
885
  outputTokens: num(usage.output_tokens ?? usage.output ?? usage.completion_tokens ?? usage.tokensOut),
1056
886
  cacheReadTokens: cacheRead,
1057
887
  cacheWriteTokens: num(
1058
- usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? usage.cache_write_tokens ?? usage.input_cache_creation ?? usage.cacheWrites
888
+ usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? usage.cache_write_tokens ?? usage.input_cache_creation ?? usage.cacheWrites ?? usage.cacheWrite
1059
889
  ),
1060
890
  thinkingTokens: num(
1061
891
  usage.thinking_tokens ?? usage.reasoning_tokens ?? usage.reasoning_output_tokens ?? details.reasoning_tokens
@@ -1199,8 +1029,8 @@ var Aggregator = class {
1199
1029
  };
1200
1030
 
1201
1031
  // src/config.ts
1202
- import { readFileSync, writeFileSync, existsSync } from "fs";
1203
- import { join } from "path";
1032
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
1033
+ import { join, dirname } from "path";
1204
1034
  import { homedir } from "os";
1205
1035
  var AIUSAGE_DIR = join(homedir(), ".aiusage");
1206
1036
  var CONFIG_PATH = join(AIUSAGE_DIR, "config.json");
@@ -1230,6 +1060,7 @@ function loadConfig() {
1230
1060
  }
1231
1061
  }
1232
1062
  function saveConfig(config) {
1063
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
1233
1064
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
1234
1065
  }
1235
1066
  function buildConsentConfig(config) {
@@ -1257,7 +1088,7 @@ function loadCredential(key) {
1257
1088
  }
1258
1089
 
1259
1090
  // src/init.ts
1260
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1091
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1261
1092
  import { join as join2 } from "path";
1262
1093
  import { randomUUID } from "crypto";
1263
1094
  function getState(aiusageDir) {
@@ -1720,6 +1551,13 @@ function probeKiloTasks(ctx) {
1720
1551
  }
1721
1552
  return null;
1722
1553
  }
1554
+ function probeKelivo(ctx) {
1555
+ const override = envOverride("kelivo", ctx.env);
1556
+ if (override) return override;
1557
+ const legacy = ctx.legacySources?.["kelivo"];
1558
+ if (legacy) return legacy;
1559
+ return null;
1560
+ }
1723
1561
  function probeCopilot(ctx) {
1724
1562
  const override = envOverride("copilot", ctx.env);
1725
1563
  if (override) return override;
@@ -1905,6 +1743,7 @@ var TOOL_REGISTRY = [
1905
1743
  { tool: "cursor", sourceKey: "cursor", label: "Cursor", probe: probeCursor },
1906
1744
  { tool: "kilocode", sourceKey: "kilocode", label: "Kilo Code (extension)", probe: probeKiloTasks },
1907
1745
  { tool: "kilocode", sourceKey: "kilocode-db", label: "KiloCode", probe: probeKiloDb },
1746
+ { tool: "kelivo", sourceKey: "kelivo", label: "Kelivo", probe: probeKelivo },
1908
1747
  { tool: "copilot", sourceKey: "copilot", label: "Copilot", probe: probeCopilot },
1909
1748
  { tool: "gemini", sourceKey: "gemini", label: "Gemini CLI", probe: probeGemini },
1910
1749
  { tool: "kimi", sourceKey: "kimi", label: "Kimi Code", probe: probeKimi },
@@ -1926,11 +1765,11 @@ function discoverTools(env = process.env) {
1926
1765
  const legacySources = config?.sources;
1927
1766
  const ctx = { home, env, legacySources };
1928
1767
  return TOOL_REGISTRY.map((entry) => {
1929
- const path2 = entry.probe(ctx);
1930
- if (!path2) {
1768
+ const path3 = entry.probe(ctx);
1769
+ if (!path3) {
1931
1770
  return { tool: entry.tool, sourceKey: entry.sourceKey, label: entry.label, path: "", fileCount: 0, status: "not_found" };
1932
1771
  }
1933
- const detectedPaths = entry.sourceKey === "opencode" && !envOverride("opencode", env) && !env.OPENCODE_DB && !legacySources?.["opencode"] ? existingOpenCodeDbs(env).filter((dbPath) => existsSync3(dbPath)) : entry.sourceKey === "codex" ? codexLogDirs(ctx, path2) : [path2];
1772
+ const detectedPaths = entry.sourceKey === "opencode" && !envOverride("opencode", env) && !env.OPENCODE_DB && !legacySources?.["opencode"] ? existingOpenCodeDbs(env).filter((dbPath) => existsSync3(dbPath)) : entry.sourceKey === "codex" ? codexLogDirs(ctx, path3) : [path3];
1934
1773
  let fileCount = 0;
1935
1774
  for (const detectedPath of detectedPaths) {
1936
1775
  try {
@@ -1938,6 +1777,8 @@ function discoverTools(env = process.env) {
1938
1777
  if (stat2.isDirectory()) {
1939
1778
  if (entry.sourceKey === "roocode" || entry.sourceKey === "kilocode") {
1940
1779
  fileCount += findJsonFiles(detectedPath).filter((p) => basename(p) === "ui_messages.json").length;
1780
+ } else if (entry.sourceKey === "kelivo") {
1781
+ fileCount += findJsonFiles(detectedPath).filter((p) => basename(p) === "chats.json").length + findZipFiles(detectedPath).length;
1941
1782
  } else if (entry.sourceKey === "kiro") {
1942
1783
  fileCount += unique([...findJsonlFiles(detectedPath), ...findJsonFiles(detectedPath)]).length;
1943
1784
  } else {
@@ -1955,7 +1796,7 @@ function discoverTools(env = process.env) {
1955
1796
  tool: entry.tool,
1956
1797
  sourceKey: entry.sourceKey,
1957
1798
  label: entry.label,
1958
- path: path2,
1799
+ path: path3,
1959
1800
  paths: visiblePaths.length > 1 || entry.sourceKey === "codex" && visiblePaths.length > 0 ? visiblePaths : void 0,
1960
1801
  fileCount,
1961
1802
  status
@@ -2057,6 +1898,11 @@ function discoverLogFiles(env = process.env) {
2057
1898
  const paths = findJsonFiles(kilocodeTaskPath).filter((p) => basename(p) === "ui_messages.json");
2058
1899
  if (paths.length > 0) results.push({ tool: "kilocode", paths: unique(paths) });
2059
1900
  }
1901
+ const kelivoPath = probeKelivo(ctx);
1902
+ if (kelivoPath && existsSync3(kelivoPath)) {
1903
+ const paths = statSync(kelivoPath).isDirectory() ? [...findJsonFiles(kelivoPath).filter((p) => basename(p) === "chats.json"), ...findZipFiles(kelivoPath)] : basename(kelivoPath) === "chats.json" || basename(kelivoPath).endsWith(".zip") ? [kelivoPath] : [];
1904
+ if (paths.length > 0) results.push({ tool: "kelivo", paths: unique(paths) });
1905
+ }
2060
1906
  return results;
2061
1907
  }
2062
1908
  function findJsonFiles(dir) {
@@ -2075,6 +1921,22 @@ function findJsonFiles(dir) {
2075
1921
  }
2076
1922
  return results;
2077
1923
  }
1924
+ function findZipFiles(dir) {
1925
+ const results = [];
1926
+ try {
1927
+ const entries = readdirSync(dir, { withFileTypes: true });
1928
+ for (const entry of entries) {
1929
+ const fullPath = join3(dir, entry.name);
1930
+ if (entry.isDirectory()) {
1931
+ results.push(...findZipFiles(fullPath));
1932
+ } else if (entry.isFile() && extname(entry.name) === ".zip") {
1933
+ results.push(fullPath);
1934
+ }
1935
+ }
1936
+ } catch {
1937
+ }
1938
+ return results;
1939
+ }
2078
1940
  function normalizeQoderPath(source) {
2079
1941
  const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
2080
1942
  if (normalized.endsWith("/logs/sessions") || normalized.endsWith("/sessions") || normalized.endsWith("/segments")) {
@@ -2594,8 +2456,8 @@ function generateNonce() {
2594
2456
  function generateIdempotencyKey() {
2595
2457
  return randomBytes(16).toString("hex");
2596
2458
  }
2597
- function buildCanonicalString(method, path2, bodyHash, timestamp, nonce, deviceId, idempotencyKey) {
2598
- return [method, path2, bodyHash, timestamp, nonce, deviceId, idempotencyKey].join("\n");
2459
+ function buildCanonicalString(method, path3, bodyHash, timestamp, nonce, deviceId, idempotencyKey) {
2460
+ return [method, path3, bodyHash, timestamp, nonce, deviceId, idempotencyKey].join("\n");
2599
2461
  }
2600
2462
  function base64url(data) {
2601
2463
  const buf = typeof data === "string" ? Buffer.from(data) : data;
@@ -2606,14 +2468,14 @@ function sha256Buffer(data) {
2606
2468
  }
2607
2469
 
2608
2470
  // src/leaderboard/credentials.ts
2609
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, unlinkSync, existsSync as existsSync6, renameSync } from "fs";
2610
- import { join as join5, dirname } from "path";
2471
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, existsSync as existsSync6, renameSync } from "fs";
2472
+ import { join as join5, dirname as dirname2 } from "path";
2611
2473
  import { randomBytes as randomBytes2 } from "crypto";
2612
2474
  var CREDENTIALS_FILE = join5(AIUSAGE_DIR, "leaderboard-credentials.json");
2613
2475
  function saveCredentials(creds) {
2614
- const dir = dirname(CREDENTIALS_FILE);
2476
+ const dir = dirname2(CREDENTIALS_FILE);
2615
2477
  if (!existsSync6(dir)) {
2616
- mkdirSync2(dir, { recursive: true });
2478
+ mkdirSync3(dir, { recursive: true });
2617
2479
  }
2618
2480
  const tmpFile = `${CREDENTIALS_FILE}.${randomBytes2(4).toString("hex")}.tmp`;
2619
2481
  try {
@@ -2668,8 +2530,8 @@ async function readJsonOrNull(response) {
2668
2530
  if (!contentType.includes("application/json")) return null;
2669
2531
  return response.json().catch(() => null);
2670
2532
  }
2671
- function endpointUnavailableMessage(path2, status) {
2672
- return `Leaderboard API endpoint ${path2} is unavailable (HTTP ${status}). The site deployment is not routing this API request to the Node server.`;
2533
+ function endpointUnavailableMessage(path3, status) {
2534
+ return `Leaderboard API endpoint ${path3} is unavailable (HTTP ${status}). The site deployment is not routing this API request to the Node server.`;
2673
2535
  }
2674
2536
  async function fetchLeaderboard(serverUrl, options = {}) {
2675
2537
  const params = new URLSearchParams();
@@ -2832,14 +2694,14 @@ async function fetchLeaderboardStatus(serverUrl) {
2832
2694
 
2833
2695
  // src/db/index.ts
2834
2696
  import Database from "better-sqlite3";
2835
- import { mkdirSync as mkdirSync3 } from "fs";
2836
- import { dirname as dirname2 } from "path";
2697
+ import { mkdirSync as mkdirSync4 } from "fs";
2698
+ import { dirname as dirname3 } from "path";
2837
2699
 
2838
2700
  // src/db/schema.ts
2839
2701
  function applyPragmas(db) {
2840
2702
  db.pragma("journal_mode = WAL");
2841
2703
  db.pragma("foreign_keys = ON");
2842
- db.pragma("busy_timeout = 5000");
2704
+ db.pragma("busy_timeout = 30000");
2843
2705
  }
2844
2706
  function createReadonlyViews(db) {
2845
2707
  db.exec(`
@@ -3132,6 +2994,79 @@ function migrateV9(db) {
3132
2994
  `);
3133
2995
  }
3134
2996
 
2997
+ // src/db/migrations/v10.ts
2998
+ function migrateV10(db) {
2999
+ db.exec(`
3000
+ CREATE TABLE IF NOT EXISTS model_prices (
3001
+ model_key TEXT PRIMARY KEY,
3002
+ provider TEXT NOT NULL DEFAULT '',
3003
+ input REAL NOT NULL,
3004
+ output REAL NOT NULL,
3005
+ cache_read REAL,
3006
+ cache_write REAL,
3007
+ currency TEXT NOT NULL DEFAULT 'USD',
3008
+ source TEXT NOT NULL DEFAULT 'manual',
3009
+ source_model_id TEXT,
3010
+ source_url TEXT,
3011
+ origin TEXT NOT NULL DEFAULT 'user',
3012
+ status TEXT NOT NULL DEFAULT 'active',
3013
+ last_synced_at INTEGER,
3014
+ created_at INTEGER NOT NULL,
3015
+ updated_at INTEGER NOT NULL
3016
+ );
3017
+
3018
+ CREATE TABLE IF NOT EXISTS model_price_aliases (
3019
+ alias TEXT PRIMARY KEY,
3020
+ model_key TEXT NOT NULL REFERENCES model_prices(model_key) ON DELETE CASCADE,
3021
+ match_type TEXT NOT NULL DEFAULT 'exact',
3022
+ provider TEXT NOT NULL DEFAULT '',
3023
+ priority INTEGER NOT NULL DEFAULT 100,
3024
+ source TEXT NOT NULL DEFAULT 'manual',
3025
+ origin TEXT NOT NULL DEFAULT 'user',
3026
+ enabled INTEGER NOT NULL DEFAULT 1,
3027
+ created_at INTEGER NOT NULL,
3028
+ updated_at INTEGER NOT NULL
3029
+ );
3030
+
3031
+ CREATE INDEX IF NOT EXISTS idx_model_prices_origin ON model_prices(origin);
3032
+ CREATE INDEX IF NOT EXISTS idx_model_prices_source ON model_prices(source);
3033
+ CREATE INDEX IF NOT EXISTS idx_model_price_aliases_model ON model_price_aliases(model_key);
3034
+
3035
+ INSERT INTO schema_version (version) VALUES (10);
3036
+ `);
3037
+ }
3038
+
3039
+ // src/db/migrations/v11.ts
3040
+ function migrateV11(db) {
3041
+ db.exec(`
3042
+ CREATE TABLE IF NOT EXISTS model_price_sync_baselines (
3043
+ model_key TEXT PRIMARY KEY,
3044
+ provider TEXT NOT NULL DEFAULT '',
3045
+ input REAL NOT NULL,
3046
+ output REAL NOT NULL,
3047
+ cache_read REAL,
3048
+ cache_write REAL,
3049
+ currency TEXT NOT NULL DEFAULT 'USD',
3050
+ source TEXT NOT NULL DEFAULT 'litellm',
3051
+ source_model_id TEXT,
3052
+ source_url TEXT,
3053
+ last_synced_at INTEGER NOT NULL,
3054
+ updated_at INTEGER NOT NULL
3055
+ );
3056
+
3057
+ INSERT INTO model_price_sync_baselines (
3058
+ model_key, provider, input, output, cache_read, cache_write, currency, source,
3059
+ source_model_id, source_url, last_synced_at, updated_at
3060
+ )
3061
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source,
3062
+ source_model_id, source_url, COALESCE(last_synced_at, updated_at), updated_at
3063
+ FROM model_prices
3064
+ WHERE origin = 'builtin' AND source != 'manual';
3065
+
3066
+ INSERT INTO schema_version (version) VALUES (11);
3067
+ `);
3068
+ }
3069
+
3135
3070
  // src/db/migrations/index.ts
3136
3071
  var MIGRATIONS = [
3137
3072
  { version: 1, migrate: migrateV1 },
@@ -3142,7 +3077,9 @@ var MIGRATIONS = [
3142
3077
  { version: 6, migrate: migrateV6 },
3143
3078
  { version: 7, migrate: migrateV7 },
3144
3079
  { version: 8, migrate: migrateV8 },
3145
- { version: 9, migrate: migrateV9 }
3080
+ { version: 9, migrate: migrateV9 },
3081
+ { version: 10, migrate: migrateV10 },
3082
+ { version: 11, migrate: migrateV11 }
3146
3083
  ];
3147
3084
  function runMigrations(db) {
3148
3085
  createSchemaVersionTable(db);
@@ -3159,14 +3096,592 @@ function runMigrations(db) {
3159
3096
  }
3160
3097
  }
3161
3098
 
3099
+ // src/pricing-registry.ts
3100
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
3101
+ function rowToPrice(row) {
3102
+ return {
3103
+ input: Number(row.input),
3104
+ output: Number(row.output),
3105
+ cacheRead: row.cache_read == null ? void 0 : Number(row.cache_read),
3106
+ cacheWrite: row.cache_write == null ? void 0 : Number(row.cache_write),
3107
+ currency: row.currency
3108
+ };
3109
+ }
3110
+ function normalizeProvider(provider) {
3111
+ return (provider ?? "").trim().toLowerCase();
3112
+ }
3113
+ function normalizeModelKey(sourceModelId) {
3114
+ const trimmed = sourceModelId.trim();
3115
+ const slash = trimmed.indexOf("/");
3116
+ return slash > 0 ? trimmed.slice(slash + 1) : trimmed;
3117
+ }
3118
+ function aliasesFor(sourceModelId, modelKey) {
3119
+ const aliases = /* @__PURE__ */ new Set([modelKey, sourceModelId]);
3120
+ if (modelKey.startsWith("claude-")) aliases.add(modelKey.replace(/-\d{8}$/, ""));
3121
+ return [...aliases].filter(Boolean);
3122
+ }
3123
+ function litellmEntryToPrice(sourceModelId, entry) {
3124
+ if (entry.mode && entry.mode !== "chat" && entry.mode !== "completion" && entry.mode !== "responses") return null;
3125
+ if (entry.input_cost_per_second != null || entry.output_cost_per_second != null) return null;
3126
+ if (typeof entry.input_cost_per_token !== "number" || typeof entry.output_cost_per_token !== "number") return null;
3127
+ const modelKey = normalizeModelKey(sourceModelId);
3128
+ if (!modelKey || modelKey.includes("*")) return null;
3129
+ return {
3130
+ modelKey,
3131
+ provider: normalizeProvider(entry.litellm_provider),
3132
+ price: {
3133
+ input: entry.input_cost_per_token * 1e6,
3134
+ output: entry.output_cost_per_token * 1e6,
3135
+ cacheRead: typeof entry.cache_read_input_token_cost === "number" ? entry.cache_read_input_token_cost * 1e6 : void 0,
3136
+ cacheWrite: typeof entry.cache_creation_input_token_cost === "number" ? entry.cache_creation_input_token_cost * 1e6 : void 0,
3137
+ currency: "USD"
3138
+ }
3139
+ };
3140
+ }
3141
+ function loadBuiltinRows(db) {
3142
+ return db.prepare(`
3143
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3144
+ source_url, origin, status, last_synced_at, updated_at
3145
+ FROM model_prices
3146
+ WHERE origin = 'builtin' AND status = 'active'
3147
+ `).all();
3148
+ }
3149
+ function loadUserRows(db) {
3150
+ return db.prepare(`
3151
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3152
+ source_url, origin, status, last_synced_at, updated_at
3153
+ FROM model_prices
3154
+ WHERE origin = 'user' AND status = 'active'
3155
+ `).all();
3156
+ }
3157
+ function loadSyncedBaselineRows(db) {
3158
+ return db.prepare(`
3159
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3160
+ source_url, last_synced_at, updated_at
3161
+ FROM model_price_sync_baselines
3162
+ `).all();
3163
+ }
3164
+ function loadAliasRows(db) {
3165
+ return db.prepare(`
3166
+ SELECT alias, model_key, match_type, provider, priority, source, origin, enabled
3167
+ FROM model_price_aliases
3168
+ WHERE enabled = 1
3169
+ ORDER BY priority ASC
3170
+ `).all();
3171
+ }
3172
+ function loadPricingRuntime(db, config) {
3173
+ const builtin = {};
3174
+ for (const row of loadBuiltinRows(db)) builtin[row.model_key] = rowToPrice(row);
3175
+ const overrides = {};
3176
+ const userRows = loadUserRows(db);
3177
+ const configOverrides = config?.priceOverrides ?? {};
3178
+ const activePrices = priceTableFromDb(db);
3179
+ for (const alias of loadAliasRows(db)) {
3180
+ const price = activePrices[alias.model_key];
3181
+ if (!price) continue;
3182
+ if (alias.origin === "user") overrides[alias.alias] = price;
3183
+ else builtin[alias.alias] = price;
3184
+ }
3185
+ for (const row of userRows) overrides[row.model_key] = rowToPrice(row);
3186
+ for (const [model, entry] of Object.entries(configOverrides)) overrides[model] = entry;
3187
+ setRuntimePriceTable(builtin, overrides);
3188
+ }
3189
+ function setUserPrice(db, modelKey, entry) {
3190
+ const now = Date.now();
3191
+ const baseline = db.prepare(`
3192
+ SELECT provider, source, source_model_id, source_url, last_synced_at
3193
+ FROM model_price_sync_baselines
3194
+ WHERE model_key = ?
3195
+ `).get(modelKey);
3196
+ db.prepare(`
3197
+ INSERT INTO model_prices (
3198
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3199
+ source_url, origin, status, last_synced_at, created_at, updated_at
3200
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'manual', ?, ?, 'user', 'active', ?, ?, ?)
3201
+ ON CONFLICT(model_key) DO UPDATE SET
3202
+ provider = excluded.provider,
3203
+ input = excluded.input,
3204
+ output = excluded.output,
3205
+ cache_read = excluded.cache_read,
3206
+ cache_write = excluded.cache_write,
3207
+ currency = excluded.currency,
3208
+ source = 'manual',
3209
+ source_model_id = excluded.source_model_id,
3210
+ source_url = excluded.source_url,
3211
+ origin = 'user',
3212
+ status = 'active',
3213
+ last_synced_at = excluded.last_synced_at,
3214
+ updated_at = excluded.updated_at
3215
+ `).run(
3216
+ modelKey,
3217
+ baseline?.provider ?? "",
3218
+ entry.input,
3219
+ entry.output,
3220
+ entry.cacheRead ?? null,
3221
+ entry.cacheWrite ?? null,
3222
+ entry.currency ?? "USD",
3223
+ baseline?.source_model_id ?? modelKey,
3224
+ baseline?.source_url ?? null,
3225
+ baseline?.last_synced_at ?? null,
3226
+ now,
3227
+ now
3228
+ );
3229
+ setPriceOverride(modelKey, entry);
3230
+ }
3231
+ function resetUserPriceToSynced(db, modelKey) {
3232
+ const baseline = db.prepare(`
3233
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3234
+ source_url, last_synced_at, updated_at
3235
+ FROM model_price_sync_baselines
3236
+ WHERE model_key = ?
3237
+ `).get(modelKey);
3238
+ if (!baseline) {
3239
+ removeUserPrice(db, modelKey);
3240
+ return;
3241
+ }
3242
+ const now = Date.now();
3243
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND model_key = ? AND origin = 'user'").run(modelKey, modelKey);
3244
+ db.prepare(`
3245
+ INSERT INTO model_prices (
3246
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3247
+ source_url, origin, status, last_synced_at, created_at, updated_at
3248
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'builtin', 'active', ?, ?, ?)
3249
+ ON CONFLICT(model_key) DO UPDATE SET
3250
+ provider = excluded.provider,
3251
+ input = excluded.input,
3252
+ output = excluded.output,
3253
+ cache_read = excluded.cache_read,
3254
+ cache_write = excluded.cache_write,
3255
+ currency = excluded.currency,
3256
+ source = excluded.source,
3257
+ source_model_id = excluded.source_model_id,
3258
+ source_url = excluded.source_url,
3259
+ origin = 'builtin',
3260
+ status = 'active',
3261
+ last_synced_at = excluded.last_synced_at,
3262
+ updated_at = excluded.updated_at
3263
+ `).run(
3264
+ baseline.model_key,
3265
+ baseline.provider,
3266
+ baseline.input,
3267
+ baseline.output,
3268
+ baseline.cache_read,
3269
+ baseline.cache_write,
3270
+ baseline.currency,
3271
+ baseline.source,
3272
+ baseline.source_model_id,
3273
+ baseline.source_url,
3274
+ baseline.last_synced_at,
3275
+ now,
3276
+ now
3277
+ );
3278
+ }
3279
+ function setUserPricingAlias(db, alias, modelKey) {
3280
+ const aliasKey = alias.trim();
3281
+ const targetKey = modelKey.trim();
3282
+ if (!aliasKey || !targetKey) throw new Error("alias and modelKey required");
3283
+ const target = db.prepare(`
3284
+ SELECT provider
3285
+ FROM model_prices
3286
+ WHERE model_key = ? AND status = 'active'
3287
+ `).get(targetKey);
3288
+ if (!target) throw new Error("Pricing model not found");
3289
+ const now = Date.now();
3290
+ db.prepare(`
3291
+ INSERT INTO model_price_aliases (alias, model_key, match_type, provider, priority, source, origin, enabled, created_at, updated_at)
3292
+ VALUES (?, ?, 'exact', ?, 200, 'manual', 'user', 1, ?, ?)
3293
+ ON CONFLICT(alias) DO UPDATE SET
3294
+ model_key = excluded.model_key,
3295
+ match_type = 'exact',
3296
+ provider = excluded.provider,
3297
+ priority = excluded.priority,
3298
+ source = 'manual',
3299
+ origin = 'user',
3300
+ enabled = 1,
3301
+ updated_at = excluded.updated_at
3302
+ `).run(aliasKey, targetKey, target.provider ?? "", now, now);
3303
+ }
3304
+ function removeUserPricingAlias(db, alias) {
3305
+ const aliasKey = alias.trim();
3306
+ if (!aliasKey) throw new Error("alias required");
3307
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND origin = 'user'").run(aliasKey);
3308
+ }
3309
+ function removeUserPrice(db, modelKey) {
3310
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND model_key = ? AND origin = 'user'").run(modelKey, modelKey);
3311
+ db.prepare("DELETE FROM model_prices WHERE model_key = ? AND origin = 'user'").run(modelKey);
3312
+ }
3313
+ function upsertBuiltinPrice(db, entry, now) {
3314
+ db.prepare(`
3315
+ INSERT INTO model_price_sync_baselines (
3316
+ model_key, provider, input, output, cache_read, cache_write, currency, source,
3317
+ source_model_id, source_url, last_synced_at, updated_at
3318
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'litellm', ?, ?, ?, ?)
3319
+ ON CONFLICT(model_key) DO UPDATE SET
3320
+ provider = excluded.provider,
3321
+ input = excluded.input,
3322
+ output = excluded.output,
3323
+ cache_read = excluded.cache_read,
3324
+ cache_write = excluded.cache_write,
3325
+ currency = excluded.currency,
3326
+ source = excluded.source,
3327
+ source_model_id = excluded.source_model_id,
3328
+ source_url = excluded.source_url,
3329
+ last_synced_at = excluded.last_synced_at,
3330
+ updated_at = excluded.updated_at
3331
+ `).run(
3332
+ entry.modelKey,
3333
+ entry.provider,
3334
+ entry.price.input,
3335
+ entry.price.output,
3336
+ entry.price.cacheRead ?? null,
3337
+ entry.price.cacheWrite ?? null,
3338
+ entry.price.currency ?? "USD",
3339
+ entry.sourceModelId,
3340
+ entry.sourceUrl,
3341
+ now,
3342
+ now
3343
+ );
3344
+ const existing = db.prepare("SELECT origin, input, output, cache_read, cache_write, currency FROM model_prices WHERE model_key = ?").get(entry.modelKey);
3345
+ if (existing?.origin === "user") return "user_preserved";
3346
+ const next = [entry.price.input, entry.price.output, entry.price.cacheRead ?? null, entry.price.cacheWrite ?? null, entry.price.currency ?? "USD"];
3347
+ if (!existing) {
3348
+ db.prepare(`
3349
+ INSERT INTO model_prices (
3350
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3351
+ source_url, origin, status, last_synced_at, created_at, updated_at
3352
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'litellm', ?, ?, 'builtin', 'active', ?, ?, ?)
3353
+ `).run(entry.modelKey, entry.provider, ...next, entry.sourceModelId, entry.sourceUrl, now, now, now);
3354
+ return "added";
3355
+ }
3356
+ const current = [Number(existing.input), Number(existing.output), existing.cache_read == null ? null : Number(existing.cache_read), existing.cache_write == null ? null : Number(existing.cache_write), existing.currency];
3357
+ const changed = current.some((value, index) => value !== next[index]);
3358
+ if (!changed) {
3359
+ db.prepare(`
3360
+ UPDATE model_prices SET provider = ?, source = 'litellm', source_model_id = ?, source_url = ?, last_synced_at = ?, updated_at = ?
3361
+ WHERE model_key = ? AND origin = 'builtin'
3362
+ `).run(entry.provider, entry.sourceModelId, entry.sourceUrl, now, now, entry.modelKey);
3363
+ return "unchanged";
3364
+ }
3365
+ db.prepare(`
3366
+ UPDATE model_prices
3367
+ SET provider = ?, input = ?, output = ?, cache_read = ?, cache_write = ?, currency = ?, source = 'litellm',
3368
+ source_model_id = ?, source_url = ?, status = 'active', last_synced_at = ?, updated_at = ?
3369
+ WHERE model_key = ? AND origin = 'builtin'
3370
+ `).run(entry.provider, ...next, entry.sourceModelId, entry.sourceUrl, now, now, entry.modelKey);
3371
+ return "updated";
3372
+ }
3373
+ function upsertBuiltinAlias(db, alias, modelKey, provider, source, now) {
3374
+ const existing = db.prepare("SELECT origin, model_key, enabled FROM model_price_aliases WHERE alias = ?").get(alias);
3375
+ if (existing?.origin === "user") return "user_preserved";
3376
+ if (!existing) {
3377
+ db.prepare(`
3378
+ INSERT INTO model_price_aliases (alias, model_key, match_type, provider, priority, source, origin, enabled, created_at, updated_at)
3379
+ VALUES (?, ?, 'exact', ?, 100, ?, 'builtin', 1, ?, ?)
3380
+ `).run(alias, modelKey, provider, source, now, now);
3381
+ return "added";
3382
+ }
3383
+ if (existing.model_key === modelKey && existing.enabled === 1) return "unchanged";
3384
+ db.prepare(`
3385
+ UPDATE model_price_aliases SET model_key = ?, provider = ?, source = ?, enabled = 1, updated_at = ?
3386
+ WHERE alias = ? AND origin = 'builtin'
3387
+ `).run(modelKey, provider, source, now, alias);
3388
+ return "updated";
3389
+ }
3390
+ async function syncPricingFromLitellm(db) {
3391
+ const response = await fetch(LITELLM_PRICING_URL, { headers: { Accept: "application/json" } });
3392
+ if (!response.ok) throw new Error(`LiteLLM pricing fetch failed: HTTP ${response.status}`);
3393
+ const data = await response.json();
3394
+ const now = Date.now();
3395
+ const summary = {
3396
+ source: "litellm",
3397
+ added: 0,
3398
+ updated: 0,
3399
+ unchanged: 0,
3400
+ skipped: 0,
3401
+ aliasesAdded: 0,
3402
+ aliasesUpdated: 0,
3403
+ userPreserved: 0,
3404
+ dryRun: { totalModels: 0, matched: 0, unresolved: [] }
3405
+ };
3406
+ const tx = db.transaction(() => {
3407
+ for (const [sourceModelId, rawEntry] of Object.entries(data)) {
3408
+ const parsed = litellmEntryToPrice(sourceModelId, rawEntry);
3409
+ if (!parsed) {
3410
+ summary.skipped++;
3411
+ continue;
3412
+ }
3413
+ const result = upsertBuiltinPrice(db, {
3414
+ modelKey: parsed.modelKey,
3415
+ provider: parsed.provider,
3416
+ price: parsed.price,
3417
+ sourceModelId,
3418
+ sourceUrl: LITELLM_PRICING_URL
3419
+ }, now);
3420
+ if (result === "user_preserved") summary.userPreserved++;
3421
+ else summary[result]++;
3422
+ for (const alias of aliasesFor(sourceModelId, parsed.modelKey)) {
3423
+ const aliasResult = upsertBuiltinAlias(db, alias, parsed.modelKey, parsed.provider, "litellm", now);
3424
+ if (aliasResult === "added") summary.aliasesAdded++;
3425
+ else if (aliasResult === "updated") summary.aliasesUpdated++;
3426
+ else if (aliasResult === "user_preserved") summary.userPreserved++;
3427
+ }
3428
+ }
3429
+ });
3430
+ tx();
3431
+ summary.dryRun = dryRunLocalModels(db);
3432
+ return summary;
3433
+ }
3434
+ function priceTableFromDb(db) {
3435
+ const table = {};
3436
+ const rows = db.prepare(`
3437
+ SELECT model_key, input, output, cache_read, cache_write, currency
3438
+ FROM model_prices WHERE status = 'active'
3439
+ `).all();
3440
+ for (const row of rows) table[row.model_key] = rowToPrice(row);
3441
+ return table;
3442
+ }
3443
+ function resolvePriceFromRegistry(db, model) {
3444
+ const directUserPrice = db.prepare(`
3445
+ SELECT input, output, cache_read, cache_write, currency
3446
+ FROM model_prices
3447
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3448
+ `).get(model);
3449
+ if (directUserPrice) return rowToPrice(directUserPrice);
3450
+ const exactAlias = db.prepare(`
3451
+ SELECT p.input, p.output, p.cache_read, p.cache_write, p.currency
3452
+ FROM model_price_aliases a
3453
+ JOIN model_prices p ON p.model_key = a.model_key
3454
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3455
+ ORDER BY a.priority DESC LIMIT 1
3456
+ `).get(model);
3457
+ if (exactAlias) return rowToPrice(exactAlias);
3458
+ return resolvePriceFromTable(model, priceTableFromDb(db));
3459
+ }
3460
+ function findMatch(db, model) {
3461
+ const directUserMatch = db.prepare(`
3462
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3463
+ source_url, origin, status, last_synced_at, updated_at
3464
+ FROM model_prices
3465
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3466
+ `).get(model);
3467
+ if (directUserMatch) return { price: rowToPrice(directUserMatch), row: directUserMatch, matchedBy: null };
3468
+ const aliasMatch = db.prepare(`
3469
+ SELECT p.model_key, p.provider, p.input, p.output, p.cache_read, p.cache_write, p.currency, p.source,
3470
+ p.source_model_id, p.source_url, p.origin, p.status, p.last_synced_at, p.updated_at, a.alias
3471
+ FROM model_price_aliases a
3472
+ JOIN model_prices p ON p.model_key = a.model_key
3473
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3474
+ ORDER BY a.priority DESC LIMIT 1
3475
+ `).get(model);
3476
+ if (aliasMatch) {
3477
+ const matchedBy = aliasMatch.alias === model ? aliasMatch.model_key === model ? null : aliasMatch.model_key : aliasMatch.alias;
3478
+ return { price: rowToPrice(aliasMatch), row: aliasMatch, matchedBy };
3479
+ }
3480
+ const rows = db.prepare(`
3481
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3482
+ source_url, origin, status, last_synced_at, updated_at
3483
+ FROM model_prices WHERE status = 'active'
3484
+ `).all();
3485
+ let best = null;
3486
+ for (const row of rows) {
3487
+ if (model === row.model_key) {
3488
+ best = row;
3489
+ break;
3490
+ }
3491
+ if (model.startsWith(row.model_key) && (!best || row.model_key.length > best.model_key.length)) best = row;
3492
+ }
3493
+ if (!best) return null;
3494
+ return { price: rowToPrice(best), row: best, matchedBy: best.model_key === model ? null : best.model_key };
3495
+ }
3496
+ function listPricingModels(db) {
3497
+ const dbModels = db.prepare("SELECT model, COUNT(*) as usage_count FROM records WHERE model != 'unknown' GROUP BY model ORDER BY usage_count DESC, model ASC").all();
3498
+ const knownModels = new Set(dbModels.map((row) => row.model));
3499
+ const userRows = loadUserRows(db);
3500
+ for (const row of userRows) knownModels.add(row.model_key);
3501
+ const syncedBaselines = new Set(loadSyncedBaselineRows(db).map((row) => row.model_key));
3502
+ return [...knownModels].map((model) => {
3503
+ const match = findMatch(db, model);
3504
+ if (!match) {
3505
+ return {
3506
+ model,
3507
+ provider: null,
3508
+ price: null,
3509
+ currency: "USD",
3510
+ origin: null,
3511
+ source: null,
3512
+ sourceModelId: null,
3513
+ lastSyncedAt: null,
3514
+ isBuiltin: false,
3515
+ isOverride: false,
3516
+ isDefault: false,
3517
+ hasSyncedBaseline: syncedBaselines.has(model),
3518
+ matchedBy: null
3519
+ };
3520
+ }
3521
+ return {
3522
+ model,
3523
+ provider: match.row.provider || null,
3524
+ price: match.price,
3525
+ currency: match.price.currency ?? "USD",
3526
+ origin: match.row.origin,
3527
+ source: match.row.source,
3528
+ sourceModelId: match.row.source_model_id,
3529
+ lastSyncedAt: match.row.last_synced_at,
3530
+ isBuiltin: match.row.origin === "builtin",
3531
+ isOverride: match.row.origin === "user",
3532
+ isDefault: match.row.origin === "builtin",
3533
+ hasSyncedBaseline: syncedBaselines.has(match.row.model_key),
3534
+ matchedBy: match.matchedBy
3535
+ };
3536
+ });
3537
+ }
3538
+ function getPricingRegistrySummary(db) {
3539
+ const priceCounts = db.prepare(`
3540
+ SELECT
3541
+ COUNT(*) AS totalPrices,
3542
+ SUM(CASE WHEN origin = 'builtin' THEN 1 ELSE 0 END) AS builtinPrices,
3543
+ SUM(CASE WHEN origin = 'user' THEN 1 ELSE 0 END) AS userPrices,
3544
+ MAX(last_synced_at) AS lastSyncedAt
3545
+ FROM model_prices
3546
+ WHERE status = 'active'
3547
+ `).get();
3548
+ const aliasCounts = db.prepare(`
3549
+ SELECT COUNT(*) AS activeAliases
3550
+ FROM model_price_aliases
3551
+ WHERE enabled = 1
3552
+ `).get();
3553
+ const dryRun = dryRunLocalModels(db);
3554
+ return {
3555
+ totalPrices: Number(priceCounts.totalPrices ?? 0),
3556
+ builtinPrices: Number(priceCounts.builtinPrices ?? 0),
3557
+ userPrices: Number(priceCounts.userPrices ?? 0),
3558
+ activeAliases: Number(aliasCounts.activeAliases ?? 0),
3559
+ lastSyncedAt: priceCounts.lastSyncedAt == null ? null : Number(priceCounts.lastSyncedAt),
3560
+ localModels: dryRun.totalModels,
3561
+ matchedLocalModels: dryRun.matched,
3562
+ unresolvedLocalModels: dryRun.unresolved
3563
+ };
3564
+ }
3565
+ function listPricingAliasTargets(db) {
3566
+ const rows = db.prepare(`
3567
+ SELECT model_key, provider, origin, source, source_model_id, currency
3568
+ FROM model_prices
3569
+ WHERE status = 'active'
3570
+ ORDER BY origin DESC, provider ASC, model_key ASC
3571
+ `).all();
3572
+ return rows.map((row) => ({
3573
+ model: row.model_key,
3574
+ provider: row.provider,
3575
+ origin: row.origin,
3576
+ source: row.source,
3577
+ sourceModelId: row.source_model_id,
3578
+ currency: row.currency
3579
+ }));
3580
+ }
3581
+ function listLocalModelBindings(db) {
3582
+ const rows = db.prepare("SELECT DISTINCT model FROM records WHERE model != 'unknown' ORDER BY model ASC").all();
3583
+ const priceRows = db.prepare(`
3584
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3585
+ source_url, origin, status, last_synced_at, updated_at
3586
+ FROM model_prices WHERE status = 'active'
3587
+ `).all();
3588
+ return rows.map(({ model }) => {
3589
+ const directUserPrice = db.prepare(`
3590
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3591
+ source_url, origin, status, last_synced_at, updated_at
3592
+ FROM model_prices
3593
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3594
+ `).get(model);
3595
+ const aliasMatch = db.prepare(`
3596
+ SELECT a.alias, a.model_key AS modelKey, a.origin, a.source, p.origin AS priceOrigin
3597
+ FROM model_price_aliases a
3598
+ JOIN model_prices p ON p.model_key = a.model_key
3599
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3600
+ ORDER BY a.priority DESC LIMIT 1
3601
+ `).get(model);
3602
+ if (directUserPrice) {
3603
+ const hasManualBinding = Boolean(aliasMatch && aliasMatch.origin === "user" && !(aliasMatch.alias === model && aliasMatch.modelKey === model));
3604
+ return {
3605
+ model,
3606
+ modelKey: hasManualBinding ? aliasMatch.modelKey : directUserPrice.model_key,
3607
+ origin: hasManualBinding ? aliasMatch.origin : directUserPrice.origin,
3608
+ priceOrigin: directUserPrice.origin,
3609
+ source: directUserPrice.source,
3610
+ matchType: "exact",
3611
+ bindingType: "custom",
3612
+ hasManualBinding,
3613
+ hasPrice: true
3614
+ };
3615
+ }
3616
+ if (aliasMatch) {
3617
+ const selfAlias = aliasMatch.alias === model && aliasMatch.modelKey === model;
3618
+ const hasManualBinding = aliasMatch.origin === "user" && !selfAlias;
3619
+ return {
3620
+ model,
3621
+ modelKey: aliasMatch.modelKey,
3622
+ origin: aliasMatch.origin,
3623
+ priceOrigin: aliasMatch.priceOrigin,
3624
+ source: aliasMatch.source,
3625
+ matchType: selfAlias ? "exact" : "alias",
3626
+ bindingType: aliasMatch.priceOrigin === "user" ? "custom" : hasManualBinding ? "manual" : "automatic",
3627
+ hasManualBinding,
3628
+ hasPrice: true
3629
+ };
3630
+ }
3631
+ let best = null;
3632
+ let matchType = null;
3633
+ for (const row of priceRows) {
3634
+ if (model === row.model_key) {
3635
+ best = row;
3636
+ matchType = "exact";
3637
+ break;
3638
+ }
3639
+ if (model.startsWith(row.model_key) && (!best || row.model_key.length > best.model_key.length)) {
3640
+ best = row;
3641
+ matchType = "prefix";
3642
+ }
3643
+ }
3644
+ return {
3645
+ model,
3646
+ modelKey: best?.model_key ?? null,
3647
+ origin: best?.origin ?? null,
3648
+ priceOrigin: best?.origin ?? null,
3649
+ source: best?.source ?? null,
3650
+ matchType,
3651
+ bindingType: best?.origin === "user" ? "custom" : best ? "automatic" : "none",
3652
+ hasManualBinding: false,
3653
+ hasPrice: Boolean(best)
3654
+ };
3655
+ });
3656
+ }
3657
+ function dryRunLocalModels(db) {
3658
+ const models = db.prepare("SELECT DISTINCT model FROM records WHERE model != 'unknown' ORDER BY model ASC").all();
3659
+ const unresolved = [];
3660
+ let matched = 0;
3661
+ for (const row of models) {
3662
+ if (resolvePriceFromRegistry(db, row.model)) matched++;
3663
+ else unresolved.push(row.model);
3664
+ }
3665
+ return { totalModels: models.length, matched, unresolved: unresolved.slice(0, 50) };
3666
+ }
3667
+ function getUserAliasBindings(db) {
3668
+ return db.prepare(`
3669
+ SELECT alias, model_key AS modelKey, origin, source
3670
+ FROM model_price_aliases
3671
+ WHERE origin = 'user' AND enabled = 1
3672
+ ORDER BY alias ASC
3673
+ `).all();
3674
+ }
3675
+
3162
3676
  // src/db/index.ts
3163
3677
  function initializeDatabase(db) {
3164
3678
  applyPragmas(db);
3165
3679
  runMigrations(db);
3680
+ loadPricingRuntime(db, loadConfig());
3166
3681
  }
3167
- function createDatabase(path2) {
3168
- mkdirSync3(dirname2(path2), { recursive: true, mode: 448 });
3169
- const db = new Database(path2);
3682
+ function createDatabase(path3) {
3683
+ mkdirSync4(dirname3(path3), { recursive: true, mode: 448 });
3684
+ const db = new Database(path3);
3170
3685
  initializeDatabase(db);
3171
3686
  return db;
3172
3687
  }
@@ -3364,10 +3879,10 @@ function getClientPlatform() {
3364
3879
  }
3365
3880
  function getCliVersion() {
3366
3881
  try {
3367
- const { readFileSync: readFileSync11 } = __require("fs");
3882
+ const { readFileSync: readFileSync12 } = __require("fs");
3368
3883
  const { join: join16 } = __require("path");
3369
3884
  const pkgPath = join16(__dirname, "../../package.json");
3370
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3885
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3371
3886
  return pkg.version || "0.0.0";
3372
3887
  } catch {
3373
3888
  return "0.0.0";
@@ -3645,102 +4160,475 @@ Summary: ${accepted} accepted, ${flagged} flagged, ${rejected} rejected`);
3645
4160
  }
3646
4161
  }
3647
4162
 
3648
- // src/api/server.ts
3649
- var pendingLeaderboardAuth = /* @__PURE__ */ new Map();
3650
- function recalcCosts(db) {
3651
- const BATCH_SIZE2 = 1e3;
3652
- let updated = 0;
3653
- let lastId = "";
3654
- const exchangeRate = resolveExchangeRate(loadConfig() ?? {});
3655
- while (true) {
3656
- const records = db.prepare(
3657
- "SELECT id, tool, model, provider, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, thinking_tokens, cost, cost_source FROM records WHERE id > ? ORDER BY id LIMIT ?"
3658
- ).all(lastId, BATCH_SIZE2);
3659
- if (records.length === 0) break;
3660
- const updateStmt = db.prepare("UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?");
3661
- const tx = db.transaction((batch) => {
3662
- for (const r of batch) {
3663
- if (r.cost_source === "log") continue;
3664
- const rawModel = r.tool === "qoder" ? normalizeQoderModel(r.model) : r.model;
3665
- const model = rawModel === "unknown" ? r.tool === "qoder" ? "qoder-auto" : r.model : rawModel;
3666
- const provider = model !== r.model ? inferProvider(model) : r.provider;
3667
- const hasPrice = resolvePrice(model) != null;
3668
- const cost = hasPrice ? calculateCost(model, {
3669
- inputTokens: r.input_tokens,
3670
- outputTokens: r.output_tokens,
3671
- cacheReadTokens: r.cache_read_tokens,
3672
- cacheWriteTokens: r.cache_write_tokens,
3673
- thinkingTokens: r.thinking_tokens
3674
- }, exchangeRate) : 0;
3675
- const costSource = hasPrice ? "pricing" : "unknown";
3676
- if (model === r.model && provider === r.provider && cost === r.cost && costSource === r.cost_source) continue;
3677
- updateStmt.run(model, provider, cost, costSource, Date.now(), r.id);
3678
- updated++;
3679
- }
3680
- });
3681
- tx(records);
3682
- lastId = records[records.length - 1].id;
3683
- }
3684
- return updated;
4163
+ // src/commands/parse-kelivo.ts
4164
+ import { readFileSync as readFileSync6 } from "fs";
4165
+ import { basename as basename2 } from "path";
4166
+ import * as yauzl from "yauzl";
4167
+ function toTokenCount(value) {
4168
+ const n = Number(value);
4169
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
3685
4170
  }
3686
- function getDateRangeFilter(range, from, to, prefix = "", weekStart = 1) {
3687
- const ts3 = prefix ? `${prefix}.ts` : "ts";
3688
- if (from && to) {
3689
- const startMs = new Date(from).getTime();
3690
- const endMs = (/* @__PURE__ */ new Date(to + "T23:59:59.999Z")).getTime();
3691
- return { where: `AND ${ts3} >= @start AND ${ts3} < @end`, params: { start: startMs, end: endMs } };
3692
- }
3693
- const now = /* @__PURE__ */ new Date();
3694
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
3695
- if (range === "week") {
3696
- const dayOfWeek = today.getDay();
3697
- const diff = (dayOfWeek - weekStart + 7) % 7;
3698
- const startOfWeek = new Date(today);
3699
- startOfWeek.setDate(today.getDate() - diff);
3700
- return { where: `AND ${ts3} >= @start`, params: { start: startOfWeek.getTime() } };
3701
- }
3702
- if (range === "month") {
3703
- const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
3704
- return { where: `AND ${ts3} >= @start`, params: { start: startOfMonth.getTime() } };
3705
- }
3706
- if (range === "last30") {
3707
- const start = new Date(today);
3708
- start.setDate(start.getDate() - 30);
3709
- return { where: `AND ${ts3} >= @start`, params: { start: start.getTime() } };
3710
- }
3711
- if (range === "all") {
3712
- return { where: "", params: {} };
4171
+ function parseTimestamp(value, fallback) {
4172
+ if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
4173
+ if (typeof value === "string" && value.trim()) {
4174
+ const parsed = new Date(value).getTime();
4175
+ if (Number.isFinite(parsed)) return parsed;
3713
4176
  }
3714
- return { where: `AND ${ts3} >= @start`, params: { start: today.getTime() } };
3715
- }
3716
- function json(res, data, status = 200) {
3717
- res.writeHead(status, { "Content-Type": "application/json" });
3718
- res.end(JSON.stringify(data));
3719
- }
3720
- async function readJsonBody(req) {
3721
- let body = "";
3722
- for await (const chunk of req) body += chunk;
3723
- if (!body.trim()) return {};
3724
- return JSON.parse(body);
4177
+ return fallback;
3725
4178
  }
3726
- function getDeviceName() {
3727
- return loadConfig()?.device || hostname() || `${platform3()}-${process.arch}`;
4179
+ async function readZipEntryText(filePath, entryName) {
4180
+ return new Promise((resolve2, reject) => {
4181
+ yauzl.open(filePath, { lazyEntries: true }, (openError, zipfile) => {
4182
+ if (openError) {
4183
+ reject(openError);
4184
+ return;
4185
+ }
4186
+ if (!zipfile) {
4187
+ reject(new Error("Unable to open zip file"));
4188
+ return;
4189
+ }
4190
+ let settled = false;
4191
+ zipfile.readEntry();
4192
+ zipfile.on("entry", (entry) => {
4193
+ if (basename2(entry.fileName) !== entryName) {
4194
+ zipfile.readEntry();
4195
+ return;
4196
+ }
4197
+ zipfile.openReadStream(entry, (streamError, stream) => {
4198
+ if (streamError) {
4199
+ settled = true;
4200
+ zipfile.close();
4201
+ reject(streamError);
4202
+ return;
4203
+ }
4204
+ if (!stream) {
4205
+ settled = true;
4206
+ zipfile.close();
4207
+ reject(new Error(`Missing ${entryName} stream`));
4208
+ return;
4209
+ }
4210
+ const chunks = [];
4211
+ stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
4212
+ stream.on("error", (e) => {
4213
+ settled = true;
4214
+ zipfile.close();
4215
+ reject(e);
4216
+ });
4217
+ stream.on("end", () => {
4218
+ settled = true;
4219
+ zipfile.close();
4220
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
4221
+ });
4222
+ });
4223
+ });
4224
+ zipfile.on("end", () => {
4225
+ if (!settled) reject(new Error(`${entryName} not found in zip`));
4226
+ });
4227
+ zipfile.on("error", (e) => {
4228
+ if (!settled) reject(e);
4229
+ });
4230
+ });
4231
+ });
3728
4232
  }
3729
- function getLeaderboardFallback(periodType) {
3730
- return {
3731
- entries: [],
3732
- next_cursor: null,
3733
- current_user: null,
3734
- period_type: periodType,
3735
- period_start: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
3736
- source_status: "unavailable"
3737
- };
4233
+ async function readKelivoExport(filePath) {
4234
+ const text = filePath.endsWith(".zip") ? await readZipEntryText(filePath, "chats.json") : readFileSync6(filePath, "utf-8");
4235
+ return JSON.parse(text);
3738
4236
  }
3739
- async function proxyLeaderboard(res, url) {
3740
- const periodType = url.searchParams.get("period_type") || "daily";
3741
- const upstreamBase = getSiteUrl();
3742
- const upstream = new URL("/api/leaderboard", upstreamBase);
3743
- upstream.searchParams.set("period_type", periodType);
4237
+ async function runParseKelivo(options) {
4238
+ const { filePath, device, deviceInstanceId, platform: platform5, now, exchangeRate } = options;
4239
+ const records = [];
4240
+ const errors = [];
4241
+ let parsed;
4242
+ try {
4243
+ parsed = await readKelivoExport(filePath);
4244
+ } catch (e) {
4245
+ return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
4246
+ }
4247
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
4248
+ for (const [index, message] of messages.entries()) {
4249
+ if (message?.role !== "assistant") continue;
4250
+ const promptTokens = toTokenCount(message.promptTokens);
4251
+ const completionTokens = toTokenCount(message.completionTokens);
4252
+ const cachedTokens = toTokenCount(message.cachedTokens);
4253
+ const totalTokens = toTokenCount(message.totalTokens);
4254
+ const inputTokens = promptTokens > 0 ? Math.max(0, promptTokens - cachedTokens) : Math.max(0, totalTokens - completionTokens - cachedTokens);
4255
+ const outputTokens = completionTokens;
4256
+ const cacheReadTokens = cachedTokens;
4257
+ const cacheWriteTokens = 0;
4258
+ const thinkingTokens = 0;
4259
+ if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
4260
+ const model = typeof message.modelId === "string" && message.modelId.trim() ? message.modelId.trim() : "unknown";
4261
+ const provider = typeof message.providerId === "string" && message.providerId.trim() ? message.providerId.trim() : inferProvider(model);
4262
+ const ts3 = parseTimestamp(message.timestamp, now);
4263
+ const sessionId = typeof message.conversationId === "string" && message.conversationId.trim() ? message.conversationId.trim() : "unknown";
4264
+ const messageId = typeof message.id === "string" && message.id.trim() ? message.id.trim() : `${sessionId}:${ts3}:${index}`;
4265
+ const sourceKey = `kelivo:${sessionId}:${messageId}`;
4266
+ const tokenArgs = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, thinkingTokens };
4267
+ const cost = model !== "unknown" ? calculateCost(model, tokenArgs, exchangeRate) : 0;
4268
+ records.push({
4269
+ id: generateRecordId(deviceInstanceId, sourceKey, ts3),
4270
+ ts: ts3,
4271
+ ingestedAt: now,
4272
+ updatedAt: now,
4273
+ lineOffset: index,
4274
+ tool: "kelivo",
4275
+ model,
4276
+ provider,
4277
+ inputTokens,
4278
+ outputTokens,
4279
+ cacheReadTokens,
4280
+ cacheWriteTokens,
4281
+ thinkingTokens,
4282
+ cost,
4283
+ costSource: cost > 0 ? "pricing" : "unknown",
4284
+ sessionId,
4285
+ sourceFile: filePath,
4286
+ device,
4287
+ deviceInstanceId,
4288
+ platform: platform5
4289
+ });
4290
+ }
4291
+ return { records, errors };
4292
+ }
4293
+
4294
+ // src/db/records.ts
4295
+ function insertRecord(db, record) {
4296
+ db.prepare(`
4297
+ INSERT OR REPLACE INTO records (
4298
+ id, ts, ingested_at, synced_at, updated_at, line_offset,
4299
+ tool, model, provider, input_tokens, output_tokens,
4300
+ cache_read_tokens, cache_write_tokens, thinking_tokens,
4301
+ cost, cost_source, session_id, source_file, cwd, device, device_instance_id, platform
4302
+ ) VALUES (
4303
+ @id, @ts, @ingestedAt, @syncedAt, @updatedAt, @lineOffset,
4304
+ @tool, @model, @provider, @inputTokens, @outputTokens,
4305
+ @cacheReadTokens, @cacheWriteTokens, @thinkingTokens,
4306
+ @cost, @costSource, @sessionId, @sourceFile, @cwd, @device, @deviceInstanceId, @platform
4307
+ )
4308
+ `).run({
4309
+ id: record.id,
4310
+ ts: record.ts,
4311
+ ingestedAt: record.ingestedAt,
4312
+ syncedAt: record.syncedAt ?? null,
4313
+ updatedAt: record.updatedAt,
4314
+ lineOffset: record.lineOffset,
4315
+ tool: record.tool,
4316
+ model: record.model,
4317
+ provider: record.provider,
4318
+ inputTokens: record.inputTokens,
4319
+ outputTokens: record.outputTokens,
4320
+ cacheReadTokens: record.cacheReadTokens,
4321
+ cacheWriteTokens: record.cacheWriteTokens,
4322
+ thinkingTokens: record.thinkingTokens,
4323
+ cost: record.cost,
4324
+ costSource: record.costSource,
4325
+ sessionId: record.sessionId,
4326
+ sourceFile: record.sourceFile,
4327
+ cwd: record.cwd ?? "",
4328
+ device: record.device,
4329
+ deviceInstanceId: record.deviceInstanceId,
4330
+ platform: record.platform ?? ""
4331
+ });
4332
+ }
4333
+ function getUnsyncedRecords(db, target) {
4334
+ const rows = target ? db.prepare(`
4335
+ SELECT r.* FROM records r
4336
+ LEFT JOIN sync_record_state s
4337
+ ON s.record_id = r.id AND s.target = ?
4338
+ WHERE r.source_file NOT LIKE 'synced/%'
4339
+ AND (s.synced_at IS NULL OR r.updated_at > s.synced_at)
4340
+ `).all(target) : db.prepare(
4341
+ "SELECT * FROM records WHERE synced_at IS NULL OR updated_at > synced_at"
4342
+ ).all();
4343
+ return rows.map(mapRowToRecord);
4344
+ }
4345
+ function markRecordsSynced(db, ids, syncedAt, target) {
4346
+ if (ids.length === 0) return;
4347
+ if (target) {
4348
+ const insertStmt = db.prepare(`
4349
+ INSERT INTO sync_record_state (record_id, target, synced_at)
4350
+ VALUES (?, ?, ?)
4351
+ ON CONFLICT(record_id, target) DO UPDATE SET synced_at = excluded.synced_at
4352
+ `);
4353
+ const legacyStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
4354
+ const tx2 = db.transaction((recordIds) => {
4355
+ for (const id of recordIds) {
4356
+ insertStmt.run(id, target, syncedAt);
4357
+ legacyStmt.run(syncedAt, id);
4358
+ }
4359
+ });
4360
+ tx2(ids);
4361
+ return;
4362
+ }
4363
+ const updateStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
4364
+ const tx = db.transaction((recordIds) => {
4365
+ for (const id of recordIds) updateStmt.run(syncedAt, id);
4366
+ });
4367
+ tx(ids);
4368
+ }
4369
+ function mapRowToRecord(row) {
4370
+ return {
4371
+ id: row.id,
4372
+ ts: row.ts,
4373
+ ingestedAt: row.ingested_at,
4374
+ syncedAt: row.synced_at != null ? row.synced_at : void 0,
4375
+ updatedAt: row.updated_at,
4376
+ lineOffset: row.line_offset,
4377
+ tool: row.tool,
4378
+ model: row.model,
4379
+ provider: row.provider,
4380
+ inputTokens: row.input_tokens,
4381
+ outputTokens: row.output_tokens,
4382
+ cacheReadTokens: row.cache_read_tokens,
4383
+ cacheWriteTokens: row.cache_write_tokens,
4384
+ thinkingTokens: row.thinking_tokens,
4385
+ cost: row.cost,
4386
+ costSource: row.cost_source,
4387
+ sessionId: row.session_id,
4388
+ sourceFile: row.source_file,
4389
+ cwd: row.cwd || void 0,
4390
+ device: row.device,
4391
+ deviceInstanceId: row.device_instance_id,
4392
+ platform: row.platform || void 0
4393
+ };
4394
+ }
4395
+
4396
+ // src/db/write-queue.ts
4397
+ var AsyncTaskQueue = class {
4398
+ tail = Promise.resolve();
4399
+ running = false;
4400
+ pending = 0;
4401
+ run(task) {
4402
+ this.pending++;
4403
+ const runTask = async () => {
4404
+ this.pending--;
4405
+ this.running = true;
4406
+ try {
4407
+ return await task();
4408
+ } finally {
4409
+ this.running = false;
4410
+ }
4411
+ };
4412
+ const result = this.tail.then(runTask, runTask);
4413
+ this.tail = result.catch(() => void 0);
4414
+ return result;
4415
+ }
4416
+ getStatus() {
4417
+ return { running: this.running, pending: this.pending };
4418
+ }
4419
+ };
4420
+
4421
+ // src/api/server.ts
4422
+ var pendingLeaderboardAuth = /* @__PURE__ */ new Map();
4423
+ function getManualImportMetadata(db) {
4424
+ const rows = db.prepare(`
4425
+ SELECT tool, MAX(ingested_at) AS lastImportedAt
4426
+ FROM records
4427
+ WHERE tool IN ('kelivo')
4428
+ GROUP BY tool
4429
+ `).all();
4430
+ const metadata = {};
4431
+ for (const row of rows) {
4432
+ if (typeof row.lastImportedAt === "number") {
4433
+ metadata[row.tool] = { lastImportedAt: row.lastImportedAt };
4434
+ }
4435
+ }
4436
+ return metadata;
4437
+ }
4438
+ function attachManualImportMetadata(tools, db) {
4439
+ const metadata = getManualImportMetadata(db);
4440
+ return tools.map((tool) => {
4441
+ const toolMetadata = metadata[tool.tool];
4442
+ return toolMetadata ? { ...tool, ...toolMetadata } : tool;
4443
+ });
4444
+ }
4445
+ function countExistingRecordIds(db, ids) {
4446
+ const uniqueIds = [...new Set(ids)];
4447
+ if (uniqueIds.length === 0) return 0;
4448
+ const stmt = db.prepare("SELECT 1 FROM records WHERE id = ? LIMIT 1");
4449
+ let count = 0;
4450
+ for (const id of uniqueIds) {
4451
+ if (stmt.get(id)) count++;
4452
+ }
4453
+ return count;
4454
+ }
4455
+ function emptyPricingRecalcStatus() {
4456
+ return {
4457
+ state: "idle",
4458
+ needsRecalc: false,
4459
+ needsRecalcSince: null,
4460
+ total: 0,
4461
+ processed: 0,
4462
+ updated: 0,
4463
+ skipped: 0,
4464
+ queueRunning: false,
4465
+ queuePending: 0,
4466
+ startedAt: null,
4467
+ finishedAt: null,
4468
+ error: null
4469
+ };
4470
+ }
4471
+ async function recalcCosts(db, onProgress) {
4472
+ const BATCH_SIZE2 = 1e3;
4473
+ let updated = 0;
4474
+ let processed = 0;
4475
+ let skipped = 0;
4476
+ let lastId = "";
4477
+ const exchangeRate = resolveExchangeRate(loadConfig() ?? {});
4478
+ const priceCache = /* @__PURE__ */ new Map();
4479
+ const resolveCachedPrice = (model) => {
4480
+ if (priceCache.has(model)) return priceCache.get(model) ?? void 0;
4481
+ const price = resolvePriceFromRegistry(db, model) ?? null;
4482
+ priceCache.set(model, price);
4483
+ return price ?? void 0;
4484
+ };
4485
+ const totalRow = db.prepare("SELECT COUNT(*) AS total FROM records").get();
4486
+ const total = totalRow.total;
4487
+ onProgress?.({ total, processed, updated, skipped });
4488
+ while (true) {
4489
+ const records = db.prepare(
4490
+ "SELECT id, tool, model, provider, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, thinking_tokens, cost, cost_source FROM records WHERE id > ? ORDER BY id LIMIT ?"
4491
+ ).all(lastId, BATCH_SIZE2);
4492
+ if (records.length === 0) break;
4493
+ const updateStmt = db.prepare("UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?");
4494
+ const tx = db.transaction((batch) => {
4495
+ for (const r of batch) {
4496
+ processed++;
4497
+ if (r.cost_source === "log") {
4498
+ skipped++;
4499
+ continue;
4500
+ }
4501
+ const rawModel = r.tool === "qoder" ? normalizeQoderModel(r.model) : r.model;
4502
+ const model = rawModel === "unknown" ? r.tool === "qoder" ? "qoder-auto" : r.model : rawModel;
4503
+ const provider = model !== r.model ? inferProvider(model) : r.provider;
4504
+ const price = resolveCachedPrice(model);
4505
+ const cost = price ? calculateCostForPrice(price, {
4506
+ inputTokens: r.input_tokens,
4507
+ outputTokens: r.output_tokens,
4508
+ cacheReadTokens: r.cache_read_tokens,
4509
+ cacheWriteTokens: r.cache_write_tokens,
4510
+ thinkingTokens: r.thinking_tokens
4511
+ }, exchangeRate) : 0;
4512
+ const costSource = price ? "pricing" : "unknown";
4513
+ if (model === r.model && provider === r.provider && cost === r.cost && costSource === r.cost_source) continue;
4514
+ updateStmt.run(model, provider, cost, costSource, Date.now(), r.id);
4515
+ updated++;
4516
+ }
4517
+ });
4518
+ tx(records);
4519
+ lastId = records[records.length - 1].id;
4520
+ onProgress?.({ total, processed, updated, skipped });
4521
+ await new Promise((resolve2) => setImmediate(resolve2));
4522
+ }
4523
+ return updated;
4524
+ }
4525
+ function getDateRangeFilter(range, from, to, prefix = "", weekStart = 1) {
4526
+ const ts3 = prefix ? `${prefix}.ts` : "ts";
4527
+ if (from && to) {
4528
+ const startMs = new Date(from).getTime();
4529
+ const endMs = (/* @__PURE__ */ new Date(to + "T23:59:59.999Z")).getTime();
4530
+ return { where: `AND ${ts3} >= @start AND ${ts3} < @end`, params: { start: startMs, end: endMs } };
4531
+ }
4532
+ const now = /* @__PURE__ */ new Date();
4533
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
4534
+ if (range === "week") {
4535
+ const dayOfWeek = today.getDay();
4536
+ const diff = (dayOfWeek - weekStart + 7) % 7;
4537
+ const startOfWeek = new Date(today);
4538
+ startOfWeek.setDate(today.getDate() - diff);
4539
+ return { where: `AND ${ts3} >= @start`, params: { start: startOfWeek.getTime() } };
4540
+ }
4541
+ if (range === "month") {
4542
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
4543
+ return { where: `AND ${ts3} >= @start`, params: { start: startOfMonth.getTime() } };
4544
+ }
4545
+ if (range === "last30") {
4546
+ const start = new Date(today);
4547
+ start.setDate(start.getDate() - 30);
4548
+ return { where: `AND ${ts3} >= @start`, params: { start: start.getTime() } };
4549
+ }
4550
+ if (range === "all") {
4551
+ return { where: "", params: {} };
4552
+ }
4553
+ return { where: `AND ${ts3} >= @start`, params: { start: today.getTime() } };
4554
+ }
4555
+ function json(res, data, status = 200) {
4556
+ res.writeHead(status, { "Content-Type": "application/json" });
4557
+ res.end(JSON.stringify(data));
4558
+ }
4559
+ function isDatabaseLockedError(error) {
4560
+ return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
4561
+ }
4562
+ function databaseBusy(res) {
4563
+ json(res, {
4564
+ error: {
4565
+ code: "DATABASE_BUSY",
4566
+ message: "Database is busy. Wait for the current refresh, sync, or import to finish and retry."
4567
+ }
4568
+ }, 503);
4569
+ }
4570
+ async function readJsonBody(req) {
4571
+ let body = "";
4572
+ for await (const chunk of req) body += chunk;
4573
+ if (!body.trim()) return {};
4574
+ return JSON.parse(body);
4575
+ }
4576
+ async function readBodyBuffer(req) {
4577
+ const chunks = [];
4578
+ for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4579
+ return Buffer.concat(chunks);
4580
+ }
4581
+ function multipartBoundary(contentType) {
4582
+ const value = Array.isArray(contentType) ? contentType[0] : contentType;
4583
+ if (!value) return null;
4584
+ const match = value.match(/(?:^|;)\s*boundary=("([^"]+)"|[^;]+)/i);
4585
+ if (!match) return null;
4586
+ return (match[2] ?? match[1]).replace(/^"|"$/g, "");
4587
+ }
4588
+ async function readMultipartFile(req) {
4589
+ const boundary = multipartBoundary(req.headers["content-type"]);
4590
+ if (!boundary) return null;
4591
+ const body = await readBodyBuffer(req);
4592
+ const marker = Buffer.from(`--${boundary}`);
4593
+ let position = body.indexOf(marker);
4594
+ while (position !== -1) {
4595
+ position += marker.length;
4596
+ if (body.subarray(position, position + 2).toString() === "--") return null;
4597
+ if (body.subarray(position, position + 2).toString() === "\r\n") position += 2;
4598
+ const headerEnd = body.indexOf(Buffer.from("\r\n\r\n"), position);
4599
+ if (headerEnd === -1) return null;
4600
+ const headers = body.subarray(position, headerEnd).toString("utf-8");
4601
+ const filenameMatch = headers.match(/Content-Disposition:[^\r\n]*\bname="file"[^\r\n]*\bfilename="([^"]+)"/i);
4602
+ const dataStart = headerEnd + 4;
4603
+ const nextBoundary = body.indexOf(Buffer.from(`\r
4604
+ --${boundary}`), dataStart);
4605
+ if (nextBoundary === -1) return null;
4606
+ if (filenameMatch) return { filename: path2.basename(filenameMatch[1]), data: body.subarray(dataStart, nextBoundary) };
4607
+ position = body.indexOf(marker, nextBoundary);
4608
+ }
4609
+ return null;
4610
+ }
4611
+ function isKelivoBackupFilename(filename) {
4612
+ return filename === "chats.json" || filename.toLowerCase().endsWith(".zip");
4613
+ }
4614
+ function getDeviceName() {
4615
+ return loadConfig()?.device || hostname() || `${platform3()}-${process.arch}`;
4616
+ }
4617
+ function getLeaderboardFallback(periodType) {
4618
+ return {
4619
+ entries: [],
4620
+ next_cursor: null,
4621
+ current_user: null,
4622
+ period_type: periodType,
4623
+ period_start: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
4624
+ source_status: "unavailable"
4625
+ };
4626
+ }
4627
+ async function proxyLeaderboard(res, url) {
4628
+ const periodType = url.searchParams.get("period_type") || "daily";
4629
+ const upstreamBase = getSiteUrl();
4630
+ const upstream = new URL("/api/leaderboard", upstreamBase);
4631
+ upstream.searchParams.set("period_type", periodType);
3744
4632
  const periodStart = url.searchParams.get("period_start");
3745
4633
  if (periodStart) upstream.searchParams.set("period_start", periodStart);
3746
4634
  const cursor = url.searchParams.get("cursor");
@@ -3764,6 +4652,23 @@ async function proxyLeaderboard(res, url) {
3764
4652
  json(res, getLeaderboardFallback(periodType));
3765
4653
  }
3766
4654
  }
4655
+ async function proxyCloudSyncStatus(res) {
4656
+ const upstream = new URL("/api/cli/sync/status", getSiteUrl());
4657
+ try {
4658
+ const response = await fetch(upstream, {
4659
+ headers: { Accept: "application/json" }
4660
+ });
4661
+ const contentType = response.headers.get("content-type") || "";
4662
+ if (!response.ok || !contentType.includes("application/json")) {
4663
+ json(res, { enabled: false });
4664
+ return;
4665
+ }
4666
+ const data = await response.json().catch(() => null);
4667
+ json(res, data && typeof data.enabled === "boolean" ? data : { enabled: false });
4668
+ } catch {
4669
+ json(res, { enabled: false });
4670
+ }
4671
+ }
3767
4672
  function getToolFilter(tool, prefix = "") {
3768
4673
  if (!tool) return { where: "", params: {} };
3769
4674
  const col = prefix ? `${prefix}.tool` : "tool";
@@ -3817,6 +4722,90 @@ function createApiServer(db, options) {
3817
4722
  const cfg = loadConfig();
3818
4723
  let weekStart = cfg?.weekStart ?? 1;
3819
4724
  const dashboardPassword = getDashboardPassword();
4725
+ const localWriteQueue = new AsyncTaskQueue();
4726
+ const runDbWrite = options?.runDbWrite ?? ((task) => localWriteQueue.run(task));
4727
+ const getDbWriteQueueStatus = options?.getDbWriteQueueStatus ?? (() => localWriteQueue.getStatus());
4728
+ let pricingRecalcStatus = emptyPricingRecalcStatus();
4729
+ let pricingNeedsRecalcSince = null;
4730
+ let pricingRecalcGeneration = 0;
4731
+ const currentPricingRecalcStatus = () => {
4732
+ const status = {
4733
+ ...pricingRecalcStatus,
4734
+ needsRecalc: pricingNeedsRecalcSince != null,
4735
+ needsRecalcSince: pricingNeedsRecalcSince
4736
+ };
4737
+ if (status.state !== "queued") return status;
4738
+ const queueStatus = getDbWriteQueueStatus();
4739
+ return {
4740
+ ...status,
4741
+ queueRunning: queueStatus.running,
4742
+ queuePending: queueStatus.pending
4743
+ };
4744
+ };
4745
+ const markPricingNeedsRecalc = () => {
4746
+ pricingRecalcGeneration += 1;
4747
+ pricingNeedsRecalcSince = Date.now();
4748
+ return pricingNeedsRecalcSince;
4749
+ };
4750
+ const startPricingRecalc = () => {
4751
+ if (pricingRecalcStatus.state === "queued" || pricingRecalcStatus.state === "running") {
4752
+ return { accepted: false, status: currentPricingRecalcStatus() };
4753
+ }
4754
+ const startedAt = Date.now();
4755
+ const startedGeneration = pricingRecalcGeneration;
4756
+ pricingRecalcStatus = {
4757
+ ...emptyPricingRecalcStatus(),
4758
+ state: "running",
4759
+ startedAt
4760
+ };
4761
+ const runRecalc = async () => {
4762
+ pricingRecalcStatus = {
4763
+ ...pricingRecalcStatus,
4764
+ state: "running",
4765
+ queueRunning: false,
4766
+ queuePending: 0,
4767
+ startedAt: pricingRecalcStatus.startedAt ?? Date.now()
4768
+ };
4769
+ try {
4770
+ await recalcCosts(db, (progress) => {
4771
+ pricingRecalcStatus = {
4772
+ ...pricingRecalcStatus,
4773
+ ...progress,
4774
+ state: "running"
4775
+ };
4776
+ });
4777
+ if (pricingNeedsRecalcSince != null && pricingRecalcGeneration === startedGeneration) {
4778
+ pricingNeedsRecalcSince = null;
4779
+ }
4780
+ pricingRecalcStatus = {
4781
+ ...pricingRecalcStatus,
4782
+ state: "done",
4783
+ needsRecalc: pricingNeedsRecalcSince != null,
4784
+ needsRecalcSince: pricingNeedsRecalcSince,
4785
+ finishedAt: Date.now(),
4786
+ error: null
4787
+ };
4788
+ } catch (error) {
4789
+ pricingRecalcStatus = {
4790
+ ...pricingRecalcStatus,
4791
+ state: "error",
4792
+ finishedAt: Date.now(),
4793
+ error: error instanceof Error ? error.message : String(error)
4794
+ };
4795
+ }
4796
+ };
4797
+ setImmediate(() => {
4798
+ void runRecalc().catch((error) => {
4799
+ pricingRecalcStatus = {
4800
+ ...pricingRecalcStatus,
4801
+ state: "error",
4802
+ finishedAt: Date.now(),
4803
+ error: error instanceof Error ? error.message : String(error)
4804
+ };
4805
+ });
4806
+ });
4807
+ return { accepted: true, status: currentPricingRecalcStatus() };
4808
+ };
3820
4809
  const server = http.createServer(async (req, res) => {
3821
4810
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
3822
4811
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -3855,6 +4844,10 @@ function createApiServer(db, options) {
3855
4844
  json(res, { ok: true });
3856
4845
  return;
3857
4846
  }
4847
+ if (url.pathname === "/api/cli/sync/status" && req.method === "GET") {
4848
+ await proxyCloudSyncStatus(res);
4849
+ return;
4850
+ }
3858
4851
  if (dashboardPassword && shouldProtectApiPath(url.pathname) && !isAuthenticated(dashboardPassword, req.headers.cookie)) {
3859
4852
  json(res, { error: { code: "UNAUTHORIZED", message: "Authentication required" } }, 401);
3860
4853
  return;
@@ -4470,72 +5463,48 @@ function createApiServer(db, options) {
4470
5463
  }
4471
5464
  if (url.pathname === "/api/pricing") {
4472
5465
  if (req.method === "GET") {
4473
- const table = getPriceTable();
4474
- const overrides = getUserOverrides();
4475
- const dbModels = db.prepare("SELECT model, COUNT(*) as usage_count FROM records WHERE model != 'unknown' GROUP BY model ORDER BY usage_count DESC, model ASC").all();
4476
- const models = dbModels.map((r) => {
4477
- const model = r.model;
4478
- const exactPrice = table[model];
4479
- const resolvedPrice = resolvePrice(model);
4480
- const isOverride = model in overrides;
4481
- const isDefault = model in DEFAULT_PRICE_TABLE && !isOverride;
4482
- let matchedBy = null;
4483
- if (!exactPrice && resolvedPrice) {
4484
- for (const prefix of Object.keys(table)) {
4485
- if (model.startsWith(prefix) || model.toLowerCase().startsWith(prefix)) {
4486
- matchedBy = prefix;
4487
- break;
4488
- }
4489
- }
4490
- if (!matchedBy) {
4491
- const stripped = model.replace(/^[^/]+\//, "").toLowerCase();
4492
- for (const prefix of Object.keys(table)) {
4493
- if (stripped.startsWith(prefix)) {
4494
- matchedBy = prefix;
4495
- break;
4496
- }
4497
- }
4498
- }
4499
- }
4500
- return {
4501
- model,
4502
- price: resolvedPrice ?? null,
4503
- currency: resolvedPrice?.currency ?? "USD",
4504
- isDefault,
4505
- isOverride,
4506
- matchedBy
4507
- };
4508
- });
4509
- json(res, { models, overrides });
5466
+ json(res, { models: listPricingModels(db), registry: getPricingRegistrySummary(db), targets: listPricingAliasTargets(db), aliasBindings: getUserAliasBindings(db), localBindings: listLocalModelBindings(db) });
4510
5467
  return;
4511
5468
  }
4512
5469
  if (req.method === "PUT") {
4513
5470
  let body = "";
4514
5471
  for await (const chunk of req) body += chunk;
5472
+ let data;
4515
5473
  try {
4516
- const data = JSON.parse(body);
4517
- if (!data.model || typeof data.input !== "number" || typeof data.output !== "number") {
4518
- json(res, { error: { code: "INVALID_PARAM", message: "model, input, output required" } }, 400);
4519
- return;
4520
- }
4521
- const entry = {
4522
- input: data.input,
4523
- output: data.output,
4524
- cacheRead: data.cacheRead,
4525
- cacheWrite: data.cacheWrite
4526
- };
4527
- if (data.currency === "CNY") {
4528
- entry.currency = "CNY";
4529
- }
4530
- setPriceOverride(data.model, entry);
4531
- const cfg2 = loadConfig() ?? {};
4532
- cfg2.priceOverrides = { ...cfg2.priceOverrides, [data.model]: entry };
4533
- saveConfig(cfg2);
4534
- const recalculated = recalcCosts(db);
4535
- json(res, { ok: true, recalculated });
5474
+ data = JSON.parse(body);
4536
5475
  } catch {
4537
5476
  json(res, { error: { code: "INVALID_JSON", message: "Invalid JSON body" } }, 400);
5477
+ return;
5478
+ }
5479
+ if (!data.model || typeof data.input !== "number" || typeof data.output !== "number") {
5480
+ json(res, { error: { code: "INVALID_PARAM", message: "model, input, output required" } }, 400);
5481
+ return;
5482
+ }
5483
+ const entry = {
5484
+ input: data.input,
5485
+ output: data.output,
5486
+ cacheRead: data.cacheRead,
5487
+ cacheWrite: data.cacheWrite
5488
+ };
5489
+ if (data.currency === "CNY") {
5490
+ entry.currency = "CNY";
4538
5491
  }
5492
+ try {
5493
+ await runDbWrite(() => {
5494
+ setUserPrice(db, data.model, entry);
5495
+ const cfg2 = loadConfig() ?? {};
5496
+ cfg2.priceOverrides = { ...cfg2.priceOverrides, [data.model]: entry };
5497
+ saveConfig(cfg2);
5498
+ loadPricingRuntime(db, cfg2);
5499
+ });
5500
+ } catch (error) {
5501
+ if (isDatabaseLockedError(error)) {
5502
+ databaseBusy(res);
5503
+ return;
5504
+ }
5505
+ throw error;
5506
+ }
5507
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
4539
5508
  return;
4540
5509
  }
4541
5510
  if (req.method === "DELETE") {
@@ -4544,20 +5513,94 @@ function createApiServer(db, options) {
4544
5513
  json(res, { error: { code: "INVALID_PARAM", message: "model param required" } }, 400);
4545
5514
  return;
4546
5515
  }
4547
- removePriceOverride(model);
4548
- const cfg2 = loadConfig() ?? {};
4549
- if (cfg2.priceOverrides) {
4550
- delete cfg2.priceOverrides[model];
4551
- saveConfig(cfg2);
5516
+ try {
5517
+ await runDbWrite(() => {
5518
+ resetUserPriceToSynced(db, model);
5519
+ removePriceOverride(model);
5520
+ const cfg2 = loadConfig() ?? {};
5521
+ if (cfg2.priceOverrides) {
5522
+ delete cfg2.priceOverrides[model];
5523
+ saveConfig(cfg2);
5524
+ }
5525
+ loadPricingRuntime(db, cfg2);
5526
+ });
5527
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5528
+ } catch (error) {
5529
+ if (isDatabaseLockedError(error)) {
5530
+ databaseBusy(res);
5531
+ return;
5532
+ }
5533
+ throw error;
4552
5534
  }
4553
- const recalculated = recalcCosts(db);
4554
- json(res, { ok: true, recalculated });
4555
5535
  return;
4556
5536
  }
4557
5537
  }
5538
+ if (url.pathname === "/api/pricing/alias" && req.method === "POST") {
5539
+ try {
5540
+ const data = await readJsonBody(req);
5541
+ if (typeof data.alias !== "string" || typeof data.modelKey !== "string") {
5542
+ json(res, { error: { code: "INVALID_PARAM", message: "alias and modelKey required" } }, 400);
5543
+ return;
5544
+ }
5545
+ await runDbWrite(() => {
5546
+ setUserPricingAlias(db, data.alias, data.modelKey);
5547
+ loadPricingRuntime(db, loadConfig());
5548
+ });
5549
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5550
+ } catch (error) {
5551
+ if (isDatabaseLockedError(error)) {
5552
+ databaseBusy(res);
5553
+ return;
5554
+ }
5555
+ json(res, { error: { code: "INVALID_PARAM", message: error instanceof Error ? error.message : "Invalid pricing alias" } }, 400);
5556
+ }
5557
+ return;
5558
+ }
5559
+ if (url.pathname === "/api/pricing/alias" && req.method === "DELETE") {
5560
+ const alias = url.searchParams.get("alias");
5561
+ if (!alias) {
5562
+ json(res, { error: { code: "INVALID_PARAM", message: "alias param required" } }, 400);
5563
+ return;
5564
+ }
5565
+ try {
5566
+ await runDbWrite(() => {
5567
+ removeUserPricingAlias(db, alias);
5568
+ loadPricingRuntime(db, loadConfig());
5569
+ });
5570
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5571
+ } catch (error) {
5572
+ if (isDatabaseLockedError(error)) {
5573
+ databaseBusy(res);
5574
+ return;
5575
+ }
5576
+ json(res, { error: { code: "INVALID_PARAM", message: error instanceof Error ? error.message : "Invalid pricing alias" } }, 400);
5577
+ }
5578
+ return;
5579
+ }
5580
+ if (url.pathname === "/api/pricing/sync" && req.method === "POST") {
5581
+ try {
5582
+ const summary = await runDbWrite(async () => {
5583
+ const summary2 = await syncPricingFromLitellm(db);
5584
+ loadPricingRuntime(db, loadConfig());
5585
+ return summary2;
5586
+ });
5587
+ json(res, { ok: true, summary, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5588
+ } catch (error) {
5589
+ if (isDatabaseLockedError(error)) {
5590
+ databaseBusy(res);
5591
+ return;
5592
+ }
5593
+ json(res, { error: { code: "SYNC_FAILED", message: error instanceof Error ? error.message : "Pricing sync failed" } }, 502);
5594
+ }
5595
+ return;
5596
+ }
5597
+ if (url.pathname === "/api/pricing/recalc" && req.method === "GET") {
5598
+ json(res, currentPricingRecalcStatus());
5599
+ return;
5600
+ }
4558
5601
  if (url.pathname === "/api/pricing/recalc" && req.method === "POST") {
4559
- const updated = recalcCosts(db);
4560
- json(res, { updated });
5602
+ const result = startPricingRecalc();
5603
+ json(res, result, result.accepted ? 202 : 200);
4561
5604
  return;
4562
5605
  }
4563
5606
  if (url.pathname === "/api/sync") {
@@ -4579,8 +5622,16 @@ function createApiServer(db, options) {
4579
5622
  json(res, { error: { code: "NOT_AVAILABLE", message: "Refresh not available" } }, 501);
4580
5623
  return;
4581
5624
  }
4582
- const result = await options.onRefresh();
4583
- json(res, result);
5625
+ try {
5626
+ const result = await runDbWrite(() => options.onRefresh());
5627
+ json(res, result);
5628
+ } catch (error) {
5629
+ if (isDatabaseLockedError(error)) {
5630
+ databaseBusy(res);
5631
+ return;
5632
+ }
5633
+ throw error;
5634
+ }
4584
5635
  return;
4585
5636
  }
4586
5637
  if (url.pathname === "/api/quotas" && req.method === "GET") {
@@ -4954,141 +6005,86 @@ function createApiServer(db, options) {
4954
6005
  }
4955
6006
  }
4956
6007
  if (url.pathname === "/api/detected-tools" && req.method === "GET") {
4957
- const tools = discoverTools();
6008
+ const tools = attachManualImportMetadata(discoverTools(), db);
4958
6009
  json(res, { tools });
4959
6010
  return;
4960
6011
  }
4961
- if (url.pathname === "/api/exchange-rate/refresh" && req.method === "POST") {
4962
- const rate = await fetchExchangeRate();
4963
- if (rate == null) {
4964
- json(res, { error: { code: "FETCH_FAILED", message: "Failed to fetch exchange rate" } }, 502);
4965
- return;
4966
- }
4967
- const cfg2 = loadConfig() ?? {};
4968
- cfg2.exchangeRateCache = { CNY_USD: rate, fetchedAt: Date.now() };
4969
- saveConfig(cfg2);
4970
- json(res, { rate, fetchedAt: cfg2.exchangeRateCache.fetchedAt });
4971
- return;
4972
- }
4973
- json(res, { error: { code: "NOT_FOUND", message: "Endpoint not found" } }, 404);
4974
- } catch (error) {
4975
- console.error("API error:", error);
4976
- json(res, { error: { code: "INTERNAL", message: "Internal server error" } }, 500);
4977
- }
4978
- });
4979
- return server;
4980
- }
4981
-
4982
- // src/commands/parse.ts
4983
- import Database2 from "better-sqlite3";
4984
- import { readFileSync as readFileSync7, statSync as statSync2, existsSync as existsSync8, openSync, readSync, closeSync } from "fs";
4985
- import { join as join7 } from "path";
4986
- import { hostname as hostname2 } from "os";
4987
-
4988
- // src/db/records.ts
4989
- function insertRecord(db, record) {
4990
- db.prepare(`
4991
- INSERT OR REPLACE INTO records (
4992
- id, ts, ingested_at, synced_at, updated_at, line_offset,
4993
- tool, model, provider, input_tokens, output_tokens,
4994
- cache_read_tokens, cache_write_tokens, thinking_tokens,
4995
- cost, cost_source, session_id, source_file, cwd, device, device_instance_id, platform
4996
- ) VALUES (
4997
- @id, @ts, @ingestedAt, @syncedAt, @updatedAt, @lineOffset,
4998
- @tool, @model, @provider, @inputTokens, @outputTokens,
4999
- @cacheReadTokens, @cacheWriteTokens, @thinkingTokens,
5000
- @cost, @costSource, @sessionId, @sourceFile, @cwd, @device, @deviceInstanceId, @platform
5001
- )
5002
- `).run({
5003
- id: record.id,
5004
- ts: record.ts,
5005
- ingestedAt: record.ingestedAt,
5006
- syncedAt: record.syncedAt ?? null,
5007
- updatedAt: record.updatedAt,
5008
- lineOffset: record.lineOffset,
5009
- tool: record.tool,
5010
- model: record.model,
5011
- provider: record.provider,
5012
- inputTokens: record.inputTokens,
5013
- outputTokens: record.outputTokens,
5014
- cacheReadTokens: record.cacheReadTokens,
5015
- cacheWriteTokens: record.cacheWriteTokens,
5016
- thinkingTokens: record.thinkingTokens,
5017
- cost: record.cost,
5018
- costSource: record.costSource,
5019
- sessionId: record.sessionId,
5020
- sourceFile: record.sourceFile,
5021
- cwd: record.cwd ?? "",
5022
- device: record.device,
5023
- deviceInstanceId: record.deviceInstanceId,
5024
- platform: record.platform ?? ""
5025
- });
5026
- }
5027
- function getUnsyncedRecords(db, target) {
5028
- const rows = target ? db.prepare(`
5029
- SELECT r.* FROM records r
5030
- LEFT JOIN sync_record_state s
5031
- ON s.record_id = r.id AND s.target = ?
5032
- WHERE r.source_file NOT LIKE 'synced/%'
5033
- AND (s.synced_at IS NULL OR r.updated_at > s.synced_at)
5034
- `).all(target) : db.prepare(
5035
- "SELECT * FROM records WHERE synced_at IS NULL OR updated_at > synced_at"
5036
- ).all();
5037
- return rows.map(mapRowToRecord);
5038
- }
5039
- function markRecordsSynced(db, ids, syncedAt, target) {
5040
- if (ids.length === 0) return;
5041
- if (target) {
5042
- const insertStmt = db.prepare(`
5043
- INSERT INTO sync_record_state (record_id, target, synced_at)
5044
- VALUES (?, ?, ?)
5045
- ON CONFLICT(record_id, target) DO UPDATE SET synced_at = excluded.synced_at
5046
- `);
5047
- const legacyStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
5048
- const tx2 = db.transaction((recordIds) => {
5049
- for (const id of recordIds) {
5050
- insertStmt.run(id, target, syncedAt);
5051
- legacyStmt.run(syncedAt, id);
6012
+ if (url.pathname === "/api/import/kelivo" && req.method === "POST") {
6013
+ let tempDir = null;
6014
+ try {
6015
+ const upload = await readMultipartFile(req);
6016
+ if (!upload) {
6017
+ json(res, { error: { code: "INVALID_UPLOAD", message: "Missing uploaded file" } }, 400);
6018
+ return;
6019
+ }
6020
+ if (!isKelivoBackupFilename(upload.filename)) {
6021
+ json(res, { error: { code: "INVALID_FILE", message: "Upload a Kelivo .zip backup or chats.json" } }, 400);
6022
+ return;
6023
+ }
6024
+ tempDir = mkdtempSync(path2.join(tmpdir(), "aiusage-kelivo-"));
6025
+ const tempPath = path2.join(tempDir, upload.filename);
6026
+ writeFileSync4(tempPath, upload.data);
6027
+ const cfg2 = loadConfig();
6028
+ const now = Date.now();
6029
+ const result = await runParseKelivo({
6030
+ filePath: tempPath,
6031
+ device: getDeviceName(),
6032
+ deviceInstanceId: options?.currentDeviceInstanceId ?? "unknown",
6033
+ platform: cfg2?.platform ?? platform3(),
6034
+ now,
6035
+ exchangeRate: resolveExchangeRate(cfg2 ?? {})
6036
+ });
6037
+ if (result.errors.length > 0 && result.records.length === 0) {
6038
+ json(res, { error: { code: "IMPORT_FAILED", message: result.errors[0] }, errors: result.errors }, 400);
6039
+ return;
6040
+ }
6041
+ const existingCount = await runDbWrite(() => {
6042
+ const existingCount2 = countExistingRecordIds(db, result.records.map((record) => record.id));
6043
+ for (const record of result.records) insertRecord(db, record);
6044
+ return existingCount2;
6045
+ });
6046
+ const imported = result.records.length;
6047
+ const added = Math.max(0, new Set(result.records.map((record) => record.id)).size - existingCount);
6048
+ json(res, {
6049
+ imported,
6050
+ added,
6051
+ lastImportedAt: imported > 0 ? now : void 0,
6052
+ errors: result.errors
6053
+ });
6054
+ } finally {
6055
+ if (tempDir) rmSync(tempDir, { recursive: true, force: true });
6056
+ }
6057
+ return;
5052
6058
  }
5053
- });
5054
- tx2(ids);
5055
- return;
5056
- }
5057
- const updateStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
5058
- const tx = db.transaction((recordIds) => {
5059
- for (const id of recordIds) updateStmt.run(syncedAt, id);
6059
+ if (url.pathname === "/api/exchange-rate/refresh" && req.method === "POST") {
6060
+ const rate = await fetchExchangeRate();
6061
+ if (rate == null) {
6062
+ json(res, { error: { code: "FETCH_FAILED", message: "Failed to fetch exchange rate" } }, 502);
6063
+ return;
6064
+ }
6065
+ const cfg2 = loadConfig() ?? {};
6066
+ cfg2.exchangeRateCache = { CNY_USD: rate, fetchedAt: Date.now() };
6067
+ saveConfig(cfg2);
6068
+ json(res, { rate, fetchedAt: cfg2.exchangeRateCache.fetchedAt });
6069
+ return;
6070
+ }
6071
+ json(res, { error: { code: "NOT_FOUND", message: "Endpoint not found" } }, 404);
6072
+ } catch (error) {
6073
+ console.error("API error:", error);
6074
+ json(res, { error: { code: "INTERNAL", message: "Internal server error" } }, 500);
6075
+ }
5060
6076
  });
5061
- tx(ids);
5062
- }
5063
- function mapRowToRecord(row) {
5064
- return {
5065
- id: row.id,
5066
- ts: row.ts,
5067
- ingestedAt: row.ingested_at,
5068
- syncedAt: row.synced_at != null ? row.synced_at : void 0,
5069
- updatedAt: row.updated_at,
5070
- lineOffset: row.line_offset,
5071
- tool: row.tool,
5072
- model: row.model,
5073
- provider: row.provider,
5074
- inputTokens: row.input_tokens,
5075
- outputTokens: row.output_tokens,
5076
- cacheReadTokens: row.cache_read_tokens,
5077
- cacheWriteTokens: row.cache_write_tokens,
5078
- thinkingTokens: row.thinking_tokens,
5079
- cost: row.cost,
5080
- costSource: row.cost_source,
5081
- sessionId: row.session_id,
5082
- sourceFile: row.source_file,
5083
- cwd: row.cwd || void 0,
5084
- device: row.device,
5085
- deviceInstanceId: row.device_instance_id,
5086
- platform: row.platform || void 0
5087
- };
6077
+ return server;
5088
6078
  }
5089
6079
 
6080
+ // src/commands/parse.ts
6081
+ import Database2 from "better-sqlite3";
6082
+ import { readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync8, openSync, readSync, closeSync } from "fs";
6083
+ import { join as join7 } from "path";
6084
+ import { hostname as hostname2 } from "os";
6085
+
5090
6086
  // src/watermark.ts
5091
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "fs";
6087
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
5092
6088
  function defaultFileData() {
5093
6089
  return {
5094
6090
  "claude-code": {},
@@ -5099,6 +6095,7 @@ function defaultFileData() {
5099
6095
  "qoder": {},
5100
6096
  "cursor": {},
5101
6097
  "kilocode": {},
6098
+ "kelivo": {},
5102
6099
  "copilot": {},
5103
6100
  "gemini": {},
5104
6101
  "kimi": {},
@@ -5118,8 +6115,8 @@ function defaultFileData() {
5118
6115
  var WatermarkManager = class {
5119
6116
  data;
5120
6117
  path;
5121
- constructor(path2) {
5122
- this.path = path2;
6118
+ constructor(path3) {
6119
+ this.path = path3;
5123
6120
  this.data = this.load();
5124
6121
  }
5125
6122
  load() {
@@ -5127,7 +6124,7 @@ var WatermarkManager = class {
5127
6124
  return { files: defaultFileData() };
5128
6125
  }
5129
6126
  try {
5130
- const content = readFileSync6(this.path, "utf-8");
6127
+ const content = readFileSync7(this.path, "utf-8");
5131
6128
  const parsed = JSON.parse(content);
5132
6129
  if (parsed && typeof parsed === "object" && !("files" in parsed)) {
5133
6130
  return { files: { ...defaultFileData(), ...parsed } };
@@ -5138,7 +6135,7 @@ var WatermarkManager = class {
5138
6135
  }
5139
6136
  }
5140
6137
  save() {
5141
- writeFileSync4(this.path, JSON.stringify(this.data, null, 2), "utf-8");
6138
+ writeFileSync5(this.path, JSON.stringify(this.data, null, 2), "utf-8");
5142
6139
  }
5143
6140
  getEntry(tool, filePath) {
5144
6141
  return this.data.files[tool]?.[filePath] ?? null;
@@ -6011,6 +7008,11 @@ function extractSessionId2(filePath, tool) {
6011
7008
  const parts = filePath.split("/");
6012
7009
  return (parts.pop() ?? "").replace(".jsonl", "");
6013
7010
  }
7011
+ if (tool === "pi") {
7012
+ const filename = filePath.split("/").pop() ?? "";
7013
+ const match = filename.match(/_([^_]+)\.jsonl$/);
7014
+ return match ? match[1] : filename.replace(".jsonl", "");
7015
+ }
6014
7016
  return "unknown";
6015
7017
  }
6016
7018
  function toNumber2(value) {
@@ -6023,12 +7025,12 @@ function parseUiMessagesFile(options) {
6023
7025
  const errors = [];
6024
7026
  let messages;
6025
7027
  try {
6026
- const parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
7028
+ const parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
6027
7029
  messages = Array.isArray(parsed) ? parsed : [];
6028
7030
  } catch (e) {
6029
7031
  return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
6030
7032
  }
6031
- const taskId = filePath.split("/").slice(-2, -1)[0] || "unknown";
7033
+ const taskId = filePath.replace(/\\/g, "/").split("/").slice(-2, -1)[0] || "unknown";
6032
7034
  for (const [index, message] of messages.entries()) {
6033
7035
  if (message?.say !== "api_req_started" || typeof message.text !== "string") continue;
6034
7036
  let payload;
@@ -6080,7 +7082,7 @@ function normalizeKiroModel(value) {
6080
7082
  if (typeof value !== "string" || !value.trim()) return "kiro-agent";
6081
7083
  return value.trim().toLowerCase().replace(/_/g, "-").replace(/^(anthropic\.|openai\.|aws\.)/, "").replace(/:\d+$/, "").replace(/-\d{8}-v\d+(?:-\d+)?$/i, "");
6082
7084
  }
6083
- function parseTimestamp(value, fallback) {
7085
+ function parseTimestamp2(value, fallback) {
6084
7086
  if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
6085
7087
  if (typeof value === "string" && value.trim()) {
6086
7088
  const parsed = new Date(value).getTime();
@@ -6101,7 +7103,7 @@ function parseKiroSessionFile(options) {
6101
7103
  const errors = [];
6102
7104
  let parsed;
6103
7105
  try {
6104
- parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
7106
+ parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
6105
7107
  } catch (e) {
6106
7108
  return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
6107
7109
  }
@@ -6114,7 +7116,7 @@ function parseKiroSessionFile(options) {
6114
7116
  const outputTokens = toNumber2(turn?.output_token_count);
6115
7117
  if (inputTokens + outputTokens === 0) continue;
6116
7118
  const model = normalizeKiroModel(turn?.model_id ?? sessionModel);
6117
- const recordTs = parseTimestamp(
7119
+ const recordTs = parseTimestamp2(
6118
7120
  turn?.request_start_timestamp_ms ?? turn?.start_timestamp ?? turn?.end_timestamp,
6119
7121
  now
6120
7122
  );
@@ -6225,7 +7227,30 @@ async function runParse(db, filterTool, options) {
6225
7227
  onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
6226
7228
  continue;
6227
7229
  }
6228
- const content = readFileSync7(filePath, "utf-8");
7230
+ if (tool === "kelivo" && (filePath.endsWith("chats.json") || filePath.endsWith(".zip"))) {
7231
+ const result = await runParseKelivo({
7232
+ filePath,
7233
+ device,
7234
+ deviceInstanceId,
7235
+ platform: devicePlatform,
7236
+ now: Date.now(),
7237
+ exchangeRate
7238
+ });
7239
+ for (const record of result.records) {
7240
+ insertRecord(db, record);
7241
+ parsedCount++;
7242
+ }
7243
+ errors.push(...result.errors);
7244
+ wm.setEntry(tool, filePath, {
7245
+ offset: stat2.size,
7246
+ size: stat2.size,
7247
+ mtime: stat2.mtimeMs
7248
+ });
7249
+ wm.save();
7250
+ onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
7251
+ continue;
7252
+ }
7253
+ const content = readFileSync8(filePath, "utf-8");
6229
7254
  const lines = content.split("\n");
6230
7255
  let byteOffset = 0;
6231
7256
  let fileCwd;
@@ -6680,7 +7705,7 @@ function backfillCodexModels(db) {
6680
7705
  );
6681
7706
  for (const [sourceFile, fileRows] of byFile) {
6682
7707
  try {
6683
- const content = readFileSync7(sourceFile, "utf-8");
7708
+ const content = readFileSync8(sourceFile, "utf-8");
6684
7709
  const lines = content.split("\n");
6685
7710
  const offsetToModel = /* @__PURE__ */ new Map();
6686
7711
  let currentModel = "";
@@ -6728,7 +7753,7 @@ function backfillMissingToolCalls(db, exchangeRate) {
6728
7753
  }
6729
7754
  for (const [sourceFile, fileRows] of rowsByFile) {
6730
7755
  try {
6731
- const content = readFileSync7(sourceFile, "utf-8");
7756
+ const content = readFileSync8(sourceFile, "utf-8");
6732
7757
  const lines = content.split("\n");
6733
7758
  const aggregator = new Aggregator();
6734
7759
  const recordIdByOffset = /* @__PURE__ */ new Map();
@@ -6794,7 +7819,7 @@ import { join as join9 } from "path";
6794
7819
 
6795
7820
  // src/db/synced-records.ts
6796
7821
  function insertSyncedRecord(db, record) {
6797
- db.prepare(`
7822
+ const result = db.prepare(`
6798
7823
  INSERT INTO synced_records (
6799
7824
  id, ts, tool, model, provider, input_tokens, output_tokens,
6800
7825
  cache_read_tokens, cache_write_tokens, thinking_tokens,
@@ -6847,6 +7872,7 @@ function insertSyncedRecord(db, record) {
6847
7872
  sourceFile: record.sourceFile ?? "",
6848
7873
  cwd: record.cwd ?? ""
6849
7874
  });
7875
+ return result.changes > 0;
6850
7876
  }
6851
7877
  function mergeSyncedRecordsIntoRecords(db) {
6852
7878
  const now = Date.now();
@@ -6907,6 +7933,7 @@ var RECORD_ID_SYNC_TOOLS = /* @__PURE__ */ new Set([
6907
7933
  "qoder",
6908
7934
  "cursor",
6909
7935
  "kilocode",
7936
+ "kelivo",
6910
7937
  "goose",
6911
7938
  "zed",
6912
7939
  "kiro",
@@ -7015,15 +8042,15 @@ var SyncOrchestrator = class {
7015
8042
  });
7016
8043
  if (paths.length === 0) return 0;
7017
8044
  let totalPulled = 0;
7018
- for (const [index, path2] of paths.entries()) {
8045
+ for (const [index, path3] of paths.entries()) {
7019
8046
  this.options.onProgress?.({
7020
8047
  phase: "pulling",
7021
- currentPath: path2,
8048
+ currentPath: path3,
7022
8049
  completedFiles: index,
7023
8050
  totalFiles: paths.length,
7024
8051
  pulledCount: totalPulled
7025
8052
  });
7026
- const content = await this.backend.readFile(path2);
8053
+ const content = await this.backend.readFile(path3);
7027
8054
  if (!content) continue;
7028
8055
  for (const line of content.split("\n").filter(Boolean)) {
7029
8056
  try {
@@ -7034,14 +8061,14 @@ var SyncOrchestrator = class {
7034
8061
  if (typeof record.updatedAt === "string") {
7035
8062
  record.updatedAt = new Date(record.updatedAt).getTime();
7036
8063
  }
7037
- insertSyncedRecord(this.db, record);
7038
- totalPulled++;
8064
+ const changed = insertSyncedRecord(this.db, record);
8065
+ if (changed) totalPulled++;
7039
8066
  } catch {
7040
8067
  }
7041
8068
  }
7042
8069
  this.options.onProgress?.({
7043
8070
  phase: "pulling",
7044
- currentPath: path2,
8071
+ currentPath: path3,
7045
8072
  completedFiles: index + 1,
7046
8073
  totalFiles: paths.length,
7047
8074
  pulledCount: totalPulled
@@ -7049,13 +8076,13 @@ var SyncOrchestrator = class {
7049
8076
  }
7050
8077
  return totalPulled;
7051
8078
  }
7052
- async uploadFile(path2, localSyncRecords) {
7053
- const existingContent = await this.backend.readFile(path2);
8079
+ async uploadFile(path3, localSyncRecords) {
8080
+ const existingContent = await this.backend.readFile(path3);
7054
8081
  const existing = existingContent ? parseNdjson(existingContent) : /* @__PURE__ */ new Map();
7055
8082
  const changedCount = mergeRecords(existing, localSyncRecords);
7056
8083
  if (changedCount === 0) return 0;
7057
8084
  const content = Array.from(existing.values()).map((r) => JSON.stringify(r)).join("\n") + "\n";
7058
- await this.backend.writeFile(path2, content);
8085
+ await this.backend.writeFile(path3, content);
7059
8086
  return changedCount;
7060
8087
  }
7061
8088
  async upload() {
@@ -7063,9 +8090,9 @@ var SyncOrchestrator = class {
7063
8090
  if (unsynced.length === 0) return 0;
7064
8091
  const byPath = /* @__PURE__ */ new Map();
7065
8092
  for (const record of unsynced) {
7066
- const path2 = getSyncPath(record.ts, this.options.deviceInstanceId);
7067
- if (!byPath.has(path2)) byPath.set(path2, []);
7068
- byPath.get(path2).push(record);
8093
+ const path3 = getSyncPath(record.ts, this.options.deviceInstanceId);
8094
+ if (!byPath.has(path3)) byPath.set(path3, []);
8095
+ byPath.get(path3).push(record);
7069
8096
  }
7070
8097
  let totalUploaded = 0;
7071
8098
  const uploads = Array.from(byPath.entries());
@@ -7075,20 +8102,20 @@ var SyncOrchestrator = class {
7075
8102
  totalFiles: uploads.length,
7076
8103
  uploadedCount: 0
7077
8104
  });
7078
- for (const [index, [path2, localRecords]] of uploads.entries()) {
8105
+ for (const [index, [path3, localRecords]] of uploads.entries()) {
7079
8106
  this.options.onProgress?.({
7080
8107
  phase: "uploading",
7081
- currentPath: path2,
8108
+ currentPath: path3,
7082
8109
  completedFiles: index,
7083
8110
  totalFiles: uploads.length,
7084
8111
  uploadedCount: totalUploaded
7085
8112
  });
7086
8113
  const localSyncRecords = localRecords.map(mapStatsRecordToSyncRecord);
7087
- await this.uploadFile(path2, localSyncRecords);
8114
+ await this.uploadFile(path3, localSyncRecords);
7088
8115
  totalUploaded += localRecords.length;
7089
8116
  this.options.onProgress?.({
7090
8117
  phase: "uploading",
7091
- currentPath: path2,
8118
+ currentPath: path3,
7092
8119
  completedFiles: index + 1,
7093
8120
  totalFiles: uploads.length,
7094
8121
  uploadedCount: totalUploaded
@@ -7113,14 +8140,14 @@ var CloudSyncError = class extends Error {
7113
8140
  code;
7114
8141
  retryAfter;
7115
8142
  };
7116
- function buildHeaders(method, path2, body) {
8143
+ function buildHeaders(method, path3, body) {
7117
8144
  const creds = loadCredentials();
7118
8145
  if (!creds) throw new CloudSyncError("Not logged in. Run `aiusage login` first.", "not_logged_in");
7119
8146
  const bodyHash = sha256(body);
7120
8147
  const timestamp = Date.now().toString();
7121
8148
  const nonce = generateNonce();
7122
8149
  const idempotencyKey = generateIdempotencyKey();
7123
- const canonical = buildCanonicalString(method, path2, bodyHash, timestamp, nonce, creds.device_id, idempotencyKey);
8150
+ const canonical = buildCanonicalString(method, path3, bodyHash, timestamp, nonce, creds.device_id, idempotencyKey);
7124
8151
  const signature = computeHmac(creds.device_secret, canonical);
7125
8152
  return {
7126
8153
  "Content-Type": "application/json",
@@ -7179,9 +8206,9 @@ async function cloudPull(cursor, limit = 1e3) {
7179
8206
  if (cursor) params.set("cursor", cursor);
7180
8207
  params.set("limit", String(limit));
7181
8208
  const query = params.toString();
7182
- const path2 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
8209
+ const path3 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
7183
8210
  const headers = buildHeaders("GET", SYNC_PULL_PATH, "");
7184
- const response = await fetch(`${serverUrl}${path2}`, { method: "GET", headers });
8211
+ const response = await fetch(`${serverUrl}${path3}`, { method: "GET", headers });
7185
8212
  const data = await readJsonOrNull2(response);
7186
8213
  if (!response.ok) {
7187
8214
  const errObj = data?.error;
@@ -7198,12 +8225,32 @@ async function cloudPull(cursor, limit = 1e3) {
7198
8225
  syncGeneration: data.sync_generation || 1
7199
8226
  };
7200
8227
  }
8228
+ async function cloudClear() {
8229
+ const serverUrl = getSiteUrl();
8230
+ const creds = loadCredentials();
8231
+ if (!creds) throw new CloudSyncError("Not logged in.", "not_logged_in");
8232
+ const SYNC_CLEAR_PATH = "/api/cli/sync/clear";
8233
+ const body = "{}";
8234
+ const headers = buildHeaders("POST", SYNC_CLEAR_PATH, body);
8235
+ const response = await fetch(`${serverUrl}${SYNC_CLEAR_PATH}`, { method: "POST", headers, body });
8236
+ const data = await readJsonOrNull2(response);
8237
+ if (!response.ok) {
8238
+ const errObj = data?.error;
8239
+ const errMsg = typeof errObj === "object" && errObj !== null ? errObj.message || `Clear failed (HTTP ${response.status})` : errObj || `Clear failed (HTTP ${response.status})`;
8240
+ const errCode = typeof errObj === "object" && errObj !== null ? errObj.code || "server_error" : data?.error_code || "server_error";
8241
+ throw new CloudSyncError(errMsg, errCode);
8242
+ }
8243
+ if (!data) throw new CloudSyncError("Invalid response from server", "invalid_response");
8244
+ return {
8245
+ syncGeneration: data.sync_generation || 1
8246
+ };
8247
+ }
7201
8248
  function getClientVersion() {
7202
8249
  try {
7203
- const { readFileSync: readFileSync11 } = __require("fs");
8250
+ const { readFileSync: readFileSync12 } = __require("fs");
7204
8251
  const { join: join16 } = __require("path");
7205
8252
  const pkgPath = join16(__dirname, "../../package.json");
7206
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
8253
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7207
8254
  return pkg.version || "0.0.0";
7208
8255
  } catch {
7209
8256
  return "0.0.0";
@@ -7293,7 +8340,7 @@ var CloudSyncOrchestrator = class {
7293
8340
  // src/sync/git.ts
7294
8341
  import { execFile } from "child_process";
7295
8342
  import { promisify } from "util";
7296
- import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
8343
+ import { readFile, writeFile, mkdir, readdir, stat, unlink, rm } from "fs/promises";
7297
8344
  import { join as join8 } from "path";
7298
8345
  var exec = promisify(execFile);
7299
8346
  var GitSyncBackend = class _GitSyncBackend {
@@ -7353,16 +8400,16 @@ var GitSyncBackend = class _GitSyncBackend {
7353
8400
  }
7354
8401
  }
7355
8402
  }
7356
- async readFile(path2) {
8403
+ async readFile(path3) {
7357
8404
  try {
7358
- const fullPath = join8(this.dataDir, path2);
8405
+ const fullPath = join8(this.dataDir, path3);
7359
8406
  return await readFile(fullPath, "utf-8");
7360
8407
  } catch {
7361
8408
  return null;
7362
8409
  }
7363
8410
  }
7364
- async writeFile(path2, content) {
7365
- const fullPath = join8(this.dataDir, path2);
8411
+ async writeFile(path3, content) {
8412
+ const fullPath = join8(this.dataDir, path3);
7366
8413
  await mkdir(join8(fullPath, ".."), { recursive: true });
7367
8414
  await writeFile(fullPath, content, "utf-8");
7368
8415
  }
@@ -7373,6 +8420,22 @@ var GitSyncBackend = class _GitSyncBackend {
7373
8420
  return [];
7374
8421
  }
7375
8422
  }
8423
+ async deleteFile(path3) {
8424
+ const fullPath = join8(this.dataDir, path3);
8425
+ try {
8426
+ await unlink(fullPath);
8427
+ } catch {
8428
+ }
8429
+ }
8430
+ async deleteAllData() {
8431
+ try {
8432
+ const files = await this.listFiles();
8433
+ await rm(this.dataDir, { recursive: true, force: true });
8434
+ return files.length;
8435
+ } catch {
8436
+ return 0;
8437
+ }
8438
+ }
7376
8439
  static MAX_PUSH_RETRIES = 3;
7377
8440
  /** Commit and push all changes. Returns true if anything was pushed. */
7378
8441
  async flush() {
@@ -7407,7 +8470,7 @@ var GitSyncBackend = class _GitSyncBackend {
7407
8470
  };
7408
8471
 
7409
8472
  // src/sync/s3.ts
7410
- import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
8473
+ import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, DeleteObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
7411
8474
  var S3SyncBackend = class {
7412
8475
  client;
7413
8476
  bucket;
@@ -7424,11 +8487,11 @@ var S3SyncBackend = class {
7424
8487
  }
7425
8488
  });
7426
8489
  }
7427
- getObjectKey(path2) {
7428
- return `${this.prefix}${path2}`;
8490
+ getObjectKey(path3) {
8491
+ return `${this.prefix}${path3}`;
7429
8492
  }
7430
- async readFile(path2) {
7431
- const key = this.getObjectKey(path2);
8493
+ async readFile(path3) {
8494
+ const key = this.getObjectKey(path3);
7432
8495
  try {
7433
8496
  const command = new GetObjectCommand({
7434
8497
  Bucket: this.bucket,
@@ -7444,8 +8507,8 @@ var S3SyncBackend = class {
7444
8507
  throw error;
7445
8508
  }
7446
8509
  }
7447
- async writeFile(path2, content) {
7448
- const key = this.getObjectKey(path2);
8510
+ async writeFile(path3, content) {
8511
+ const key = this.getObjectKey(path3);
7449
8512
  const command = new PutObjectCommand({
7450
8513
  Bucket: this.bucket,
7451
8514
  Key: key,
@@ -7477,8 +8540,8 @@ var S3SyncBackend = class {
7477
8540
  } while (continuationToken);
7478
8541
  return files.sort();
7479
8542
  }
7480
- async fileExists(path2) {
7481
- const key = this.getObjectKey(path2);
8543
+ async fileExists(path3) {
8544
+ const key = this.getObjectKey(path3);
7482
8545
  try {
7483
8546
  const command = new HeadObjectCommand({
7484
8547
  Bucket: this.bucket,
@@ -7493,6 +8556,31 @@ var S3SyncBackend = class {
7493
8556
  throw error;
7494
8557
  }
7495
8558
  }
8559
+ async deleteFile(path3) {
8560
+ const key = this.getObjectKey(path3);
8561
+ const command = new DeleteObjectCommand({
8562
+ Bucket: this.bucket,
8563
+ Key: key
8564
+ });
8565
+ await this.client.send(command);
8566
+ }
8567
+ async deleteAllData() {
8568
+ const files = await this.listFiles();
8569
+ if (files.length === 0) return 0;
8570
+ const BATCH_SIZE2 = 1e3;
8571
+ for (let i = 0; i < files.length; i += BATCH_SIZE2) {
8572
+ const batch = files.slice(i, i + BATCH_SIZE2);
8573
+ const command = new DeleteObjectsCommand({
8574
+ Bucket: this.bucket,
8575
+ Delete: {
8576
+ Objects: batch.map((f) => ({ Key: this.getObjectKey(f) })),
8577
+ Quiet: true
8578
+ }
8579
+ });
8580
+ await this.client.send(command);
8581
+ }
8582
+ return files.length;
8583
+ }
7496
8584
  };
7497
8585
 
7498
8586
  // src/commands/sync.ts
@@ -7601,6 +8689,8 @@ async function runSync(db, options) {
7601
8689
  }
7602
8690
 
7603
8691
  // src/commands/clean.ts
8692
+ import { unlinkSync as unlinkSync2, existsSync as existsSync9 } from "fs";
8693
+ import { join as join10 } from "path";
7604
8694
  function cleanOldData(db, days) {
7605
8695
  const cutoff = Date.now() - days * 864e5;
7606
8696
  const recordsResult = db.prepare("DELETE FROM records WHERE ts < ?").run(cutoff);
@@ -7615,6 +8705,172 @@ function cleanOldData(db, days) {
7615
8705
  deletedOrphanToolCalls
7616
8706
  };
7617
8707
  }
8708
+ function cleanAll(db) {
8709
+ const recordsResult = db.prepare("DELETE FROM records").run();
8710
+ const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
8711
+ const syncedResult = db.prepare("DELETE FROM synced_records").run();
8712
+ const syncStateResult = db.prepare("DELETE FROM sync_record_state").run();
8713
+ const tombstonesResult = db.prepare("DELETE FROM sync_tombstones").run();
8714
+ const watermarkPath = join10(AIUSAGE_DIR, "watermark.json");
8715
+ let watermarkRemoved = false;
8716
+ if (existsSync9(watermarkPath)) {
8717
+ unlinkSync2(watermarkPath);
8718
+ watermarkRemoved = true;
8719
+ }
8720
+ return {
8721
+ deletedRecords: recordsResult.changes,
8722
+ deletedToolCalls: toolCallsResult.changes,
8723
+ deletedSyncedRecords: syncedResult.changes,
8724
+ deletedSyncRecordState: syncStateResult.changes,
8725
+ deletedTombstones: tombstonesResult.changes,
8726
+ watermarkRemoved
8727
+ };
8728
+ }
8729
+ function getRemoteBackends() {
8730
+ const config = loadConfig();
8731
+ if (!config?.sync) return [];
8732
+ const backends = [];
8733
+ if (config.sync.backend === "cloud") {
8734
+ backends.push({ type: "cloud", label: "AIUsage Cloud (cloud)" });
8735
+ } else if (config.sync.backend === "github" && config.sync.repo) {
8736
+ backends.push({ type: "github", label: `GitHub (${config.sync.repo})` });
8737
+ } else if (config.sync.backend === "s3" && config.sync.bucket) {
8738
+ backends.push({ type: "s3", label: `S3 (${config.sync.bucket})` });
8739
+ }
8740
+ return backends;
8741
+ }
8742
+ function createBackend2(config) {
8743
+ if (!config?.sync) return null;
8744
+ if (config.sync.backend === "github") {
8745
+ if (!config.sync.repo) return null;
8746
+ const token = loadCredential(`github/${config.sync.repo}/token`);
8747
+ if (!token) return null;
8748
+ return new GitSyncBackend({
8749
+ repo: config.sync.repo,
8750
+ token,
8751
+ cacheDir: join10(AIUSAGE_DIR, "sync-repo")
8752
+ });
8753
+ }
8754
+ if (config.sync.backend === "s3") {
8755
+ if (!config.sync.bucket) return null;
8756
+ const accessKeyId = loadCredential(`s3/${config.sync.bucket}/accessKeyId`);
8757
+ const secretAccessKey = loadCredential(`s3/${config.sync.bucket}/secretAccessKey`);
8758
+ if (!accessKeyId || !secretAccessKey) return null;
8759
+ return new S3SyncBackend({
8760
+ bucket: config.sync.bucket,
8761
+ prefix: config.sync.prefix ?? "aiusage/",
8762
+ accessKeyId,
8763
+ secretAccessKey,
8764
+ endpoint: config.sync.endpoint,
8765
+ region: config.sync.region
8766
+ });
8767
+ }
8768
+ return null;
8769
+ }
8770
+ async function propagateClean(options) {
8771
+ const config = loadConfig();
8772
+ if (!config?.sync) return { backends: [] };
8773
+ const results = [];
8774
+ if (config.sync.backend === "cloud") {
8775
+ if (!options.target || options.target === "cloud") {
8776
+ try {
8777
+ const result = await cloudClear();
8778
+ results.push({
8779
+ backend: { type: "cloud", label: "AIUsage Cloud" },
8780
+ status: "ok",
8781
+ detail: `generation updated to ${result.syncGeneration}`
8782
+ });
8783
+ } catch (err) {
8784
+ results.push({
8785
+ backend: { type: "cloud", label: "AIUsage Cloud" },
8786
+ status: "skipped",
8787
+ detail: err instanceof Error ? err.message : "unknown error"
8788
+ });
8789
+ }
8790
+ }
8791
+ return { backends: results };
8792
+ }
8793
+ if (options.target && options.target !== config.sync.backend) {
8794
+ return { backends: results };
8795
+ }
8796
+ const backend = createBackend2(config);
8797
+ if (!backend) {
8798
+ return { backends: [{
8799
+ backend: { type: config.sync.backend, label: config.sync.backend },
8800
+ status: "skipped",
8801
+ detail: "Could not create backend (missing credentials)"
8802
+ }] };
8803
+ }
8804
+ try {
8805
+ await backend.prepare?.();
8806
+ if (options.all) {
8807
+ const files = await backend.listFiles();
8808
+ const fileCount = files.length;
8809
+ if (fileCount > 0) {
8810
+ await backend.deleteAllData();
8811
+ }
8812
+ await backend.flush?.();
8813
+ results.push({
8814
+ backend: {
8815
+ type: config.sync.backend,
8816
+ label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
8817
+ },
8818
+ status: "ok",
8819
+ detail: `cleared ${fileCount} files`
8820
+ });
8821
+ } else if (options.beforeDays) {
8822
+ const cutoff = Date.now() - options.beforeDays * 864e5;
8823
+ const files = await backend.listFiles();
8824
+ let removedRecords = 0;
8825
+ let modifiedFiles = 0;
8826
+ for (const file of files) {
8827
+ const content = await backend.readFile(file);
8828
+ if (!content) continue;
8829
+ const lines = content.split("\n").filter(Boolean);
8830
+ const kept = [];
8831
+ for (const line of lines) {
8832
+ try {
8833
+ const record = JSON.parse(line);
8834
+ const ts3 = typeof record.ts === "string" ? new Date(record.ts).getTime() : record.ts;
8835
+ if (ts3 >= cutoff) {
8836
+ kept.push(line);
8837
+ } else {
8838
+ removedRecords++;
8839
+ }
8840
+ } catch {
8841
+ kept.push(line);
8842
+ }
8843
+ }
8844
+ if (kept.length === 0) {
8845
+ await backend.deleteFile?.(file);
8846
+ modifiedFiles++;
8847
+ } else if (kept.length < lines.length) {
8848
+ await backend.writeFile(file, kept.join("\n") + "\n");
8849
+ modifiedFiles++;
8850
+ }
8851
+ }
8852
+ await backend.flush?.();
8853
+ results.push({
8854
+ backend: {
8855
+ type: config.sync.backend,
8856
+ label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
8857
+ },
8858
+ status: "ok",
8859
+ detail: `removed ${removedRecords} records from ${modifiedFiles} files`
8860
+ });
8861
+ }
8862
+ } catch (err) {
8863
+ results.push({
8864
+ backend: {
8865
+ type: config.sync.backend,
8866
+ label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
8867
+ },
8868
+ status: "skipped",
8869
+ detail: err instanceof Error ? err.message : "unknown error"
8870
+ });
8871
+ }
8872
+ return { backends: results };
8873
+ }
7618
8874
 
7619
8875
  // src/sync/runtime.ts
7620
8876
  var SyncRuntimeController = class {
@@ -7712,6 +8968,7 @@ var RuntimeSettingsController = class {
7712
8968
  leaderboardUploadTimer = null;
7713
8969
  syncTimer = null;
7714
8970
  parseInFlight = false;
8971
+ cleanupInFlight = false;
7715
8972
  leaderboardUploadInFlight = false;
7716
8973
  started = false;
7717
8974
  constructor(options) {
@@ -7764,11 +9021,7 @@ var RuntimeSettingsController = class {
7764
9021
  }
7765
9022
  if (retentionDays > 0) {
7766
9023
  this.cleanupTimer = setInterval(() => {
7767
- try {
7768
- this.runCleanupFn(this.db, retentionDays);
7769
- } catch (err) {
7770
- console.error("[settings-controller] cleanup failed:", err);
7771
- }
9024
+ void this.runCleanupSafely(retentionDays);
7772
9025
  }, this.cleanupIntervalMs);
7773
9026
  }
7774
9027
  if (config?.leaderboardAutoUpload === true && leaderboardUploadInterval > 0 && this.runLeaderboardUploadFn) {
@@ -7801,6 +9054,17 @@ var RuntimeSettingsController = class {
7801
9054
  this.parseInFlight = false;
7802
9055
  }
7803
9056
  }
9057
+ async runCleanupSafely(retentionDays) {
9058
+ if (this.cleanupInFlight) return;
9059
+ this.cleanupInFlight = true;
9060
+ try {
9061
+ await this.runCleanupFn(this.db, retentionDays);
9062
+ } catch (err) {
9063
+ console.error("[settings-controller] cleanup failed:", err);
9064
+ } finally {
9065
+ this.cleanupInFlight = false;
9066
+ }
9067
+ }
7804
9068
  async runLeaderboardUploadSafely() {
7805
9069
  if (this.leaderboardUploadInFlight) return;
7806
9070
  this.leaderboardUploadInFlight = true;
@@ -7817,7 +9081,7 @@ var RuntimeSettingsController = class {
7817
9081
 
7818
9082
  // src/commands/serve.ts
7819
9083
  var MAX_PORT_ATTEMPTS = 10;
7820
- var PORT_FILE = join10(AIUSAGE_DIR, ".serve-port");
9084
+ var PORT_FILE = join11(AIUSAGE_DIR, ".serve-port");
7821
9085
  var MIME_TYPES = {
7822
9086
  ".html": "text/html",
7823
9087
  ".js": "application/javascript",
@@ -7832,11 +9096,8 @@ var MIME_TYPES = {
7832
9096
  };
7833
9097
  function serve(options) {
7834
9098
  const config = loadConfig();
7835
- if (config?.priceOverrides) {
7836
- for (const [model, entry] of Object.entries(config.priceOverrides)) {
7837
- setPriceOverride(model, entry);
7838
- }
7839
- }
9099
+ const dbWriteQueue = new AsyncTaskQueue();
9100
+ const runDbWrite = (task) => dbWriteQueue.run(task);
7840
9101
  if (config == null || config.exchangeRate == null) {
7841
9102
  const cacheAge = config?.exchangeRateCache ? Date.now() - config.exchangeRateCache.fetchedAt : Infinity;
7842
9103
  if (cacheAge >= CACHE_TTL_MS) {
@@ -7852,8 +9113,10 @@ function serve(options) {
7852
9113
  }
7853
9114
  const syncRuntime = new SyncRuntimeController({
7854
9115
  runSync: async (runtimeOptions) => {
7855
- await runParse(options.db);
7856
- await runSync(options.db, runtimeOptions);
9116
+ await runDbWrite(async () => {
9117
+ await runParse(options.db);
9118
+ await runSync(options.db, runtimeOptions);
9119
+ });
7857
9120
  },
7858
9121
  getPersistedState: () => getState(AIUSAGE_DIR),
7859
9122
  getCurrentTarget: () => getSyncTarget(loadConfig()?.sync)
@@ -7861,14 +9124,14 @@ function serve(options) {
7861
9124
  const runtimeSettings = new RuntimeSettingsController({
7862
9125
  db: options.db,
7863
9126
  loadConfig,
7864
- runParse,
7865
- runCleanup: cleanOldData,
9127
+ runParse: (db) => runDbWrite(() => runParse(db)),
9128
+ runCleanup: (db, retentionDays) => runDbWrite(() => cleanOldData(db, retentionDays)),
7866
9129
  runLeaderboardUpload: (db) => uploadLeaderboardData(db, getState(AIUSAGE_DIR)?.deviceInstanceId).then(() => void 0),
7867
9130
  runSync: () => syncRuntime.start(),
7868
9131
  onSyncScheduleChanged: (ts3) => syncRuntime.setNextSyncAt(ts3)
7869
9132
  });
7870
9133
  runtimeSettings.start();
7871
- runParse(options.db).catch((err) => {
9134
+ runDbWrite(() => runParse(options.db)).catch((err) => {
7872
9135
  console.error("[serve] initial parse failed:", err);
7873
9136
  });
7874
9137
  const apiServer = createApiServer(options.db, {
@@ -7876,14 +9139,16 @@ function serve(options) {
7876
9139
  onRefresh: () => runParse(options.db),
7877
9140
  onSyncStart: () => syncRuntime.start(),
7878
9141
  getSyncStatus: () => syncRuntime.getStatus(),
7879
- onConfigUpdated: () => runtimeSettings.reload()
9142
+ onConfigUpdated: () => runtimeSettings.reload(),
9143
+ runDbWrite,
9144
+ getDbWriteQueueStatus: () => dbWriteQueue.getStatus()
7880
9145
  });
7881
9146
  const webBuildDir = (() => {
7882
- const prodDir = join10(dirname3(fileURLToPath(import.meta.url)), "web");
7883
- if (existsSync9(prodDir)) return prodDir;
7884
- return join10(dirname3(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
9147
+ const prodDir = join11(dirname4(fileURLToPath(import.meta.url)), "web");
9148
+ if (existsSync10(prodDir)) return prodDir;
9149
+ return join11(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
7885
9150
  })();
7886
- if (!existsSync9(webBuildDir)) {
9151
+ if (!existsSync10(webBuildDir)) {
7887
9152
  console.error("Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage");
7888
9153
  process.exit(1);
7889
9154
  }
@@ -7893,19 +9158,19 @@ function serve(options) {
7893
9158
  apiServer.emit("request", req, res);
7894
9159
  return;
7895
9160
  }
7896
- if (existsSync9(webBuildDir)) {
7897
- let filePath = join10(webBuildDir, url.pathname);
9161
+ if (existsSync10(webBuildDir)) {
9162
+ let filePath = join11(webBuildDir, url.pathname);
7898
9163
  try {
7899
9164
  if (statSync3(filePath).isDirectory()) {
7900
- filePath = join10(filePath, "index.html");
9165
+ filePath = join11(filePath, "index.html");
7901
9166
  }
7902
9167
  } catch {
7903
9168
  }
7904
- if (!existsSync9(filePath)) {
7905
- filePath = join10(webBuildDir, "index.html");
9169
+ if (!existsSync10(filePath)) {
9170
+ filePath = join11(webBuildDir, "index.html");
7906
9171
  }
7907
9172
  try {
7908
- const content = readFileSync8(filePath);
9173
+ const content = readFileSync9(filePath);
7909
9174
  const ext = extname2(filePath);
7910
9175
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7911
9176
  res.writeHead(200, { "Content-Type": contentType });
@@ -7925,7 +9190,7 @@ function serve(options) {
7925
9190
  };
7926
9191
  server.on("listening", () => {
7927
9192
  started = true;
7928
- writeFileSync5(PORT_FILE, String(currentPort), "utf-8");
9193
+ writeFileSync6(PORT_FILE, String(currentPort), "utf-8");
7929
9194
  console.log(`aiusage serve listening on http://localhost:${currentPort}`);
7930
9195
  });
7931
9196
  server.on("error", (error) => {
@@ -7943,7 +9208,7 @@ function serve(options) {
7943
9208
  listenOnPort(options.port);
7944
9209
  const cleanup = () => {
7945
9210
  try {
7946
- unlinkSync2(PORT_FILE);
9211
+ unlinkSync3(PORT_FILE);
7947
9212
  } catch {
7948
9213
  }
7949
9214
  };
@@ -8087,9 +9352,9 @@ Consent recorded for schema v1.`
8087
9352
  }
8088
9353
 
8089
9354
  // src/commands/status.ts
8090
- import { statSync as statSync4, existsSync as existsSync10 } from "fs";
9355
+ import { statSync as statSync4, existsSync as existsSync11 } from "fs";
8091
9356
  import { hostname as hostname3 } from "os";
8092
- import { join as join11 } from "path";
9357
+ import { join as join12 } from "path";
8093
9358
  function formatFileSize(bytes) {
8094
9359
  if (bytes < 1024) return `${bytes} B`;
8095
9360
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -8107,9 +9372,9 @@ function generateStatus(db) {
8107
9372
  const schemaVersion = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get()?.version ?? 0;
8108
9373
  const tableCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get()?.count ?? 0;
8109
9374
  const viewCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'view'").get()?.count ?? 0;
8110
- const sizePath = join11(AIUSAGE_DIR, "cache.db");
9375
+ const sizePath = join12(AIUSAGE_DIR, "cache.db");
8111
9376
  let databaseSize = "0 B";
8112
- if (existsSync10(sizePath)) {
9377
+ if (existsSync11(sizePath)) {
8113
9378
  try {
8114
9379
  const stat2 = statSync4(sizePath);
8115
9380
  databaseSize = formatFileSize(stat2.size);
@@ -8186,27 +9451,6 @@ function exportData(db, format) {
8186
9451
  return lines.join("\n");
8187
9452
  }
8188
9453
 
8189
- // src/commands/reset.ts
8190
- import { unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
8191
- import { join as join12 } from "path";
8192
- function runReset(db) {
8193
- const recordsResult = db.prepare("DELETE FROM records").run();
8194
- const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
8195
- const syncedResult = db.prepare("DELETE FROM synced_records").run();
8196
- const watermarkPath = join12(AIUSAGE_DIR, "watermark.json");
8197
- let watermarkRemoved = false;
8198
- if (existsSync11(watermarkPath)) {
8199
- unlinkSync3(watermarkPath);
8200
- watermarkRemoved = true;
8201
- }
8202
- return {
8203
- deletedRecords: recordsResult.changes,
8204
- deletedToolCalls: toolCallsResult.changes,
8205
- deletedSyncedRecords: syncedResult.changes,
8206
- watermarkRemoved
8207
- };
8208
- }
8209
-
8210
9454
  // src/commands/recalc.ts
8211
9455
  var BATCH_SIZE = 1e3;
8212
9456
  function recalcPricing(db) {
@@ -8227,15 +9471,15 @@ function recalcPricing(db) {
8227
9471
  }
8228
9472
  const model = record.tool === "qoder" ? normalizeQoderModel(record.model) : record.model;
8229
9473
  const provider = model !== record.model ? inferProvider(model) : record.provider;
8230
- const hasPrice = resolvePrice(model) != null;
8231
- const newCost = hasPrice ? calculateCost(model, {
9474
+ const price = resolvePriceFromRegistry(db, model);
9475
+ const newCost = price ? calculateCostForPrice(price, {
8232
9476
  inputTokens: record.input_tokens,
8233
9477
  outputTokens: record.output_tokens,
8234
9478
  cacheReadTokens: record.cache_read_tokens,
8235
9479
  cacheWriteTokens: record.cache_write_tokens,
8236
9480
  thinkingTokens: record.thinking_tokens
8237
9481
  }, exchangeRate) : 0;
8238
- const costSource = hasPrice ? "pricing" : "unknown";
9482
+ const costSource = price ? "pricing" : "unknown";
8239
9483
  if (model !== record.model || provider !== record.provider || newCost !== record.cost || costSource !== record.cost_source) {
8240
9484
  updateStmt.run(model, provider, newCost, costSource, Date.now(), record.id);
8241
9485
  updatedCount++;
@@ -8319,7 +9563,7 @@ var SyncProgressReporter = class {
8319
9563
  };
8320
9564
 
8321
9565
  // src/commands/widget.ts
8322
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
9566
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
8323
9567
  import { spawn, execSync as execSync2 } from "child_process";
8324
9568
  import { join as join13 } from "path";
8325
9569
  var WIDGET_PID_PATH = join13(AIUSAGE_DIR, "widget.pid");
@@ -8347,7 +9591,7 @@ function isWidgetRunning() {
8347
9591
  if (!existsSync12(WIDGET_PID_PATH)) return false;
8348
9592
  let pid;
8349
9593
  try {
8350
- pid = parseInt(readFileSync9(WIDGET_PID_PATH, "utf-8").trim(), 10);
9594
+ pid = parseInt(readFileSync10(WIDGET_PID_PATH, "utf-8").trim(), 10);
8351
9595
  } catch {
8352
9596
  return false;
8353
9597
  }
@@ -8391,10 +9635,10 @@ function getDeviceName2() {
8391
9635
  }
8392
9636
  function getCliVersion2() {
8393
9637
  try {
8394
- const { readFileSync: readFileSync11 } = __require("fs");
9638
+ const { readFileSync: readFileSync12 } = __require("fs");
8395
9639
  const { join: join16 } = __require("path");
8396
9640
  const pkgPath = join16(__dirname, "../../package.json");
8397
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
9641
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
8398
9642
  return pkg.version || "0.0.0";
8399
9643
  } catch {
8400
9644
  return "0.0.0";
@@ -8638,7 +9882,7 @@ async function runLeaderboardView(options = {}) {
8638
9882
  // src/commands/menu.ts
8639
9883
  import { createInterface } from "readline";
8640
9884
  import { spawn as spawn2, spawnSync, execSync as execSync4 } from "child_process";
8641
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
9885
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
8642
9886
  import { join as join14 } from "path";
8643
9887
  var DEFAULT_PORT = 3847;
8644
9888
  var PORT_FILE2 = join14(AIUSAGE_DIR, ".serve-port");
@@ -8647,7 +9891,7 @@ function clearScreen() {
8647
9891
  }
8648
9892
  function getPort() {
8649
9893
  if (existsSync13(PORT_FILE2)) {
8650
- const raw = readFileSync10(PORT_FILE2, "utf-8").trim();
9894
+ const raw = readFileSync11(PORT_FILE2, "utf-8").trim();
8651
9895
  const n = parseInt(raw, 10);
8652
9896
  if (!isNaN(n) && n > 0) return n;
8653
9897
  }
@@ -8825,11 +10069,10 @@ async function dataSubmenu(rl) {
8825
10069
  [2] Summary
8826
10070
  [3] Export
8827
10071
  [4] Clean
8828
- [5] Reset
8829
- [6] Recalc
10072
+ [5] Recalc
8830
10073
  [0] Back
8831
10074
  `);
8832
- const choice = await prompt(rl, " Select [0-6]: ");
10075
+ const choice = await prompt(rl, " Select [0-5]: ");
8833
10076
  switch (choice) {
8834
10077
  case "1": {
8835
10078
  const progress = await prompt(rl, " Show progress? [Y/N]: ");
@@ -8865,28 +10108,28 @@ async function dataSubmenu(rl) {
8865
10108
  break;
8866
10109
  }
8867
10110
  case "4": {
8868
- const dur = await prompt(rl, " Duration to keep (default 180d): ");
8869
- const before = dur || "180d";
10111
+ const mode = await prompt(rl, " 1=Old data (180d) 2=Old data (custom) 3=All (=reset): ");
8870
10112
  console.log("");
8871
- runCommand(["clean", "--before", before, "--yes"]);
8872
- await prompt(rl, "\n Press Enter to continue...");
8873
- break;
8874
- }
8875
- case "5": {
8876
- const confirm = await prompt(
8877
- rl,
8878
- ' WARNING: This will delete ALL parsed data. Type "yes" to confirm: '
8879
- );
8880
- if (confirm.toLowerCase() === "yes") {
8881
- console.log("");
8882
- runCommand(["reset", "--yes"]);
10113
+ if (mode === "3") {
10114
+ const confirm = await prompt(
10115
+ rl,
10116
+ ' WARNING: This will delete ALL parsed data. Type "confirm" to proceed: '
10117
+ );
10118
+ if (confirm.toLowerCase() === "confirm") {
10119
+ runCommand(["clean", "--all", "--yes"]);
10120
+ } else {
10121
+ console.log(" Cancelled.");
10122
+ }
10123
+ } else if (mode === "2") {
10124
+ const dur = await prompt(rl, " Duration to keep (e.g. 30d, 90d): ");
10125
+ runCommand(["clean", "--before", dur || "180d", "--yes"]);
8883
10126
  } else {
8884
- console.log("\n Cancelled.");
10127
+ runCommand(["clean", "--before", "180d", "--yes"]);
8885
10128
  }
8886
10129
  await prompt(rl, "\n Press Enter to continue...");
8887
10130
  break;
8888
10131
  }
8889
- case "6": {
10132
+ case "5": {
8890
10133
  console.log("");
8891
10134
  runCommand(["recalc"]);
8892
10135
  await prompt(rl, "\n Press Enter to continue...");
@@ -9101,7 +10344,7 @@ async function runMenu() {
9101
10344
  ${dashboardStatusLine()}
9102
10345
 
9103
10346
  [1] Dashboard (serve/stop/restart/open)
9104
- [2] Data (parse/summary/export/clean/reset/recalc)
10347
+ [2] Data (parse/summary/export/clean/recalc)
9105
10348
  [3] Sync (init/sync)
9106
10349
  [4] Leaderboard (view/login/upload/status/logout)
9107
10350
  [5] System (status/widget/pm2/update)
@@ -9140,7 +10383,7 @@ ${dashboardStatusLine()}
9140
10383
  import { join as join15 } from "path";
9141
10384
  var DB_PATH2 = join15(AIUSAGE_DIR, "cache.db");
9142
10385
  var program = new Command();
9143
- program.name("aiusage").version(true ? "1.5.1" : "dev").description("CLI tool for AI usage statistics");
10386
+ program.name("aiusage").version(true ? "1.5.3" : "dev").description("CLI tool for AI usage statistics");
9144
10387
  program.action(() => {
9145
10388
  const unknownCommand = program.args.find((arg) => typeof arg === "string");
9146
10389
  if (unknownCommand) {
@@ -9222,7 +10465,7 @@ program.command("export").description("Export data").requiredOption("--format <f
9222
10465
  const db = createDatabase(DB_PATH2);
9223
10466
  const data = exportData(db, format);
9224
10467
  if (options.output) {
9225
- writeFileSync6(options.output, data, "utf-8");
10468
+ writeFileSync7(options.output, data, "utf-8");
9226
10469
  console.log(`Exported to ${options.output}`);
9227
10470
  } else {
9228
10471
  console.log(data);
@@ -9249,32 +10492,109 @@ program.command("parse").description("Parse AI tool session logs").option("--too
9249
10492
  db.close();
9250
10493
  }
9251
10494
  });
9252
- program.command("clean").description("Clean old data").option("--before <duration>", "Duration (e.g., 30d)", "180d").option("--remote", "Also clean remote data").option("--all-devices", "Clean all devices (with --remote)").option("--yes", "Skip confirmation").option("--approve-delete", "Approve delete permission upgrade").action((options) => {
9253
- const daysMatch = options.before.match(/^(\d+)d$/);
9254
- if (!daysMatch) {
9255
- console.error("Invalid duration format. Use e.g. 30d, 180d.");
10495
+ program.command("clean").description("Clean data (old records or all data)").option("--before <duration>", "Delete data older than this (e.g., 30d, 180d)", "180d").option("--all", "Wipe all data (equivalent to former reset: all records, tool calls, watermark)").option("--local-only", "Only clean local data, do not propagate to remote backends").option("--target <backend>", "Target specific remote backend (github/s3/cloud)").option("--yes", "Skip local confirmation (remote deletion still requires confirmation)").action(async (options) => {
10496
+ const isAll = !!options.all;
10497
+ const localOnly = !!options.localOnly;
10498
+ let days = 0;
10499
+ if (isAll && options.before !== "180d") {
10500
+ console.error("Cannot use --all and --before together. Use one or the other.");
9256
10501
  process.exit(1);
9257
10502
  }
9258
- const days = parseInt(daysMatch[1], 10);
9259
- const db = createDatabase(DB_PATH2);
9260
- const result = cleanOldData(db, days);
9261
- console.log(`Deleted ${result.deletedCount} records, ${result.deletedSyncedCount} synced records, ${result.deletedOrphanToolCalls} orphan tool calls.`);
9262
- db.close();
9263
- });
9264
- program.command("reset").description("Delete all parsed data and watermark, then re-parse from scratch").option("--yes", "Skip confirmation").action((options) => {
10503
+ if (!isAll) {
10504
+ const daysMatch = options.before.match(/^(\d+)d$/);
10505
+ if (!daysMatch) {
10506
+ console.error("Invalid duration format. Use e.g. 30d, 180d.");
10507
+ process.exit(1);
10508
+ }
10509
+ days = parseInt(daysMatch[1], 10);
10510
+ }
10511
+ const remoteBackends = localOnly ? [] : getRemoteBackends();
10512
+ if (isAll) {
10513
+ console.log("\x1B[33m\u26A0 WARNING: This will delete ALL parsed data, tool calls, synced records,\x1B[0m");
10514
+ console.log("\x1B[33m and the parse watermark. Next parse will re-import from scratch.\x1B[0m");
10515
+ } else {
10516
+ console.log(`This will delete local records older than ${days} days.`);
10517
+ }
10518
+ if (localOnly) {
10519
+ console.log("\x1B[36m (--local-only: remote data will NOT be affected)\x1B[0m");
10520
+ }
10521
+ if (remoteBackends.length > 0) {
10522
+ console.log("\nCloud sync is configured. The following remote backends will also be affected:");
10523
+ for (const rb of remoteBackends) {
10524
+ console.log(` - ${rb.label}`);
10525
+ }
10526
+ }
10527
+ const ask = async (question) => {
10528
+ if (!process.stdin.isTTY) {
10529
+ console.error("Non-interactive mode detected. Use --local-only to skip remote propagation, or run interactively.");
10530
+ process.exit(1);
10531
+ }
10532
+ const { createInterface: createInterface2 } = await import("readline");
10533
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
10534
+ return new Promise((resolve2) => {
10535
+ rl.question(question, (answer) => {
10536
+ rl.close();
10537
+ resolve2(answer.trim());
10538
+ });
10539
+ });
10540
+ };
9265
10541
  if (!options.yes) {
9266
- console.error("This will delete ALL parsed records, tool calls, synced data, and the parse watermark.");
9267
- console.error("Source log files (~/.claude, ~/.codex, etc.) will NOT be affected.");
9268
- console.error("Use --yes to confirm.");
9269
- process.exit(1);
10542
+ if (remoteBackends.length > 0) {
10543
+ const answer = await ask(`
10544
+ ? Confirm deletion on local + ${remoteBackends.length} remote backend(s)? [y/N] `);
10545
+ if (answer.toLowerCase() !== "y") {
10546
+ console.log("Cancelled.");
10547
+ process.exit(0);
10548
+ }
10549
+ } else {
10550
+ const answer = await ask("\n? Confirm? [y/N] ");
10551
+ if (answer.toLowerCase() !== "y") {
10552
+ console.log("Cancelled.");
10553
+ process.exit(0);
10554
+ }
10555
+ }
10556
+ } else if (remoteBackends.length > 0) {
10557
+ const answer = await ask('\n? Type "confirm" to proceed with remote deletion: ');
10558
+ if (answer !== "confirm") {
10559
+ console.log("Cancelled.");
10560
+ process.exit(0);
10561
+ }
9270
10562
  }
9271
10563
  const db = createDatabase(DB_PATH2);
9272
- const result = runReset(db);
9273
- console.log(`Deleted ${result.deletedRecords} records, ${result.deletedToolCalls} tool calls, ${result.deletedSyncedRecords} synced records.`);
9274
- if (result.watermarkRemoved) {
9275
- console.log("Watermark removed \u2014 next parse will re-import all data from scratch.");
10564
+ if (isAll) {
10565
+ console.log("\nDeleting all local data...");
10566
+ const result = cleanAll(db);
10567
+ console.log(` Local: deleted ${result.deletedRecords} records, ${result.deletedToolCalls} tool calls, ${result.deletedSyncedRecords} synced records`);
10568
+ if (result.watermarkRemoved) {
10569
+ console.log(" Watermark removed");
10570
+ }
10571
+ } else {
10572
+ console.log(`
10573
+ Cleaning local records older than ${days} days...`);
10574
+ const result = cleanOldData(db, days);
10575
+ console.log(` Local: deleted ${result.deletedCount} records, ${result.deletedSyncedCount} synced records, ${result.deletedOrphanToolCalls} orphan tool calls`);
9276
10576
  }
9277
10577
  db.close();
10578
+ if (remoteBackends.length > 0) {
10579
+ console.log("\nSyncing deletion to remote backends...");
10580
+ try {
10581
+ const propagation = await propagateClean({
10582
+ all: isAll,
10583
+ beforeDays: isAll ? void 0 : days,
10584
+ target: options.target
10585
+ });
10586
+ for (const r of propagation.backends) {
10587
+ if (r.status === "ok") {
10588
+ console.log(` ${r.backend.label}: ${r.detail}`);
10589
+ } else {
10590
+ console.log(` ${r.backend.label}: skipped \u2014 ${r.detail}`);
10591
+ }
10592
+ }
10593
+ } catch (err) {
10594
+ console.error(` Propagation failed: ${err instanceof Error ? err.message : err}`);
10595
+ }
10596
+ }
10597
+ console.log("\nDone.");
9278
10598
  });
9279
10599
  program.command("recalc").description("Recalculate costs").option("--pricing", "Recalculate using latest pricing").action(() => {
9280
10600
  const db = createDatabase(DB_PATH2);
@@ -9351,7 +10671,7 @@ function hasWidget() {
9351
10671
  }
9352
10672
  function buildPm2Config(includeWidget) {
9353
10673
  const wrapperPath = join15(AIUSAGE_DIR, "pm2-server.cjs");
9354
- writeFileSync6(wrapperPath, [
10674
+ writeFileSync7(wrapperPath, [
9355
10675
  `// Auto-generated by aiusage pm2-start \u2014 do not edit`,
9356
10676
  `const { execSync, spawn } = require('child_process')`,
9357
10677
  `const { dirname, join } = require('path')`,
@@ -9398,7 +10718,7 @@ program.command("pm2-setup").description("Generate ecosystem.config.cjs for PM2
9398
10718
  const includeWidget = options.serverOnly ? false : hasWidget();
9399
10719
  const config = buildPm2Config(includeWidget);
9400
10720
  const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
9401
- writeFileSync6(configPath, config, "utf-8");
10721
+ writeFileSync7(configPath, config, "utf-8");
9402
10722
  console.log(`Config created: ${configPath}`);
9403
10723
  if (!includeWidget) {
9404
10724
  console.log("Note: aiusage-widget not found, only server is configured.");
@@ -9408,7 +10728,7 @@ program.command("pm2-start").description("Setup and start PM2 background service
9408
10728
  const includeWidget = options.serverOnly ? false : hasWidget();
9409
10729
  const config = buildPm2Config(includeWidget);
9410
10730
  const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
9411
- writeFileSync6(configPath, config, "utf-8");
10731
+ writeFileSync7(configPath, config, "utf-8");
9412
10732
  console.log(`Config: ${configPath}`);
9413
10733
  try {
9414
10734
  execSync5(`pm2 start "${configPath}"`, { stdio: "inherit" });