@juliantanx/aiusage 1.5.0 → 1.5.2

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 (36) hide show
  1. package/README.md +5 -15
  2. package/dist/index.js +849 -259
  3. package/dist/web/_app/immutable/assets/12.C1f-IaHK.css +1 -0
  4. package/dist/web/_app/immutable/assets/_page.DO7aPEA-.css +1 -0
  5. package/dist/web/_app/immutable/chunks/{ToolSelector.CofvKVw2.js → ToolSelector.DTPGX4zP.js} +1 -1
  6. package/dist/web/_app/immutable/chunks/api.DC92Gfvo.js +1 -0
  7. package/dist/web/_app/immutable/chunks/{entry.PDoHFbXz.js → entry.Cxpjn2cB.js} +2 -2
  8. package/dist/web/_app/immutable/chunks/i18n.yxU9Ieh-.js +1 -0
  9. package/dist/web/_app/immutable/chunks/{stores.Botc0ioY.js → stores.CK2Vof2m.js} +1 -1
  10. package/dist/web/_app/immutable/entry/{app.CoPtpbXI.js → app.BzFxp-0D.js} +2 -2
  11. package/dist/web/_app/immutable/entry/start.C2M6huNJ.js +1 -0
  12. package/dist/web/_app/immutable/nodes/{0.CZMa2uzk.js → 0.5419U2xS.js} +2 -2
  13. package/dist/web/_app/immutable/nodes/{1.DJI2NNga.js → 1.C4em9nwH.js} +1 -1
  14. package/dist/web/_app/immutable/nodes/{10.DTaETIM6.js → 10.DGXLHPeN.js} +1 -1
  15. package/dist/web/_app/immutable/nodes/{11.CScZqjVc.js → 11.SDWfdWaC.js} +1 -1
  16. package/dist/web/_app/immutable/nodes/12.B5loVhie.js +1 -0
  17. package/dist/web/_app/immutable/nodes/{13.BFaBQy6e.js → 13.9M-J-sT0.js} +1 -1
  18. package/dist/web/_app/immutable/nodes/{14.BgJEC28r.js → 14.F_F2PTIe.js} +1 -1
  19. package/dist/web/_app/immutable/nodes/{15.DjuCO6OE.js → 15.B0ylahpi.js} +1 -1
  20. package/dist/web/_app/immutable/nodes/{2.CoxR54dV.js → 2.oTDdkLn9.js} +1 -1
  21. package/dist/web/_app/immutable/nodes/{3.Bua2oewW.js → 3.Oyghjrgn.js} +1 -1
  22. package/dist/web/_app/immutable/nodes/{4.DJwmYYKI.js → 4.DiPBEiKy.js} +1 -1
  23. package/dist/web/_app/immutable/nodes/{5.CXmtjVJL.js → 5.CtE-oF5k.js} +1 -1
  24. package/dist/web/_app/immutable/nodes/{6.kpxBlrHt.js → 6.tVPvf7jD.js} +1 -1
  25. package/dist/web/_app/immutable/nodes/{7.LSfdx4vz.js → 7.CAfC8ItM.js} +1 -1
  26. package/dist/web/_app/immutable/nodes/{8.ByAI01dT.js → 8.DJsWN3X2.js} +1 -1
  27. package/dist/web/_app/immutable/nodes/{9.Ubt4Bfxd.js → 9.30YXO0yd.js} +1 -1
  28. package/dist/web/_app/version.json +1 -1
  29. package/dist/web/index.html +6 -6
  30. package/package.json +7 -5
  31. package/dist/web/_app/immutable/assets/12.nNHFRI35.css +0 -1
  32. package/dist/web/_app/immutable/assets/_page.ZSDsIDry.css +0 -1
  33. package/dist/web/_app/immutable/chunks/api.Mv_KcGhp.js +0 -1
  34. package/dist/web/_app/immutable/chunks/i18n.CwV3KceS.js +0 -1
  35. package/dist/web/_app/immutable/entry/start.CaOJNprV.js +0 -1
  36. package/dist/web/_app/immutable/nodes/12.DeLNKhlw.js +0 -1
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 dirname3 } 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",
@@ -1720,6 +1723,13 @@ function probeKiloTasks(ctx) {
1720
1723
  }
1721
1724
  return null;
1722
1725
  }
1726
+ function probeKelivo(ctx) {
1727
+ const override = envOverride("kelivo", ctx.env);
1728
+ if (override) return override;
1729
+ const legacy = ctx.legacySources?.["kelivo"];
1730
+ if (legacy) return legacy;
1731
+ return null;
1732
+ }
1723
1733
  function probeCopilot(ctx) {
1724
1734
  const override = envOverride("copilot", ctx.env);
1725
1735
  if (override) return override;
@@ -1905,6 +1915,7 @@ var TOOL_REGISTRY = [
1905
1915
  { tool: "cursor", sourceKey: "cursor", label: "Cursor", probe: probeCursor },
1906
1916
  { tool: "kilocode", sourceKey: "kilocode", label: "Kilo Code (extension)", probe: probeKiloTasks },
1907
1917
  { tool: "kilocode", sourceKey: "kilocode-db", label: "KiloCode", probe: probeKiloDb },
1918
+ { tool: "kelivo", sourceKey: "kelivo", label: "Kelivo", probe: probeKelivo },
1908
1919
  { tool: "copilot", sourceKey: "copilot", label: "Copilot", probe: probeCopilot },
1909
1920
  { tool: "gemini", sourceKey: "gemini", label: "Gemini CLI", probe: probeGemini },
1910
1921
  { tool: "kimi", sourceKey: "kimi", label: "Kimi Code", probe: probeKimi },
@@ -1926,11 +1937,11 @@ function discoverTools(env = process.env) {
1926
1937
  const legacySources = config?.sources;
1927
1938
  const ctx = { home, env, legacySources };
1928
1939
  return TOOL_REGISTRY.map((entry) => {
1929
- const path2 = entry.probe(ctx);
1930
- if (!path2) {
1940
+ const path3 = entry.probe(ctx);
1941
+ if (!path3) {
1931
1942
  return { tool: entry.tool, sourceKey: entry.sourceKey, label: entry.label, path: "", fileCount: 0, status: "not_found" };
1932
1943
  }
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];
1944
+ 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
1945
  let fileCount = 0;
1935
1946
  for (const detectedPath of detectedPaths) {
1936
1947
  try {
@@ -1938,6 +1949,8 @@ function discoverTools(env = process.env) {
1938
1949
  if (stat2.isDirectory()) {
1939
1950
  if (entry.sourceKey === "roocode" || entry.sourceKey === "kilocode") {
1940
1951
  fileCount += findJsonFiles(detectedPath).filter((p) => basename(p) === "ui_messages.json").length;
1952
+ } else if (entry.sourceKey === "kelivo") {
1953
+ fileCount += findJsonFiles(detectedPath).filter((p) => basename(p) === "chats.json").length + findZipFiles(detectedPath).length;
1941
1954
  } else if (entry.sourceKey === "kiro") {
1942
1955
  fileCount += unique([...findJsonlFiles(detectedPath), ...findJsonFiles(detectedPath)]).length;
1943
1956
  } else {
@@ -1955,7 +1968,7 @@ function discoverTools(env = process.env) {
1955
1968
  tool: entry.tool,
1956
1969
  sourceKey: entry.sourceKey,
1957
1970
  label: entry.label,
1958
- path: path2,
1971
+ path: path3,
1959
1972
  paths: visiblePaths.length > 1 || entry.sourceKey === "codex" && visiblePaths.length > 0 ? visiblePaths : void 0,
1960
1973
  fileCount,
1961
1974
  status
@@ -2057,6 +2070,11 @@ function discoverLogFiles(env = process.env) {
2057
2070
  const paths = findJsonFiles(kilocodeTaskPath).filter((p) => basename(p) === "ui_messages.json");
2058
2071
  if (paths.length > 0) results.push({ tool: "kilocode", paths: unique(paths) });
2059
2072
  }
2073
+ const kelivoPath = probeKelivo(ctx);
2074
+ if (kelivoPath && existsSync3(kelivoPath)) {
2075
+ const paths = statSync(kelivoPath).isDirectory() ? [...findJsonFiles(kelivoPath).filter((p) => basename(p) === "chats.json"), ...findZipFiles(kelivoPath)] : basename(kelivoPath) === "chats.json" || basename(kelivoPath).endsWith(".zip") ? [kelivoPath] : [];
2076
+ if (paths.length > 0) results.push({ tool: "kelivo", paths: unique(paths) });
2077
+ }
2060
2078
  return results;
2061
2079
  }
2062
2080
  function findJsonFiles(dir) {
@@ -2075,6 +2093,22 @@ function findJsonFiles(dir) {
2075
2093
  }
2076
2094
  return results;
2077
2095
  }
2096
+ function findZipFiles(dir) {
2097
+ const results = [];
2098
+ try {
2099
+ const entries = readdirSync(dir, { withFileTypes: true });
2100
+ for (const entry of entries) {
2101
+ const fullPath = join3(dir, entry.name);
2102
+ if (entry.isDirectory()) {
2103
+ results.push(...findZipFiles(fullPath));
2104
+ } else if (entry.isFile() && extname(entry.name) === ".zip") {
2105
+ results.push(fullPath);
2106
+ }
2107
+ }
2108
+ } catch {
2109
+ }
2110
+ return results;
2111
+ }
2078
2112
  function normalizeQoderPath(source) {
2079
2113
  const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
2080
2114
  if (normalized.endsWith("/logs/sessions") || normalized.endsWith("/sessions") || normalized.endsWith("/segments")) {
@@ -2557,9 +2591,7 @@ function loadLocalEnv() {
2557
2591
  if (envLoaded) return;
2558
2592
  envLoaded = true;
2559
2593
  const candidates = [
2560
- resolve(process.cwd(), ".env"),
2561
- resolve(process.cwd(), "packages/site/.env"),
2562
- resolve(process.cwd(), "../site/.env")
2594
+ resolve(process.cwd(), ".env")
2563
2595
  ];
2564
2596
  for (const envPath of candidates) {
2565
2597
  if (!existsSync5(envPath)) continue;
@@ -2596,8 +2628,8 @@ function generateNonce() {
2596
2628
  function generateIdempotencyKey() {
2597
2629
  return randomBytes(16).toString("hex");
2598
2630
  }
2599
- function buildCanonicalString(method, path2, bodyHash, timestamp, nonce, deviceId, idempotencyKey) {
2600
- return [method, path2, bodyHash, timestamp, nonce, deviceId, idempotencyKey].join("\n");
2631
+ function buildCanonicalString(method, path3, bodyHash, timestamp, nonce, deviceId, idempotencyKey) {
2632
+ return [method, path3, bodyHash, timestamp, nonce, deviceId, idempotencyKey].join("\n");
2601
2633
  }
2602
2634
  function base64url(data) {
2603
2635
  const buf = typeof data === "string" ? Buffer.from(data) : data;
@@ -2670,8 +2702,8 @@ async function readJsonOrNull(response) {
2670
2702
  if (!contentType.includes("application/json")) return null;
2671
2703
  return response.json().catch(() => null);
2672
2704
  }
2673
- function endpointUnavailableMessage(path2, status) {
2674
- return `Leaderboard API endpoint ${path2} is unavailable (HTTP ${status}). The site deployment is not routing this API request to the Node server.`;
2705
+ function endpointUnavailableMessage(path3, status) {
2706
+ return `Leaderboard API endpoint ${path3} is unavailable (HTTP ${status}). The site deployment is not routing this API request to the Node server.`;
2675
2707
  }
2676
2708
  async function fetchLeaderboard(serverUrl, options = {}) {
2677
2709
  const params = new URLSearchParams();
@@ -3166,9 +3198,9 @@ function initializeDatabase(db) {
3166
3198
  applyPragmas(db);
3167
3199
  runMigrations(db);
3168
3200
  }
3169
- function createDatabase(path2) {
3170
- mkdirSync3(dirname2(path2), { recursive: true, mode: 448 });
3171
- const db = new Database(path2);
3201
+ function createDatabase(path3) {
3202
+ mkdirSync3(dirname2(path3), { recursive: true, mode: 448 });
3203
+ const db = new Database(path3);
3172
3204
  initializeDatabase(db);
3173
3205
  return db;
3174
3206
  }
@@ -3366,10 +3398,10 @@ function getClientPlatform() {
3366
3398
  }
3367
3399
  function getCliVersion() {
3368
3400
  try {
3369
- const { readFileSync: readFileSync11 } = __require("fs");
3401
+ const { readFileSync: readFileSync12 } = __require("fs");
3370
3402
  const { join: join16 } = __require("path");
3371
3403
  const pkgPath = join16(__dirname, "../../package.json");
3372
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3404
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3373
3405
  return pkg.version || "0.0.0";
3374
3406
  } catch {
3375
3407
  return "0.0.0";
@@ -3647,8 +3679,273 @@ Summary: ${accepted} accepted, ${flagged} flagged, ${rejected} rejected`);
3647
3679
  }
3648
3680
  }
3649
3681
 
3682
+ // src/commands/parse-kelivo.ts
3683
+ import { readFileSync as readFileSync6 } from "fs";
3684
+ import { basename as basename2 } from "path";
3685
+ import * as yauzl from "yauzl";
3686
+ function toTokenCount(value) {
3687
+ const n = Number(value);
3688
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
3689
+ }
3690
+ function parseTimestamp(value, fallback) {
3691
+ if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
3692
+ if (typeof value === "string" && value.trim()) {
3693
+ const parsed = new Date(value).getTime();
3694
+ if (Number.isFinite(parsed)) return parsed;
3695
+ }
3696
+ return fallback;
3697
+ }
3698
+ async function readZipEntryText(filePath, entryName) {
3699
+ return new Promise((resolve2, reject) => {
3700
+ yauzl.open(filePath, { lazyEntries: true }, (openError, zipfile) => {
3701
+ if (openError) {
3702
+ reject(openError);
3703
+ return;
3704
+ }
3705
+ if (!zipfile) {
3706
+ reject(new Error("Unable to open zip file"));
3707
+ return;
3708
+ }
3709
+ let settled = false;
3710
+ zipfile.readEntry();
3711
+ zipfile.on("entry", (entry) => {
3712
+ if (basename2(entry.fileName) !== entryName) {
3713
+ zipfile.readEntry();
3714
+ return;
3715
+ }
3716
+ zipfile.openReadStream(entry, (streamError, stream) => {
3717
+ if (streamError) {
3718
+ settled = true;
3719
+ zipfile.close();
3720
+ reject(streamError);
3721
+ return;
3722
+ }
3723
+ if (!stream) {
3724
+ settled = true;
3725
+ zipfile.close();
3726
+ reject(new Error(`Missing ${entryName} stream`));
3727
+ return;
3728
+ }
3729
+ const chunks = [];
3730
+ stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
3731
+ stream.on("error", (e) => {
3732
+ settled = true;
3733
+ zipfile.close();
3734
+ reject(e);
3735
+ });
3736
+ stream.on("end", () => {
3737
+ settled = true;
3738
+ zipfile.close();
3739
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3740
+ });
3741
+ });
3742
+ });
3743
+ zipfile.on("end", () => {
3744
+ if (!settled) reject(new Error(`${entryName} not found in zip`));
3745
+ });
3746
+ zipfile.on("error", (e) => {
3747
+ if (!settled) reject(e);
3748
+ });
3749
+ });
3750
+ });
3751
+ }
3752
+ async function readKelivoExport(filePath) {
3753
+ const text = filePath.endsWith(".zip") ? await readZipEntryText(filePath, "chats.json") : readFileSync6(filePath, "utf-8");
3754
+ return JSON.parse(text);
3755
+ }
3756
+ async function runParseKelivo(options) {
3757
+ const { filePath, device, deviceInstanceId, platform: platform5, now, exchangeRate } = options;
3758
+ const records = [];
3759
+ const errors = [];
3760
+ let parsed;
3761
+ try {
3762
+ parsed = await readKelivoExport(filePath);
3763
+ } catch (e) {
3764
+ return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
3765
+ }
3766
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
3767
+ for (const [index, message] of messages.entries()) {
3768
+ if (message?.role !== "assistant") continue;
3769
+ const promptTokens = toTokenCount(message.promptTokens);
3770
+ const completionTokens = toTokenCount(message.completionTokens);
3771
+ const cachedTokens = toTokenCount(message.cachedTokens);
3772
+ const totalTokens = toTokenCount(message.totalTokens);
3773
+ const inputTokens = promptTokens > 0 ? Math.max(0, promptTokens - cachedTokens) : Math.max(0, totalTokens - completionTokens - cachedTokens);
3774
+ const outputTokens = completionTokens;
3775
+ const cacheReadTokens = cachedTokens;
3776
+ const cacheWriteTokens = 0;
3777
+ const thinkingTokens = 0;
3778
+ if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
3779
+ const model = typeof message.modelId === "string" && message.modelId.trim() ? message.modelId.trim() : "unknown";
3780
+ const provider = typeof message.providerId === "string" && message.providerId.trim() ? message.providerId.trim() : inferProvider(model);
3781
+ const ts3 = parseTimestamp(message.timestamp, now);
3782
+ const sessionId = typeof message.conversationId === "string" && message.conversationId.trim() ? message.conversationId.trim() : "unknown";
3783
+ const messageId = typeof message.id === "string" && message.id.trim() ? message.id.trim() : `${sessionId}:${ts3}:${index}`;
3784
+ const sourceKey = `kelivo:${sessionId}:${messageId}`;
3785
+ const tokenArgs = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, thinkingTokens };
3786
+ const cost = model !== "unknown" ? calculateCost(model, tokenArgs, exchangeRate) : 0;
3787
+ records.push({
3788
+ id: generateRecordId(deviceInstanceId, sourceKey, ts3),
3789
+ ts: ts3,
3790
+ ingestedAt: now,
3791
+ updatedAt: now,
3792
+ lineOffset: index,
3793
+ tool: "kelivo",
3794
+ model,
3795
+ provider,
3796
+ inputTokens,
3797
+ outputTokens,
3798
+ cacheReadTokens,
3799
+ cacheWriteTokens,
3800
+ thinkingTokens,
3801
+ cost,
3802
+ costSource: cost > 0 ? "pricing" : "unknown",
3803
+ sessionId,
3804
+ sourceFile: filePath,
3805
+ device,
3806
+ deviceInstanceId,
3807
+ platform: platform5
3808
+ });
3809
+ }
3810
+ return { records, errors };
3811
+ }
3812
+
3813
+ // src/db/records.ts
3814
+ function insertRecord(db, record) {
3815
+ db.prepare(`
3816
+ INSERT OR REPLACE INTO records (
3817
+ id, ts, ingested_at, synced_at, updated_at, line_offset,
3818
+ tool, model, provider, input_tokens, output_tokens,
3819
+ cache_read_tokens, cache_write_tokens, thinking_tokens,
3820
+ cost, cost_source, session_id, source_file, cwd, device, device_instance_id, platform
3821
+ ) VALUES (
3822
+ @id, @ts, @ingestedAt, @syncedAt, @updatedAt, @lineOffset,
3823
+ @tool, @model, @provider, @inputTokens, @outputTokens,
3824
+ @cacheReadTokens, @cacheWriteTokens, @thinkingTokens,
3825
+ @cost, @costSource, @sessionId, @sourceFile, @cwd, @device, @deviceInstanceId, @platform
3826
+ )
3827
+ `).run({
3828
+ id: record.id,
3829
+ ts: record.ts,
3830
+ ingestedAt: record.ingestedAt,
3831
+ syncedAt: record.syncedAt ?? null,
3832
+ updatedAt: record.updatedAt,
3833
+ lineOffset: record.lineOffset,
3834
+ tool: record.tool,
3835
+ model: record.model,
3836
+ provider: record.provider,
3837
+ inputTokens: record.inputTokens,
3838
+ outputTokens: record.outputTokens,
3839
+ cacheReadTokens: record.cacheReadTokens,
3840
+ cacheWriteTokens: record.cacheWriteTokens,
3841
+ thinkingTokens: record.thinkingTokens,
3842
+ cost: record.cost,
3843
+ costSource: record.costSource,
3844
+ sessionId: record.sessionId,
3845
+ sourceFile: record.sourceFile,
3846
+ cwd: record.cwd ?? "",
3847
+ device: record.device,
3848
+ deviceInstanceId: record.deviceInstanceId,
3849
+ platform: record.platform ?? ""
3850
+ });
3851
+ }
3852
+ function getUnsyncedRecords(db, target) {
3853
+ const rows = target ? db.prepare(`
3854
+ SELECT r.* FROM records r
3855
+ LEFT JOIN sync_record_state s
3856
+ ON s.record_id = r.id AND s.target = ?
3857
+ WHERE r.source_file NOT LIKE 'synced/%'
3858
+ AND (s.synced_at IS NULL OR r.updated_at > s.synced_at)
3859
+ `).all(target) : db.prepare(
3860
+ "SELECT * FROM records WHERE synced_at IS NULL OR updated_at > synced_at"
3861
+ ).all();
3862
+ return rows.map(mapRowToRecord);
3863
+ }
3864
+ function markRecordsSynced(db, ids, syncedAt, target) {
3865
+ if (ids.length === 0) return;
3866
+ if (target) {
3867
+ const insertStmt = db.prepare(`
3868
+ INSERT INTO sync_record_state (record_id, target, synced_at)
3869
+ VALUES (?, ?, ?)
3870
+ ON CONFLICT(record_id, target) DO UPDATE SET synced_at = excluded.synced_at
3871
+ `);
3872
+ const legacyStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
3873
+ const tx2 = db.transaction((recordIds) => {
3874
+ for (const id of recordIds) {
3875
+ insertStmt.run(id, target, syncedAt);
3876
+ legacyStmt.run(syncedAt, id);
3877
+ }
3878
+ });
3879
+ tx2(ids);
3880
+ return;
3881
+ }
3882
+ const updateStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
3883
+ const tx = db.transaction((recordIds) => {
3884
+ for (const id of recordIds) updateStmt.run(syncedAt, id);
3885
+ });
3886
+ tx(ids);
3887
+ }
3888
+ function mapRowToRecord(row) {
3889
+ return {
3890
+ id: row.id,
3891
+ ts: row.ts,
3892
+ ingestedAt: row.ingested_at,
3893
+ syncedAt: row.synced_at != null ? row.synced_at : void 0,
3894
+ updatedAt: row.updated_at,
3895
+ lineOffset: row.line_offset,
3896
+ tool: row.tool,
3897
+ model: row.model,
3898
+ provider: row.provider,
3899
+ inputTokens: row.input_tokens,
3900
+ outputTokens: row.output_tokens,
3901
+ cacheReadTokens: row.cache_read_tokens,
3902
+ cacheWriteTokens: row.cache_write_tokens,
3903
+ thinkingTokens: row.thinking_tokens,
3904
+ cost: row.cost,
3905
+ costSource: row.cost_source,
3906
+ sessionId: row.session_id,
3907
+ sourceFile: row.source_file,
3908
+ cwd: row.cwd || void 0,
3909
+ device: row.device,
3910
+ deviceInstanceId: row.device_instance_id,
3911
+ platform: row.platform || void 0
3912
+ };
3913
+ }
3914
+
3650
3915
  // src/api/server.ts
3651
3916
  var pendingLeaderboardAuth = /* @__PURE__ */ new Map();
3917
+ function getManualImportMetadata(db) {
3918
+ const rows = db.prepare(`
3919
+ SELECT tool, MAX(ingested_at) AS lastImportedAt
3920
+ FROM records
3921
+ WHERE tool IN ('kelivo')
3922
+ GROUP BY tool
3923
+ `).all();
3924
+ const metadata = {};
3925
+ for (const row of rows) {
3926
+ if (typeof row.lastImportedAt === "number") {
3927
+ metadata[row.tool] = { lastImportedAt: row.lastImportedAt };
3928
+ }
3929
+ }
3930
+ return metadata;
3931
+ }
3932
+ function attachManualImportMetadata(tools, db) {
3933
+ const metadata = getManualImportMetadata(db);
3934
+ return tools.map((tool) => {
3935
+ const toolMetadata = metadata[tool.tool];
3936
+ return toolMetadata ? { ...tool, ...toolMetadata } : tool;
3937
+ });
3938
+ }
3939
+ function countExistingRecordIds(db, ids) {
3940
+ const uniqueIds = [...new Set(ids)];
3941
+ if (uniqueIds.length === 0) return 0;
3942
+ const stmt = db.prepare("SELECT 1 FROM records WHERE id = ? LIMIT 1");
3943
+ let count = 0;
3944
+ for (const id of uniqueIds) {
3945
+ if (stmt.get(id)) count++;
3946
+ }
3947
+ return count;
3948
+ }
3652
3949
  function recalcCosts(db) {
3653
3950
  const BATCH_SIZE2 = 1e3;
3654
3951
  let updated = 0;
@@ -3725,6 +4022,44 @@ async function readJsonBody(req) {
3725
4022
  if (!body.trim()) return {};
3726
4023
  return JSON.parse(body);
3727
4024
  }
4025
+ async function readBodyBuffer(req) {
4026
+ const chunks = [];
4027
+ for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4028
+ return Buffer.concat(chunks);
4029
+ }
4030
+ function multipartBoundary(contentType) {
4031
+ const value = Array.isArray(contentType) ? contentType[0] : contentType;
4032
+ if (!value) return null;
4033
+ const match = value.match(/(?:^|;)\s*boundary=("([^"]+)"|[^;]+)/i);
4034
+ if (!match) return null;
4035
+ return (match[2] ?? match[1]).replace(/^"|"$/g, "");
4036
+ }
4037
+ async function readMultipartFile(req) {
4038
+ const boundary = multipartBoundary(req.headers["content-type"]);
4039
+ if (!boundary) return null;
4040
+ const body = await readBodyBuffer(req);
4041
+ const marker = Buffer.from(`--${boundary}`);
4042
+ let position = body.indexOf(marker);
4043
+ while (position !== -1) {
4044
+ position += marker.length;
4045
+ if (body.subarray(position, position + 2).toString() === "--") return null;
4046
+ if (body.subarray(position, position + 2).toString() === "\r\n") position += 2;
4047
+ const headerEnd = body.indexOf(Buffer.from("\r\n\r\n"), position);
4048
+ if (headerEnd === -1) return null;
4049
+ const headers = body.subarray(position, headerEnd).toString("utf-8");
4050
+ const filenameMatch = headers.match(/Content-Disposition:[^\r\n]*\bname="file"[^\r\n]*\bfilename="([^"]+)"/i);
4051
+ const dataStart = headerEnd + 4;
4052
+ const nextBoundary = body.indexOf(Buffer.from(`\r
4053
+ --${boundary}`), dataStart);
4054
+ if (nextBoundary === -1) return null;
4055
+ if (filenameMatch) return { filename: path2.basename(filenameMatch[1]), data: body.subarray(dataStart, nextBoundary) };
4056
+ position = body.indexOf(marker, nextBoundary);
4057
+ }
4058
+ return null;
4059
+ }
4060
+ function isKelivoBackupFilename(filename) {
4061
+ return filename === "chats.json" || filename.toLowerCase().endsWith(".zip");
4062
+ }
3728
4063
  function getDeviceName() {
3729
4064
  return loadConfig()?.device || hostname() || `${platform3()}-${process.arch}`;
3730
4065
  }
@@ -4956,10 +5291,54 @@ function createApiServer(db, options) {
4956
5291
  }
4957
5292
  }
4958
5293
  if (url.pathname === "/api/detected-tools" && req.method === "GET") {
4959
- const tools = discoverTools();
5294
+ const tools = attachManualImportMetadata(discoverTools(), db);
4960
5295
  json(res, { tools });
4961
5296
  return;
4962
5297
  }
5298
+ if (url.pathname === "/api/import/kelivo" && req.method === "POST") {
5299
+ let tempDir = null;
5300
+ try {
5301
+ const upload = await readMultipartFile(req);
5302
+ if (!upload) {
5303
+ json(res, { error: { code: "INVALID_UPLOAD", message: "Missing uploaded file" } }, 400);
5304
+ return;
5305
+ }
5306
+ if (!isKelivoBackupFilename(upload.filename)) {
5307
+ json(res, { error: { code: "INVALID_FILE", message: "Upload a Kelivo .zip backup or chats.json" } }, 400);
5308
+ return;
5309
+ }
5310
+ tempDir = mkdtempSync(path2.join(tmpdir(), "aiusage-kelivo-"));
5311
+ const tempPath = path2.join(tempDir, upload.filename);
5312
+ writeFileSync4(tempPath, upload.data);
5313
+ const cfg2 = loadConfig();
5314
+ const now = Date.now();
5315
+ const result = await runParseKelivo({
5316
+ filePath: tempPath,
5317
+ device: getDeviceName(),
5318
+ deviceInstanceId: options?.currentDeviceInstanceId ?? "unknown",
5319
+ platform: cfg2?.platform ?? platform3(),
5320
+ now,
5321
+ exchangeRate: resolveExchangeRate(cfg2 ?? {})
5322
+ });
5323
+ if (result.errors.length > 0 && result.records.length === 0) {
5324
+ json(res, { error: { code: "IMPORT_FAILED", message: result.errors[0] }, errors: result.errors }, 400);
5325
+ return;
5326
+ }
5327
+ const existingCount = countExistingRecordIds(db, result.records.map((record) => record.id));
5328
+ for (const record of result.records) insertRecord(db, record);
5329
+ const imported = result.records.length;
5330
+ const added = Math.max(0, new Set(result.records.map((record) => record.id)).size - existingCount);
5331
+ json(res, {
5332
+ imported,
5333
+ added,
5334
+ lastImportedAt: imported > 0 ? now : void 0,
5335
+ errors: result.errors
5336
+ });
5337
+ } finally {
5338
+ if (tempDir) rmSync(tempDir, { recursive: true, force: true });
5339
+ }
5340
+ return;
5341
+ }
4963
5342
  if (url.pathname === "/api/exchange-rate/refresh" && req.method === "POST") {
4964
5343
  const rate = await fetchExchangeRate();
4965
5344
  if (rate == null) {
@@ -4983,114 +5362,12 @@ function createApiServer(db, options) {
4983
5362
 
4984
5363
  // src/commands/parse.ts
4985
5364
  import Database2 from "better-sqlite3";
4986
- import { readFileSync as readFileSync7, statSync as statSync2, existsSync as existsSync8, openSync, readSync, closeSync } from "fs";
5365
+ import { readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync8, openSync, readSync, closeSync } from "fs";
4987
5366
  import { join as join7 } from "path";
4988
5367
  import { hostname as hostname2 } from "os";
4989
5368
 
4990
- // src/db/records.ts
4991
- function insertRecord(db, record) {
4992
- db.prepare(`
4993
- INSERT OR REPLACE INTO records (
4994
- id, ts, ingested_at, synced_at, updated_at, line_offset,
4995
- tool, model, provider, input_tokens, output_tokens,
4996
- cache_read_tokens, cache_write_tokens, thinking_tokens,
4997
- cost, cost_source, session_id, source_file, cwd, device, device_instance_id, platform
4998
- ) VALUES (
4999
- @id, @ts, @ingestedAt, @syncedAt, @updatedAt, @lineOffset,
5000
- @tool, @model, @provider, @inputTokens, @outputTokens,
5001
- @cacheReadTokens, @cacheWriteTokens, @thinkingTokens,
5002
- @cost, @costSource, @sessionId, @sourceFile, @cwd, @device, @deviceInstanceId, @platform
5003
- )
5004
- `).run({
5005
- id: record.id,
5006
- ts: record.ts,
5007
- ingestedAt: record.ingestedAt,
5008
- syncedAt: record.syncedAt ?? null,
5009
- updatedAt: record.updatedAt,
5010
- lineOffset: record.lineOffset,
5011
- tool: record.tool,
5012
- model: record.model,
5013
- provider: record.provider,
5014
- inputTokens: record.inputTokens,
5015
- outputTokens: record.outputTokens,
5016
- cacheReadTokens: record.cacheReadTokens,
5017
- cacheWriteTokens: record.cacheWriteTokens,
5018
- thinkingTokens: record.thinkingTokens,
5019
- cost: record.cost,
5020
- costSource: record.costSource,
5021
- sessionId: record.sessionId,
5022
- sourceFile: record.sourceFile,
5023
- cwd: record.cwd ?? "",
5024
- device: record.device,
5025
- deviceInstanceId: record.deviceInstanceId,
5026
- platform: record.platform ?? ""
5027
- });
5028
- }
5029
- function getUnsyncedRecords(db, target) {
5030
- const rows = target ? db.prepare(`
5031
- SELECT r.* FROM records r
5032
- LEFT JOIN sync_record_state s
5033
- ON s.record_id = r.id AND s.target = ?
5034
- WHERE r.source_file NOT LIKE 'synced/%'
5035
- AND (s.synced_at IS NULL OR r.updated_at > s.synced_at)
5036
- `).all(target) : db.prepare(
5037
- "SELECT * FROM records WHERE synced_at IS NULL OR updated_at > synced_at"
5038
- ).all();
5039
- return rows.map(mapRowToRecord);
5040
- }
5041
- function markRecordsSynced(db, ids, syncedAt, target) {
5042
- if (ids.length === 0) return;
5043
- if (target) {
5044
- const insertStmt = db.prepare(`
5045
- INSERT INTO sync_record_state (record_id, target, synced_at)
5046
- VALUES (?, ?, ?)
5047
- ON CONFLICT(record_id, target) DO UPDATE SET synced_at = excluded.synced_at
5048
- `);
5049
- const legacyStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
5050
- const tx2 = db.transaction((recordIds) => {
5051
- for (const id of recordIds) {
5052
- insertStmt.run(id, target, syncedAt);
5053
- legacyStmt.run(syncedAt, id);
5054
- }
5055
- });
5056
- tx2(ids);
5057
- return;
5058
- }
5059
- const updateStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
5060
- const tx = db.transaction((recordIds) => {
5061
- for (const id of recordIds) updateStmt.run(syncedAt, id);
5062
- });
5063
- tx(ids);
5064
- }
5065
- function mapRowToRecord(row) {
5066
- return {
5067
- id: row.id,
5068
- ts: row.ts,
5069
- ingestedAt: row.ingested_at,
5070
- syncedAt: row.synced_at != null ? row.synced_at : void 0,
5071
- updatedAt: row.updated_at,
5072
- lineOffset: row.line_offset,
5073
- tool: row.tool,
5074
- model: row.model,
5075
- provider: row.provider,
5076
- inputTokens: row.input_tokens,
5077
- outputTokens: row.output_tokens,
5078
- cacheReadTokens: row.cache_read_tokens,
5079
- cacheWriteTokens: row.cache_write_tokens,
5080
- thinkingTokens: row.thinking_tokens,
5081
- cost: row.cost,
5082
- costSource: row.cost_source,
5083
- sessionId: row.session_id,
5084
- sourceFile: row.source_file,
5085
- cwd: row.cwd || void 0,
5086
- device: row.device,
5087
- deviceInstanceId: row.device_instance_id,
5088
- platform: row.platform || void 0
5089
- };
5090
- }
5091
-
5092
5369
  // src/watermark.ts
5093
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "fs";
5370
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
5094
5371
  function defaultFileData() {
5095
5372
  return {
5096
5373
  "claude-code": {},
@@ -5101,6 +5378,7 @@ function defaultFileData() {
5101
5378
  "qoder": {},
5102
5379
  "cursor": {},
5103
5380
  "kilocode": {},
5381
+ "kelivo": {},
5104
5382
  "copilot": {},
5105
5383
  "gemini": {},
5106
5384
  "kimi": {},
@@ -5120,8 +5398,8 @@ function defaultFileData() {
5120
5398
  var WatermarkManager = class {
5121
5399
  data;
5122
5400
  path;
5123
- constructor(path2) {
5124
- this.path = path2;
5401
+ constructor(path3) {
5402
+ this.path = path3;
5125
5403
  this.data = this.load();
5126
5404
  }
5127
5405
  load() {
@@ -5129,7 +5407,7 @@ var WatermarkManager = class {
5129
5407
  return { files: defaultFileData() };
5130
5408
  }
5131
5409
  try {
5132
- const content = readFileSync6(this.path, "utf-8");
5410
+ const content = readFileSync7(this.path, "utf-8");
5133
5411
  const parsed = JSON.parse(content);
5134
5412
  if (parsed && typeof parsed === "object" && !("files" in parsed)) {
5135
5413
  return { files: { ...defaultFileData(), ...parsed } };
@@ -5140,7 +5418,7 @@ var WatermarkManager = class {
5140
5418
  }
5141
5419
  }
5142
5420
  save() {
5143
- writeFileSync4(this.path, JSON.stringify(this.data, null, 2), "utf-8");
5421
+ writeFileSync5(this.path, JSON.stringify(this.data, null, 2), "utf-8");
5144
5422
  }
5145
5423
  getEntry(tool, filePath) {
5146
5424
  return this.data.files[tool]?.[filePath] ?? null;
@@ -6025,12 +6303,12 @@ function parseUiMessagesFile(options) {
6025
6303
  const errors = [];
6026
6304
  let messages;
6027
6305
  try {
6028
- const parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
6306
+ const parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
6029
6307
  messages = Array.isArray(parsed) ? parsed : [];
6030
6308
  } catch (e) {
6031
6309
  return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
6032
6310
  }
6033
- const taskId = filePath.split("/").slice(-2, -1)[0] || "unknown";
6311
+ const taskId = filePath.replace(/\\/g, "/").split("/").slice(-2, -1)[0] || "unknown";
6034
6312
  for (const [index, message] of messages.entries()) {
6035
6313
  if (message?.say !== "api_req_started" || typeof message.text !== "string") continue;
6036
6314
  let payload;
@@ -6082,7 +6360,7 @@ function normalizeKiroModel(value) {
6082
6360
  if (typeof value !== "string" || !value.trim()) return "kiro-agent";
6083
6361
  return value.trim().toLowerCase().replace(/_/g, "-").replace(/^(anthropic\.|openai\.|aws\.)/, "").replace(/:\d+$/, "").replace(/-\d{8}-v\d+(?:-\d+)?$/i, "");
6084
6362
  }
6085
- function parseTimestamp(value, fallback) {
6363
+ function parseTimestamp2(value, fallback) {
6086
6364
  if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
6087
6365
  if (typeof value === "string" && value.trim()) {
6088
6366
  const parsed = new Date(value).getTime();
@@ -6103,7 +6381,7 @@ function parseKiroSessionFile(options) {
6103
6381
  const errors = [];
6104
6382
  let parsed;
6105
6383
  try {
6106
- parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
6384
+ parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
6107
6385
  } catch (e) {
6108
6386
  return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
6109
6387
  }
@@ -6116,7 +6394,7 @@ function parseKiroSessionFile(options) {
6116
6394
  const outputTokens = toNumber2(turn?.output_token_count);
6117
6395
  if (inputTokens + outputTokens === 0) continue;
6118
6396
  const model = normalizeKiroModel(turn?.model_id ?? sessionModel);
6119
- const recordTs = parseTimestamp(
6397
+ const recordTs = parseTimestamp2(
6120
6398
  turn?.request_start_timestamp_ms ?? turn?.start_timestamp ?? turn?.end_timestamp,
6121
6399
  now
6122
6400
  );
@@ -6227,7 +6505,30 @@ async function runParse(db, filterTool, options) {
6227
6505
  onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
6228
6506
  continue;
6229
6507
  }
6230
- const content = readFileSync7(filePath, "utf-8");
6508
+ if (tool === "kelivo" && (filePath.endsWith("chats.json") || filePath.endsWith(".zip"))) {
6509
+ const result = await runParseKelivo({
6510
+ filePath,
6511
+ device,
6512
+ deviceInstanceId,
6513
+ platform: devicePlatform,
6514
+ now: Date.now(),
6515
+ exchangeRate
6516
+ });
6517
+ for (const record of result.records) {
6518
+ insertRecord(db, record);
6519
+ parsedCount++;
6520
+ }
6521
+ errors.push(...result.errors);
6522
+ wm.setEntry(tool, filePath, {
6523
+ offset: stat2.size,
6524
+ size: stat2.size,
6525
+ mtime: stat2.mtimeMs
6526
+ });
6527
+ wm.save();
6528
+ onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
6529
+ continue;
6530
+ }
6531
+ const content = readFileSync8(filePath, "utf-8");
6231
6532
  const lines = content.split("\n");
6232
6533
  let byteOffset = 0;
6233
6534
  let fileCwd;
@@ -6682,7 +6983,7 @@ function backfillCodexModels(db) {
6682
6983
  );
6683
6984
  for (const [sourceFile, fileRows] of byFile) {
6684
6985
  try {
6685
- const content = readFileSync7(sourceFile, "utf-8");
6986
+ const content = readFileSync8(sourceFile, "utf-8");
6686
6987
  const lines = content.split("\n");
6687
6988
  const offsetToModel = /* @__PURE__ */ new Map();
6688
6989
  let currentModel = "";
@@ -6730,7 +7031,7 @@ function backfillMissingToolCalls(db, exchangeRate) {
6730
7031
  }
6731
7032
  for (const [sourceFile, fileRows] of rowsByFile) {
6732
7033
  try {
6733
- const content = readFileSync7(sourceFile, "utf-8");
7034
+ const content = readFileSync8(sourceFile, "utf-8");
6734
7035
  const lines = content.split("\n");
6735
7036
  const aggregator = new Aggregator();
6736
7037
  const recordIdByOffset = /* @__PURE__ */ new Map();
@@ -6909,6 +7210,7 @@ var RECORD_ID_SYNC_TOOLS = /* @__PURE__ */ new Set([
6909
7210
  "qoder",
6910
7211
  "cursor",
6911
7212
  "kilocode",
7213
+ "kelivo",
6912
7214
  "goose",
6913
7215
  "zed",
6914
7216
  "kiro",
@@ -7017,15 +7319,15 @@ var SyncOrchestrator = class {
7017
7319
  });
7018
7320
  if (paths.length === 0) return 0;
7019
7321
  let totalPulled = 0;
7020
- for (const [index, path2] of paths.entries()) {
7322
+ for (const [index, path3] of paths.entries()) {
7021
7323
  this.options.onProgress?.({
7022
7324
  phase: "pulling",
7023
- currentPath: path2,
7325
+ currentPath: path3,
7024
7326
  completedFiles: index,
7025
7327
  totalFiles: paths.length,
7026
7328
  pulledCount: totalPulled
7027
7329
  });
7028
- const content = await this.backend.readFile(path2);
7330
+ const content = await this.backend.readFile(path3);
7029
7331
  if (!content) continue;
7030
7332
  for (const line of content.split("\n").filter(Boolean)) {
7031
7333
  try {
@@ -7043,7 +7345,7 @@ var SyncOrchestrator = class {
7043
7345
  }
7044
7346
  this.options.onProgress?.({
7045
7347
  phase: "pulling",
7046
- currentPath: path2,
7348
+ currentPath: path3,
7047
7349
  completedFiles: index + 1,
7048
7350
  totalFiles: paths.length,
7049
7351
  pulledCount: totalPulled
@@ -7051,13 +7353,13 @@ var SyncOrchestrator = class {
7051
7353
  }
7052
7354
  return totalPulled;
7053
7355
  }
7054
- async uploadFile(path2, localSyncRecords) {
7055
- const existingContent = await this.backend.readFile(path2);
7356
+ async uploadFile(path3, localSyncRecords) {
7357
+ const existingContent = await this.backend.readFile(path3);
7056
7358
  const existing = existingContent ? parseNdjson(existingContent) : /* @__PURE__ */ new Map();
7057
7359
  const changedCount = mergeRecords(existing, localSyncRecords);
7058
7360
  if (changedCount === 0) return 0;
7059
7361
  const content = Array.from(existing.values()).map((r) => JSON.stringify(r)).join("\n") + "\n";
7060
- await this.backend.writeFile(path2, content);
7362
+ await this.backend.writeFile(path3, content);
7061
7363
  return changedCount;
7062
7364
  }
7063
7365
  async upload() {
@@ -7065,9 +7367,9 @@ var SyncOrchestrator = class {
7065
7367
  if (unsynced.length === 0) return 0;
7066
7368
  const byPath = /* @__PURE__ */ new Map();
7067
7369
  for (const record of unsynced) {
7068
- const path2 = getSyncPath(record.ts, this.options.deviceInstanceId);
7069
- if (!byPath.has(path2)) byPath.set(path2, []);
7070
- byPath.get(path2).push(record);
7370
+ const path3 = getSyncPath(record.ts, this.options.deviceInstanceId);
7371
+ if (!byPath.has(path3)) byPath.set(path3, []);
7372
+ byPath.get(path3).push(record);
7071
7373
  }
7072
7374
  let totalUploaded = 0;
7073
7375
  const uploads = Array.from(byPath.entries());
@@ -7077,20 +7379,20 @@ var SyncOrchestrator = class {
7077
7379
  totalFiles: uploads.length,
7078
7380
  uploadedCount: 0
7079
7381
  });
7080
- for (const [index, [path2, localRecords]] of uploads.entries()) {
7382
+ for (const [index, [path3, localRecords]] of uploads.entries()) {
7081
7383
  this.options.onProgress?.({
7082
7384
  phase: "uploading",
7083
- currentPath: path2,
7385
+ currentPath: path3,
7084
7386
  completedFiles: index,
7085
7387
  totalFiles: uploads.length,
7086
7388
  uploadedCount: totalUploaded
7087
7389
  });
7088
7390
  const localSyncRecords = localRecords.map(mapStatsRecordToSyncRecord);
7089
- await this.uploadFile(path2, localSyncRecords);
7391
+ await this.uploadFile(path3, localSyncRecords);
7090
7392
  totalUploaded += localRecords.length;
7091
7393
  this.options.onProgress?.({
7092
7394
  phase: "uploading",
7093
- currentPath: path2,
7395
+ currentPath: path3,
7094
7396
  completedFiles: index + 1,
7095
7397
  totalFiles: uploads.length,
7096
7398
  uploadedCount: totalUploaded
@@ -7115,14 +7417,14 @@ var CloudSyncError = class extends Error {
7115
7417
  code;
7116
7418
  retryAfter;
7117
7419
  };
7118
- function buildHeaders(method, path2, body) {
7420
+ function buildHeaders(method, path3, body) {
7119
7421
  const creds = loadCredentials();
7120
7422
  if (!creds) throw new CloudSyncError("Not logged in. Run `aiusage login` first.", "not_logged_in");
7121
7423
  const bodyHash = sha256(body);
7122
7424
  const timestamp = Date.now().toString();
7123
7425
  const nonce = generateNonce();
7124
7426
  const idempotencyKey = generateIdempotencyKey();
7125
- const canonical = buildCanonicalString(method, path2, bodyHash, timestamp, nonce, creds.device_id, idempotencyKey);
7427
+ const canonical = buildCanonicalString(method, path3, bodyHash, timestamp, nonce, creds.device_id, idempotencyKey);
7126
7428
  const signature = computeHmac(creds.device_secret, canonical);
7127
7429
  return {
7128
7430
  "Content-Type": "application/json",
@@ -7181,9 +7483,9 @@ async function cloudPull(cursor, limit = 1e3) {
7181
7483
  if (cursor) params.set("cursor", cursor);
7182
7484
  params.set("limit", String(limit));
7183
7485
  const query = params.toString();
7184
- const path2 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
7486
+ const path3 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
7185
7487
  const headers = buildHeaders("GET", SYNC_PULL_PATH, "");
7186
- const response = await fetch(`${serverUrl}${path2}`, { method: "GET", headers });
7488
+ const response = await fetch(`${serverUrl}${path3}`, { method: "GET", headers });
7187
7489
  const data = await readJsonOrNull2(response);
7188
7490
  if (!response.ok) {
7189
7491
  const errObj = data?.error;
@@ -7200,12 +7502,32 @@ async function cloudPull(cursor, limit = 1e3) {
7200
7502
  syncGeneration: data.sync_generation || 1
7201
7503
  };
7202
7504
  }
7505
+ async function cloudClear() {
7506
+ const serverUrl = getSiteUrl();
7507
+ const creds = loadCredentials();
7508
+ if (!creds) throw new CloudSyncError("Not logged in.", "not_logged_in");
7509
+ const SYNC_CLEAR_PATH = "/api/cli/sync/clear";
7510
+ const body = "{}";
7511
+ const headers = buildHeaders("POST", SYNC_CLEAR_PATH, body);
7512
+ const response = await fetch(`${serverUrl}${SYNC_CLEAR_PATH}`, { method: "POST", headers, body });
7513
+ const data = await readJsonOrNull2(response);
7514
+ if (!response.ok) {
7515
+ const errObj = data?.error;
7516
+ const errMsg = typeof errObj === "object" && errObj !== null ? errObj.message || `Clear failed (HTTP ${response.status})` : errObj || `Clear failed (HTTP ${response.status})`;
7517
+ const errCode = typeof errObj === "object" && errObj !== null ? errObj.code || "server_error" : data?.error_code || "server_error";
7518
+ throw new CloudSyncError(errMsg, errCode);
7519
+ }
7520
+ if (!data) throw new CloudSyncError("Invalid response from server", "invalid_response");
7521
+ return {
7522
+ syncGeneration: data.sync_generation || 1
7523
+ };
7524
+ }
7203
7525
  function getClientVersion() {
7204
7526
  try {
7205
- const { readFileSync: readFileSync11 } = __require("fs");
7527
+ const { readFileSync: readFileSync12 } = __require("fs");
7206
7528
  const { join: join16 } = __require("path");
7207
7529
  const pkgPath = join16(__dirname, "../../package.json");
7208
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7530
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7209
7531
  return pkg.version || "0.0.0";
7210
7532
  } catch {
7211
7533
  return "0.0.0";
@@ -7295,7 +7617,7 @@ var CloudSyncOrchestrator = class {
7295
7617
  // src/sync/git.ts
7296
7618
  import { execFile } from "child_process";
7297
7619
  import { promisify } from "util";
7298
- import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
7620
+ import { readFile, writeFile, mkdir, readdir, stat, unlink, rm } from "fs/promises";
7299
7621
  import { join as join8 } from "path";
7300
7622
  var exec = promisify(execFile);
7301
7623
  var GitSyncBackend = class _GitSyncBackend {
@@ -7355,16 +7677,16 @@ var GitSyncBackend = class _GitSyncBackend {
7355
7677
  }
7356
7678
  }
7357
7679
  }
7358
- async readFile(path2) {
7680
+ async readFile(path3) {
7359
7681
  try {
7360
- const fullPath = join8(this.dataDir, path2);
7682
+ const fullPath = join8(this.dataDir, path3);
7361
7683
  return await readFile(fullPath, "utf-8");
7362
7684
  } catch {
7363
7685
  return null;
7364
7686
  }
7365
7687
  }
7366
- async writeFile(path2, content) {
7367
- const fullPath = join8(this.dataDir, path2);
7688
+ async writeFile(path3, content) {
7689
+ const fullPath = join8(this.dataDir, path3);
7368
7690
  await mkdir(join8(fullPath, ".."), { recursive: true });
7369
7691
  await writeFile(fullPath, content, "utf-8");
7370
7692
  }
@@ -7375,6 +7697,22 @@ var GitSyncBackend = class _GitSyncBackend {
7375
7697
  return [];
7376
7698
  }
7377
7699
  }
7700
+ async deleteFile(path3) {
7701
+ const fullPath = join8(this.dataDir, path3);
7702
+ try {
7703
+ await unlink(fullPath);
7704
+ } catch {
7705
+ }
7706
+ }
7707
+ async deleteAllData() {
7708
+ try {
7709
+ const files = await this.listFiles();
7710
+ await rm(this.dataDir, { recursive: true, force: true });
7711
+ return files.length;
7712
+ } catch {
7713
+ return 0;
7714
+ }
7715
+ }
7378
7716
  static MAX_PUSH_RETRIES = 3;
7379
7717
  /** Commit and push all changes. Returns true if anything was pushed. */
7380
7718
  async flush() {
@@ -7409,7 +7747,7 @@ var GitSyncBackend = class _GitSyncBackend {
7409
7747
  };
7410
7748
 
7411
7749
  // src/sync/s3.ts
7412
- import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
7750
+ import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, DeleteObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
7413
7751
  var S3SyncBackend = class {
7414
7752
  client;
7415
7753
  bucket;
@@ -7426,11 +7764,11 @@ var S3SyncBackend = class {
7426
7764
  }
7427
7765
  });
7428
7766
  }
7429
- getObjectKey(path2) {
7430
- return `${this.prefix}${path2}`;
7767
+ getObjectKey(path3) {
7768
+ return `${this.prefix}${path3}`;
7431
7769
  }
7432
- async readFile(path2) {
7433
- const key = this.getObjectKey(path2);
7770
+ async readFile(path3) {
7771
+ const key = this.getObjectKey(path3);
7434
7772
  try {
7435
7773
  const command = new GetObjectCommand({
7436
7774
  Bucket: this.bucket,
@@ -7446,8 +7784,8 @@ var S3SyncBackend = class {
7446
7784
  throw error;
7447
7785
  }
7448
7786
  }
7449
- async writeFile(path2, content) {
7450
- const key = this.getObjectKey(path2);
7787
+ async writeFile(path3, content) {
7788
+ const key = this.getObjectKey(path3);
7451
7789
  const command = new PutObjectCommand({
7452
7790
  Bucket: this.bucket,
7453
7791
  Key: key,
@@ -7479,8 +7817,8 @@ var S3SyncBackend = class {
7479
7817
  } while (continuationToken);
7480
7818
  return files.sort();
7481
7819
  }
7482
- async fileExists(path2) {
7483
- const key = this.getObjectKey(path2);
7820
+ async fileExists(path3) {
7821
+ const key = this.getObjectKey(path3);
7484
7822
  try {
7485
7823
  const command = new HeadObjectCommand({
7486
7824
  Bucket: this.bucket,
@@ -7495,6 +7833,31 @@ var S3SyncBackend = class {
7495
7833
  throw error;
7496
7834
  }
7497
7835
  }
7836
+ async deleteFile(path3) {
7837
+ const key = this.getObjectKey(path3);
7838
+ const command = new DeleteObjectCommand({
7839
+ Bucket: this.bucket,
7840
+ Key: key
7841
+ });
7842
+ await this.client.send(command);
7843
+ }
7844
+ async deleteAllData() {
7845
+ const files = await this.listFiles();
7846
+ if (files.length === 0) return 0;
7847
+ const BATCH_SIZE2 = 1e3;
7848
+ for (let i = 0; i < files.length; i += BATCH_SIZE2) {
7849
+ const batch = files.slice(i, i + BATCH_SIZE2);
7850
+ const command = new DeleteObjectsCommand({
7851
+ Bucket: this.bucket,
7852
+ Delete: {
7853
+ Objects: batch.map((f) => ({ Key: this.getObjectKey(f) })),
7854
+ Quiet: true
7855
+ }
7856
+ });
7857
+ await this.client.send(command);
7858
+ }
7859
+ return files.length;
7860
+ }
7498
7861
  };
7499
7862
 
7500
7863
  // src/commands/sync.ts
@@ -7603,6 +7966,8 @@ async function runSync(db, options) {
7603
7966
  }
7604
7967
 
7605
7968
  // src/commands/clean.ts
7969
+ import { unlinkSync as unlinkSync2, existsSync as existsSync9 } from "fs";
7970
+ import { join as join10 } from "path";
7606
7971
  function cleanOldData(db, days) {
7607
7972
  const cutoff = Date.now() - days * 864e5;
7608
7973
  const recordsResult = db.prepare("DELETE FROM records WHERE ts < ?").run(cutoff);
@@ -7617,6 +7982,172 @@ function cleanOldData(db, days) {
7617
7982
  deletedOrphanToolCalls
7618
7983
  };
7619
7984
  }
7985
+ function cleanAll(db) {
7986
+ const recordsResult = db.prepare("DELETE FROM records").run();
7987
+ const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
7988
+ const syncedResult = db.prepare("DELETE FROM synced_records").run();
7989
+ const syncStateResult = db.prepare("DELETE FROM sync_record_state").run();
7990
+ const tombstonesResult = db.prepare("DELETE FROM sync_tombstones").run();
7991
+ const watermarkPath = join10(AIUSAGE_DIR, "watermark.json");
7992
+ let watermarkRemoved = false;
7993
+ if (existsSync9(watermarkPath)) {
7994
+ unlinkSync2(watermarkPath);
7995
+ watermarkRemoved = true;
7996
+ }
7997
+ return {
7998
+ deletedRecords: recordsResult.changes,
7999
+ deletedToolCalls: toolCallsResult.changes,
8000
+ deletedSyncedRecords: syncedResult.changes,
8001
+ deletedSyncRecordState: syncStateResult.changes,
8002
+ deletedTombstones: tombstonesResult.changes,
8003
+ watermarkRemoved
8004
+ };
8005
+ }
8006
+ function getRemoteBackends() {
8007
+ const config = loadConfig();
8008
+ if (!config?.sync) return [];
8009
+ const backends = [];
8010
+ if (config.sync.backend === "cloud") {
8011
+ backends.push({ type: "cloud", label: "AIUsage Cloud (cloud)" });
8012
+ } else if (config.sync.backend === "github" && config.sync.repo) {
8013
+ backends.push({ type: "github", label: `GitHub (${config.sync.repo})` });
8014
+ } else if (config.sync.backend === "s3" && config.sync.bucket) {
8015
+ backends.push({ type: "s3", label: `S3 (${config.sync.bucket})` });
8016
+ }
8017
+ return backends;
8018
+ }
8019
+ function createBackend2(config) {
8020
+ if (!config?.sync) return null;
8021
+ if (config.sync.backend === "github") {
8022
+ if (!config.sync.repo) return null;
8023
+ const token = loadCredential(`github/${config.sync.repo}/token`);
8024
+ if (!token) return null;
8025
+ return new GitSyncBackend({
8026
+ repo: config.sync.repo,
8027
+ token,
8028
+ cacheDir: join10(AIUSAGE_DIR, "sync-repo")
8029
+ });
8030
+ }
8031
+ if (config.sync.backend === "s3") {
8032
+ if (!config.sync.bucket) return null;
8033
+ const accessKeyId = loadCredential(`s3/${config.sync.bucket}/accessKeyId`);
8034
+ const secretAccessKey = loadCredential(`s3/${config.sync.bucket}/secretAccessKey`);
8035
+ if (!accessKeyId || !secretAccessKey) return null;
8036
+ return new S3SyncBackend({
8037
+ bucket: config.sync.bucket,
8038
+ prefix: config.sync.prefix ?? "aiusage/",
8039
+ accessKeyId,
8040
+ secretAccessKey,
8041
+ endpoint: config.sync.endpoint,
8042
+ region: config.sync.region
8043
+ });
8044
+ }
8045
+ return null;
8046
+ }
8047
+ async function propagateClean(options) {
8048
+ const config = loadConfig();
8049
+ if (!config?.sync) return { backends: [] };
8050
+ const results = [];
8051
+ if (config.sync.backend === "cloud") {
8052
+ if (!options.target || options.target === "cloud") {
8053
+ try {
8054
+ const result = await cloudClear();
8055
+ results.push({
8056
+ backend: { type: "cloud", label: "AIUsage Cloud" },
8057
+ status: "ok",
8058
+ detail: `generation updated to ${result.syncGeneration}`
8059
+ });
8060
+ } catch (err) {
8061
+ results.push({
8062
+ backend: { type: "cloud", label: "AIUsage Cloud" },
8063
+ status: "skipped",
8064
+ detail: err instanceof Error ? err.message : "unknown error"
8065
+ });
8066
+ }
8067
+ }
8068
+ return { backends: results };
8069
+ }
8070
+ if (options.target && options.target !== config.sync.backend) {
8071
+ return { backends: results };
8072
+ }
8073
+ const backend = createBackend2(config);
8074
+ if (!backend) {
8075
+ return { backends: [{
8076
+ backend: { type: config.sync.backend, label: config.sync.backend },
8077
+ status: "skipped",
8078
+ detail: "Could not create backend (missing credentials)"
8079
+ }] };
8080
+ }
8081
+ try {
8082
+ await backend.prepare?.();
8083
+ if (options.all) {
8084
+ const files = await backend.listFiles();
8085
+ const fileCount = files.length;
8086
+ if (fileCount > 0) {
8087
+ await backend.deleteAllData();
8088
+ }
8089
+ await backend.flush?.();
8090
+ results.push({
8091
+ backend: {
8092
+ type: config.sync.backend,
8093
+ label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
8094
+ },
8095
+ status: "ok",
8096
+ detail: `cleared ${fileCount} files`
8097
+ });
8098
+ } else if (options.beforeDays) {
8099
+ const cutoff = Date.now() - options.beforeDays * 864e5;
8100
+ const files = await backend.listFiles();
8101
+ let removedRecords = 0;
8102
+ let modifiedFiles = 0;
8103
+ for (const file of files) {
8104
+ const content = await backend.readFile(file);
8105
+ if (!content) continue;
8106
+ const lines = content.split("\n").filter(Boolean);
8107
+ const kept = [];
8108
+ for (const line of lines) {
8109
+ try {
8110
+ const record = JSON.parse(line);
8111
+ const ts3 = typeof record.ts === "string" ? new Date(record.ts).getTime() : record.ts;
8112
+ if (ts3 >= cutoff) {
8113
+ kept.push(line);
8114
+ } else {
8115
+ removedRecords++;
8116
+ }
8117
+ } catch {
8118
+ kept.push(line);
8119
+ }
8120
+ }
8121
+ if (kept.length === 0) {
8122
+ await backend.deleteFile?.(file);
8123
+ modifiedFiles++;
8124
+ } else if (kept.length < lines.length) {
8125
+ await backend.writeFile(file, kept.join("\n") + "\n");
8126
+ modifiedFiles++;
8127
+ }
8128
+ }
8129
+ await backend.flush?.();
8130
+ results.push({
8131
+ backend: {
8132
+ type: config.sync.backend,
8133
+ label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
8134
+ },
8135
+ status: "ok",
8136
+ detail: `removed ${removedRecords} records from ${modifiedFiles} files`
8137
+ });
8138
+ }
8139
+ } catch (err) {
8140
+ results.push({
8141
+ backend: {
8142
+ type: config.sync.backend,
8143
+ label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
8144
+ },
8145
+ status: "skipped",
8146
+ detail: err instanceof Error ? err.message : "unknown error"
8147
+ });
8148
+ }
8149
+ return { backends: results };
8150
+ }
7620
8151
 
7621
8152
  // src/sync/runtime.ts
7622
8153
  var SyncRuntimeController = class {
@@ -7819,7 +8350,7 @@ var RuntimeSettingsController = class {
7819
8350
 
7820
8351
  // src/commands/serve.ts
7821
8352
  var MAX_PORT_ATTEMPTS = 10;
7822
- var PORT_FILE = join10(AIUSAGE_DIR, ".serve-port");
8353
+ var PORT_FILE = join11(AIUSAGE_DIR, ".serve-port");
7823
8354
  var MIME_TYPES = {
7824
8355
  ".html": "text/html",
7825
8356
  ".js": "application/javascript",
@@ -7881,29 +8412,33 @@ function serve(options) {
7881
8412
  onConfigUpdated: () => runtimeSettings.reload()
7882
8413
  });
7883
8414
  const webBuildDir = (() => {
7884
- const prodDir = join10(dirname3(fileURLToPath(import.meta.url)), "web");
7885
- if (existsSync9(prodDir)) return prodDir;
7886
- return join10(dirname3(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
8415
+ const prodDir = join11(dirname3(fileURLToPath(import.meta.url)), "web");
8416
+ if (existsSync10(prodDir)) return prodDir;
8417
+ return join11(dirname3(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
7887
8418
  })();
8419
+ if (!existsSync10(webBuildDir)) {
8420
+ console.error("Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage");
8421
+ process.exit(1);
8422
+ }
7888
8423
  const server = http2.createServer(async (req, res) => {
7889
8424
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
7890
8425
  if (url.pathname.startsWith("/api/")) {
7891
8426
  apiServer.emit("request", req, res);
7892
8427
  return;
7893
8428
  }
7894
- if (existsSync9(webBuildDir)) {
7895
- let filePath = join10(webBuildDir, url.pathname);
8429
+ if (existsSync10(webBuildDir)) {
8430
+ let filePath = join11(webBuildDir, url.pathname);
7896
8431
  try {
7897
8432
  if (statSync3(filePath).isDirectory()) {
7898
- filePath = join10(filePath, "index.html");
8433
+ filePath = join11(filePath, "index.html");
7899
8434
  }
7900
8435
  } catch {
7901
8436
  }
7902
- if (!existsSync9(filePath)) {
7903
- filePath = join10(webBuildDir, "index.html");
8437
+ if (!existsSync10(filePath)) {
8438
+ filePath = join11(webBuildDir, "index.html");
7904
8439
  }
7905
8440
  try {
7906
- const content = readFileSync8(filePath);
8441
+ const content = readFileSync9(filePath);
7907
8442
  const ext = extname2(filePath);
7908
8443
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7909
8444
  res.writeHead(200, { "Content-Type": contentType });
@@ -7913,7 +8448,7 @@ function serve(options) {
7913
8448
  }
7914
8449
  }
7915
8450
  res.writeHead(404, { "Content-Type": "application/json" });
7916
- res.end(JSON.stringify({ error: { code: "NOT_FOUND", message: "Web dashboard not found. Reinstall the package: npm install -g aiusage" } }));
8451
+ res.end(JSON.stringify({ error: { code: "NOT_FOUND", message: "Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage" } }));
7917
8452
  });
7918
8453
  let currentPort = options.port;
7919
8454
  let started = false;
@@ -7923,7 +8458,7 @@ function serve(options) {
7923
8458
  };
7924
8459
  server.on("listening", () => {
7925
8460
  started = true;
7926
- writeFileSync5(PORT_FILE, String(currentPort), "utf-8");
8461
+ writeFileSync6(PORT_FILE, String(currentPort), "utf-8");
7927
8462
  console.log(`aiusage serve listening on http://localhost:${currentPort}`);
7928
8463
  });
7929
8464
  server.on("error", (error) => {
@@ -7941,7 +8476,7 @@ function serve(options) {
7941
8476
  listenOnPort(options.port);
7942
8477
  const cleanup = () => {
7943
8478
  try {
7944
- unlinkSync2(PORT_FILE);
8479
+ unlinkSync3(PORT_FILE);
7945
8480
  } catch {
7946
8481
  }
7947
8482
  };
@@ -8085,9 +8620,9 @@ Consent recorded for schema v1.`
8085
8620
  }
8086
8621
 
8087
8622
  // src/commands/status.ts
8088
- import { statSync as statSync4, existsSync as existsSync10 } from "fs";
8623
+ import { statSync as statSync4, existsSync as existsSync11 } from "fs";
8089
8624
  import { hostname as hostname3 } from "os";
8090
- import { join as join11 } from "path";
8625
+ import { join as join12 } from "path";
8091
8626
  function formatFileSize(bytes) {
8092
8627
  if (bytes < 1024) return `${bytes} B`;
8093
8628
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -8105,9 +8640,9 @@ function generateStatus(db) {
8105
8640
  const schemaVersion = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get()?.version ?? 0;
8106
8641
  const tableCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get()?.count ?? 0;
8107
8642
  const viewCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'view'").get()?.count ?? 0;
8108
- const sizePath = join11(AIUSAGE_DIR, "cache.db");
8643
+ const sizePath = join12(AIUSAGE_DIR, "cache.db");
8109
8644
  let databaseSize = "0 B";
8110
- if (existsSync10(sizePath)) {
8645
+ if (existsSync11(sizePath)) {
8111
8646
  try {
8112
8647
  const stat2 = statSync4(sizePath);
8113
8648
  databaseSize = formatFileSize(stat2.size);
@@ -8184,27 +8719,6 @@ function exportData(db, format) {
8184
8719
  return lines.join("\n");
8185
8720
  }
8186
8721
 
8187
- // src/commands/reset.ts
8188
- import { unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
8189
- import { join as join12 } from "path";
8190
- function runReset(db) {
8191
- const recordsResult = db.prepare("DELETE FROM records").run();
8192
- const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
8193
- const syncedResult = db.prepare("DELETE FROM synced_records").run();
8194
- const watermarkPath = join12(AIUSAGE_DIR, "watermark.json");
8195
- let watermarkRemoved = false;
8196
- if (existsSync11(watermarkPath)) {
8197
- unlinkSync3(watermarkPath);
8198
- watermarkRemoved = true;
8199
- }
8200
- return {
8201
- deletedRecords: recordsResult.changes,
8202
- deletedToolCalls: toolCallsResult.changes,
8203
- deletedSyncedRecords: syncedResult.changes,
8204
- watermarkRemoved
8205
- };
8206
- }
8207
-
8208
8722
  // src/commands/recalc.ts
8209
8723
  var BATCH_SIZE = 1e3;
8210
8724
  function recalcPricing(db) {
@@ -8317,7 +8831,7 @@ var SyncProgressReporter = class {
8317
8831
  };
8318
8832
 
8319
8833
  // src/commands/widget.ts
8320
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
8834
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
8321
8835
  import { spawn, execSync as execSync2 } from "child_process";
8322
8836
  import { join as join13 } from "path";
8323
8837
  var WIDGET_PID_PATH = join13(AIUSAGE_DIR, "widget.pid");
@@ -8345,7 +8859,7 @@ function isWidgetRunning() {
8345
8859
  if (!existsSync12(WIDGET_PID_PATH)) return false;
8346
8860
  let pid;
8347
8861
  try {
8348
- pid = parseInt(readFileSync9(WIDGET_PID_PATH, "utf-8").trim(), 10);
8862
+ pid = parseInt(readFileSync10(WIDGET_PID_PATH, "utf-8").trim(), 10);
8349
8863
  } catch {
8350
8864
  return false;
8351
8865
  }
@@ -8389,10 +8903,10 @@ function getDeviceName2() {
8389
8903
  }
8390
8904
  function getCliVersion2() {
8391
8905
  try {
8392
- const { readFileSync: readFileSync11 } = __require("fs");
8906
+ const { readFileSync: readFileSync12 } = __require("fs");
8393
8907
  const { join: join16 } = __require("path");
8394
8908
  const pkgPath = join16(__dirname, "../../package.json");
8395
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
8909
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
8396
8910
  return pkg.version || "0.0.0";
8397
8911
  } catch {
8398
8912
  return "0.0.0";
@@ -8636,7 +9150,7 @@ async function runLeaderboardView(options = {}) {
8636
9150
  // src/commands/menu.ts
8637
9151
  import { createInterface } from "readline";
8638
9152
  import { spawn as spawn2, spawnSync, execSync as execSync4 } from "child_process";
8639
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
9153
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
8640
9154
  import { join as join14 } from "path";
8641
9155
  var DEFAULT_PORT = 3847;
8642
9156
  var PORT_FILE2 = join14(AIUSAGE_DIR, ".serve-port");
@@ -8645,7 +9159,7 @@ function clearScreen() {
8645
9159
  }
8646
9160
  function getPort() {
8647
9161
  if (existsSync13(PORT_FILE2)) {
8648
- const raw = readFileSync10(PORT_FILE2, "utf-8").trim();
9162
+ const raw = readFileSync11(PORT_FILE2, "utf-8").trim();
8649
9163
  const n = parseInt(raw, 10);
8650
9164
  if (!isNaN(n) && n > 0) return n;
8651
9165
  }
@@ -8823,11 +9337,10 @@ async function dataSubmenu(rl) {
8823
9337
  [2] Summary
8824
9338
  [3] Export
8825
9339
  [4] Clean
8826
- [5] Reset
8827
- [6] Recalc
9340
+ [5] Recalc
8828
9341
  [0] Back
8829
9342
  `);
8830
- const choice = await prompt(rl, " Select [0-6]: ");
9343
+ const choice = await prompt(rl, " Select [0-5]: ");
8831
9344
  switch (choice) {
8832
9345
  case "1": {
8833
9346
  const progress = await prompt(rl, " Show progress? [Y/N]: ");
@@ -8863,28 +9376,28 @@ async function dataSubmenu(rl) {
8863
9376
  break;
8864
9377
  }
8865
9378
  case "4": {
8866
- const dur = await prompt(rl, " Duration to keep (default 180d): ");
8867
- const before = dur || "180d";
9379
+ const mode = await prompt(rl, " 1=Old data (180d) 2=Old data (custom) 3=All (=reset): ");
8868
9380
  console.log("");
8869
- runCommand(["clean", "--before", before, "--yes"]);
8870
- await prompt(rl, "\n Press Enter to continue...");
8871
- break;
8872
- }
8873
- case "5": {
8874
- const confirm = await prompt(
8875
- rl,
8876
- ' WARNING: This will delete ALL parsed data. Type "yes" to confirm: '
8877
- );
8878
- if (confirm.toLowerCase() === "yes") {
8879
- console.log("");
8880
- runCommand(["reset", "--yes"]);
9381
+ if (mode === "3") {
9382
+ const confirm = await prompt(
9383
+ rl,
9384
+ ' WARNING: This will delete ALL parsed data. Type "confirm" to proceed: '
9385
+ );
9386
+ if (confirm.toLowerCase() === "confirm") {
9387
+ runCommand(["clean", "--all", "--yes"]);
9388
+ } else {
9389
+ console.log(" Cancelled.");
9390
+ }
9391
+ } else if (mode === "2") {
9392
+ const dur = await prompt(rl, " Duration to keep (e.g. 30d, 90d): ");
9393
+ runCommand(["clean", "--before", dur || "180d", "--yes"]);
8881
9394
  } else {
8882
- console.log("\n Cancelled.");
9395
+ runCommand(["clean", "--before", "180d", "--yes"]);
8883
9396
  }
8884
9397
  await prompt(rl, "\n Press Enter to continue...");
8885
9398
  break;
8886
9399
  }
8887
- case "6": {
9400
+ case "5": {
8888
9401
  console.log("");
8889
9402
  runCommand(["recalc"]);
8890
9403
  await prompt(rl, "\n Press Enter to continue...");
@@ -9099,7 +9612,7 @@ async function runMenu() {
9099
9612
  ${dashboardStatusLine()}
9100
9613
 
9101
9614
  [1] Dashboard (serve/stop/restart/open)
9102
- [2] Data (parse/summary/export/clean/reset/recalc)
9615
+ [2] Data (parse/summary/export/clean/recalc)
9103
9616
  [3] Sync (init/sync)
9104
9617
  [4] Leaderboard (view/login/upload/status/logout)
9105
9618
  [5] System (status/widget/pm2/update)
@@ -9138,7 +9651,7 @@ ${dashboardStatusLine()}
9138
9651
  import { join as join15 } from "path";
9139
9652
  var DB_PATH2 = join15(AIUSAGE_DIR, "cache.db");
9140
9653
  var program = new Command();
9141
- program.name("aiusage").version(true ? "1.5.0" : "dev").description("CLI tool for AI usage statistics");
9654
+ program.name("aiusage").version(true ? "1.5.2" : "dev").description("CLI tool for AI usage statistics");
9142
9655
  program.action(() => {
9143
9656
  const unknownCommand = program.args.find((arg) => typeof arg === "string");
9144
9657
  if (unknownCommand) {
@@ -9220,7 +9733,7 @@ program.command("export").description("Export data").requiredOption("--format <f
9220
9733
  const db = createDatabase(DB_PATH2);
9221
9734
  const data = exportData(db, format);
9222
9735
  if (options.output) {
9223
- writeFileSync6(options.output, data, "utf-8");
9736
+ writeFileSync7(options.output, data, "utf-8");
9224
9737
  console.log(`Exported to ${options.output}`);
9225
9738
  } else {
9226
9739
  console.log(data);
@@ -9247,32 +9760,109 @@ program.command("parse").description("Parse AI tool session logs").option("--too
9247
9760
  db.close();
9248
9761
  }
9249
9762
  });
9250
- 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) => {
9251
- const daysMatch = options.before.match(/^(\d+)d$/);
9252
- if (!daysMatch) {
9253
- console.error("Invalid duration format. Use e.g. 30d, 180d.");
9763
+ 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) => {
9764
+ const isAll = !!options.all;
9765
+ const localOnly = !!options.localOnly;
9766
+ let days = 0;
9767
+ if (isAll && options.before !== "180d") {
9768
+ console.error("Cannot use --all and --before together. Use one or the other.");
9254
9769
  process.exit(1);
9255
9770
  }
9256
- const days = parseInt(daysMatch[1], 10);
9257
- const db = createDatabase(DB_PATH2);
9258
- const result = cleanOldData(db, days);
9259
- console.log(`Deleted ${result.deletedCount} records, ${result.deletedSyncedCount} synced records, ${result.deletedOrphanToolCalls} orphan tool calls.`);
9260
- db.close();
9261
- });
9262
- program.command("reset").description("Delete all parsed data and watermark, then re-parse from scratch").option("--yes", "Skip confirmation").action((options) => {
9771
+ if (!isAll) {
9772
+ const daysMatch = options.before.match(/^(\d+)d$/);
9773
+ if (!daysMatch) {
9774
+ console.error("Invalid duration format. Use e.g. 30d, 180d.");
9775
+ process.exit(1);
9776
+ }
9777
+ days = parseInt(daysMatch[1], 10);
9778
+ }
9779
+ const remoteBackends = localOnly ? [] : getRemoteBackends();
9780
+ if (isAll) {
9781
+ console.log("\x1B[33m\u26A0 WARNING: This will delete ALL parsed data, tool calls, synced records,\x1B[0m");
9782
+ console.log("\x1B[33m and the parse watermark. Next parse will re-import from scratch.\x1B[0m");
9783
+ } else {
9784
+ console.log(`This will delete local records older than ${days} days.`);
9785
+ }
9786
+ if (localOnly) {
9787
+ console.log("\x1B[36m (--local-only: remote data will NOT be affected)\x1B[0m");
9788
+ }
9789
+ if (remoteBackends.length > 0) {
9790
+ console.log("\nCloud sync is configured. The following remote backends will also be affected:");
9791
+ for (const rb of remoteBackends) {
9792
+ console.log(` - ${rb.label}`);
9793
+ }
9794
+ }
9795
+ const ask = async (question) => {
9796
+ if (!process.stdin.isTTY) {
9797
+ console.error("Non-interactive mode detected. Use --local-only to skip remote propagation, or run interactively.");
9798
+ process.exit(1);
9799
+ }
9800
+ const { createInterface: createInterface2 } = await import("readline");
9801
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
9802
+ return new Promise((resolve2) => {
9803
+ rl.question(question, (answer) => {
9804
+ rl.close();
9805
+ resolve2(answer.trim());
9806
+ });
9807
+ });
9808
+ };
9263
9809
  if (!options.yes) {
9264
- console.error("This will delete ALL parsed records, tool calls, synced data, and the parse watermark.");
9265
- console.error("Source log files (~/.claude, ~/.codex, etc.) will NOT be affected.");
9266
- console.error("Use --yes to confirm.");
9267
- process.exit(1);
9810
+ if (remoteBackends.length > 0) {
9811
+ const answer = await ask(`
9812
+ ? Confirm deletion on local + ${remoteBackends.length} remote backend(s)? [y/N] `);
9813
+ if (answer.toLowerCase() !== "y") {
9814
+ console.log("Cancelled.");
9815
+ process.exit(0);
9816
+ }
9817
+ } else {
9818
+ const answer = await ask("\n? Confirm? [y/N] ");
9819
+ if (answer.toLowerCase() !== "y") {
9820
+ console.log("Cancelled.");
9821
+ process.exit(0);
9822
+ }
9823
+ }
9824
+ } else if (remoteBackends.length > 0) {
9825
+ const answer = await ask('\n? Type "confirm" to proceed with remote deletion: ');
9826
+ if (answer !== "confirm") {
9827
+ console.log("Cancelled.");
9828
+ process.exit(0);
9829
+ }
9268
9830
  }
9269
9831
  const db = createDatabase(DB_PATH2);
9270
- const result = runReset(db);
9271
- console.log(`Deleted ${result.deletedRecords} records, ${result.deletedToolCalls} tool calls, ${result.deletedSyncedRecords} synced records.`);
9272
- if (result.watermarkRemoved) {
9273
- console.log("Watermark removed \u2014 next parse will re-import all data from scratch.");
9832
+ if (isAll) {
9833
+ console.log("\nDeleting all local data...");
9834
+ const result = cleanAll(db);
9835
+ console.log(` Local: deleted ${result.deletedRecords} records, ${result.deletedToolCalls} tool calls, ${result.deletedSyncedRecords} synced records`);
9836
+ if (result.watermarkRemoved) {
9837
+ console.log(" Watermark removed");
9838
+ }
9839
+ } else {
9840
+ console.log(`
9841
+ Cleaning local records older than ${days} days...`);
9842
+ const result = cleanOldData(db, days);
9843
+ console.log(` Local: deleted ${result.deletedCount} records, ${result.deletedSyncedCount} synced records, ${result.deletedOrphanToolCalls} orphan tool calls`);
9274
9844
  }
9275
9845
  db.close();
9846
+ if (remoteBackends.length > 0) {
9847
+ console.log("\nSyncing deletion to remote backends...");
9848
+ try {
9849
+ const propagation = await propagateClean({
9850
+ all: isAll,
9851
+ beforeDays: isAll ? void 0 : days,
9852
+ target: options.target
9853
+ });
9854
+ for (const r of propagation.backends) {
9855
+ if (r.status === "ok") {
9856
+ console.log(` ${r.backend.label}: ${r.detail}`);
9857
+ } else {
9858
+ console.log(` ${r.backend.label}: skipped \u2014 ${r.detail}`);
9859
+ }
9860
+ }
9861
+ } catch (err) {
9862
+ console.error(` Propagation failed: ${err instanceof Error ? err.message : err}`);
9863
+ }
9864
+ }
9865
+ console.log("\nDone.");
9276
9866
  });
9277
9867
  program.command("recalc").description("Recalculate costs").option("--pricing", "Recalculate using latest pricing").action(() => {
9278
9868
  const db = createDatabase(DB_PATH2);
@@ -9349,7 +9939,7 @@ function hasWidget() {
9349
9939
  }
9350
9940
  function buildPm2Config(includeWidget) {
9351
9941
  const wrapperPath = join15(AIUSAGE_DIR, "pm2-server.cjs");
9352
- writeFileSync6(wrapperPath, [
9942
+ writeFileSync7(wrapperPath, [
9353
9943
  `// Auto-generated by aiusage pm2-start \u2014 do not edit`,
9354
9944
  `const { execSync, spawn } = require('child_process')`,
9355
9945
  `const { dirname, join } = require('path')`,
@@ -9396,7 +9986,7 @@ program.command("pm2-setup").description("Generate ecosystem.config.cjs for PM2
9396
9986
  const includeWidget = options.serverOnly ? false : hasWidget();
9397
9987
  const config = buildPm2Config(includeWidget);
9398
9988
  const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
9399
- writeFileSync6(configPath, config, "utf-8");
9989
+ writeFileSync7(configPath, config, "utf-8");
9400
9990
  console.log(`Config created: ${configPath}`);
9401
9991
  if (!includeWidget) {
9402
9992
  console.log("Note: aiusage-widget not found, only server is configured.");
@@ -9406,7 +9996,7 @@ program.command("pm2-start").description("Setup and start PM2 background service
9406
9996
  const includeWidget = options.serverOnly ? false : hasWidget();
9407
9997
  const config = buildPm2Config(includeWidget);
9408
9998
  const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
9409
- writeFileSync6(configPath, config, "utf-8");
9999
+ writeFileSync7(configPath, config, "utf-8");
9410
10000
  console.log(`Config: ${configPath}`);
9411
10001
  try {
9412
10002
  execSync5(`pm2 start "${configPath}"`, { stdio: "inherit" });