@juliantanx/aiusage 1.5.1 → 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 (37) hide show
  1. package/README.md +5 -15
  2. package/dist/index.js +844 -256
  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.Cxpjn2cB.js +3 -0
  8. package/dist/web/_app/immutable/chunks/i18n.yxU9Ieh-.js +1 -0
  9. package/dist/web/_app/immutable/chunks/{stores.COyyqc3i.js → stores.CK2Vof2m.js} +1 -1
  10. package/dist/web/_app/immutable/entry/{app.fIWuRyD7.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.D19pLRbx.js → 0.5419U2xS.js} +2 -2
  13. package/dist/web/_app/immutable/nodes/{1.Cr_OSeqa.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.BbAcHr8g.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/entry.BdArkS4U.js +0 -3
  35. package/dist/web/_app/immutable/chunks/i18n.CwV3KceS.js +0 -1
  36. package/dist/web/_app/immutable/entry/start.B0xmplla.js +0 -1
  37. 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")) {
@@ -2594,8 +2628,8 @@ function generateNonce() {
2594
2628
  function generateIdempotencyKey() {
2595
2629
  return randomBytes(16).toString("hex");
2596
2630
  }
2597
- function buildCanonicalString(method, path2, bodyHash, timestamp, nonce, deviceId, idempotencyKey) {
2598
- 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");
2599
2633
  }
2600
2634
  function base64url(data) {
2601
2635
  const buf = typeof data === "string" ? Buffer.from(data) : data;
@@ -2668,8 +2702,8 @@ async function readJsonOrNull(response) {
2668
2702
  if (!contentType.includes("application/json")) return null;
2669
2703
  return response.json().catch(() => null);
2670
2704
  }
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.`;
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.`;
2673
2707
  }
2674
2708
  async function fetchLeaderboard(serverUrl, options = {}) {
2675
2709
  const params = new URLSearchParams();
@@ -3164,9 +3198,9 @@ function initializeDatabase(db) {
3164
3198
  applyPragmas(db);
3165
3199
  runMigrations(db);
3166
3200
  }
3167
- function createDatabase(path2) {
3168
- mkdirSync3(dirname2(path2), { recursive: true, mode: 448 });
3169
- const db = new Database(path2);
3201
+ function createDatabase(path3) {
3202
+ mkdirSync3(dirname2(path3), { recursive: true, mode: 448 });
3203
+ const db = new Database(path3);
3170
3204
  initializeDatabase(db);
3171
3205
  return db;
3172
3206
  }
@@ -3364,10 +3398,10 @@ function getClientPlatform() {
3364
3398
  }
3365
3399
  function getCliVersion() {
3366
3400
  try {
3367
- const { readFileSync: readFileSync11 } = __require("fs");
3401
+ const { readFileSync: readFileSync12 } = __require("fs");
3368
3402
  const { join: join16 } = __require("path");
3369
3403
  const pkgPath = join16(__dirname, "../../package.json");
3370
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3404
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3371
3405
  return pkg.version || "0.0.0";
3372
3406
  } catch {
3373
3407
  return "0.0.0";
@@ -3645,8 +3679,273 @@ Summary: ${accepted} accepted, ${flagged} flagged, ${rejected} rejected`);
3645
3679
  }
3646
3680
  }
3647
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
+
3648
3915
  // src/api/server.ts
3649
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
+ }
3650
3949
  function recalcCosts(db) {
3651
3950
  const BATCH_SIZE2 = 1e3;
3652
3951
  let updated = 0;
@@ -3723,6 +4022,44 @@ async function readJsonBody(req) {
3723
4022
  if (!body.trim()) return {};
3724
4023
  return JSON.parse(body);
3725
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
+ }
3726
4063
  function getDeviceName() {
3727
4064
  return loadConfig()?.device || hostname() || `${platform3()}-${process.arch}`;
3728
4065
  }
@@ -4954,10 +5291,54 @@ function createApiServer(db, options) {
4954
5291
  }
4955
5292
  }
4956
5293
  if (url.pathname === "/api/detected-tools" && req.method === "GET") {
4957
- const tools = discoverTools();
5294
+ const tools = attachManualImportMetadata(discoverTools(), db);
4958
5295
  json(res, { tools });
4959
5296
  return;
4960
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
+ }
4961
5342
  if (url.pathname === "/api/exchange-rate/refresh" && req.method === "POST") {
4962
5343
  const rate = await fetchExchangeRate();
4963
5344
  if (rate == null) {
@@ -4981,114 +5362,12 @@ function createApiServer(db, options) {
4981
5362
 
4982
5363
  // src/commands/parse.ts
4983
5364
  import Database2 from "better-sqlite3";
4984
- 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";
4985
5366
  import { join as join7 } from "path";
4986
5367
  import { hostname as hostname2 } from "os";
4987
5368
 
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);
5052
- }
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);
5060
- });
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
- };
5088
- }
5089
-
5090
5369
  // src/watermark.ts
5091
- 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";
5092
5371
  function defaultFileData() {
5093
5372
  return {
5094
5373
  "claude-code": {},
@@ -5099,6 +5378,7 @@ function defaultFileData() {
5099
5378
  "qoder": {},
5100
5379
  "cursor": {},
5101
5380
  "kilocode": {},
5381
+ "kelivo": {},
5102
5382
  "copilot": {},
5103
5383
  "gemini": {},
5104
5384
  "kimi": {},
@@ -5118,8 +5398,8 @@ function defaultFileData() {
5118
5398
  var WatermarkManager = class {
5119
5399
  data;
5120
5400
  path;
5121
- constructor(path2) {
5122
- this.path = path2;
5401
+ constructor(path3) {
5402
+ this.path = path3;
5123
5403
  this.data = this.load();
5124
5404
  }
5125
5405
  load() {
@@ -5127,7 +5407,7 @@ var WatermarkManager = class {
5127
5407
  return { files: defaultFileData() };
5128
5408
  }
5129
5409
  try {
5130
- const content = readFileSync6(this.path, "utf-8");
5410
+ const content = readFileSync7(this.path, "utf-8");
5131
5411
  const parsed = JSON.parse(content);
5132
5412
  if (parsed && typeof parsed === "object" && !("files" in parsed)) {
5133
5413
  return { files: { ...defaultFileData(), ...parsed } };
@@ -5138,7 +5418,7 @@ var WatermarkManager = class {
5138
5418
  }
5139
5419
  }
5140
5420
  save() {
5141
- writeFileSync4(this.path, JSON.stringify(this.data, null, 2), "utf-8");
5421
+ writeFileSync5(this.path, JSON.stringify(this.data, null, 2), "utf-8");
5142
5422
  }
5143
5423
  getEntry(tool, filePath) {
5144
5424
  return this.data.files[tool]?.[filePath] ?? null;
@@ -6023,12 +6303,12 @@ function parseUiMessagesFile(options) {
6023
6303
  const errors = [];
6024
6304
  let messages;
6025
6305
  try {
6026
- const parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
6306
+ const parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
6027
6307
  messages = Array.isArray(parsed) ? parsed : [];
6028
6308
  } catch (e) {
6029
6309
  return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
6030
6310
  }
6031
- const taskId = filePath.split("/").slice(-2, -1)[0] || "unknown";
6311
+ const taskId = filePath.replace(/\\/g, "/").split("/").slice(-2, -1)[0] || "unknown";
6032
6312
  for (const [index, message] of messages.entries()) {
6033
6313
  if (message?.say !== "api_req_started" || typeof message.text !== "string") continue;
6034
6314
  let payload;
@@ -6080,7 +6360,7 @@ function normalizeKiroModel(value) {
6080
6360
  if (typeof value !== "string" || !value.trim()) return "kiro-agent";
6081
6361
  return value.trim().toLowerCase().replace(/_/g, "-").replace(/^(anthropic\.|openai\.|aws\.)/, "").replace(/:\d+$/, "").replace(/-\d{8}-v\d+(?:-\d+)?$/i, "");
6082
6362
  }
6083
- function parseTimestamp(value, fallback) {
6363
+ function parseTimestamp2(value, fallback) {
6084
6364
  if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
6085
6365
  if (typeof value === "string" && value.trim()) {
6086
6366
  const parsed = new Date(value).getTime();
@@ -6101,7 +6381,7 @@ function parseKiroSessionFile(options) {
6101
6381
  const errors = [];
6102
6382
  let parsed;
6103
6383
  try {
6104
- parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
6384
+ parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
6105
6385
  } catch (e) {
6106
6386
  return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
6107
6387
  }
@@ -6114,7 +6394,7 @@ function parseKiroSessionFile(options) {
6114
6394
  const outputTokens = toNumber2(turn?.output_token_count);
6115
6395
  if (inputTokens + outputTokens === 0) continue;
6116
6396
  const model = normalizeKiroModel(turn?.model_id ?? sessionModel);
6117
- const recordTs = parseTimestamp(
6397
+ const recordTs = parseTimestamp2(
6118
6398
  turn?.request_start_timestamp_ms ?? turn?.start_timestamp ?? turn?.end_timestamp,
6119
6399
  now
6120
6400
  );
@@ -6225,7 +6505,30 @@ async function runParse(db, filterTool, options) {
6225
6505
  onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
6226
6506
  continue;
6227
6507
  }
6228
- 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");
6229
6532
  const lines = content.split("\n");
6230
6533
  let byteOffset = 0;
6231
6534
  let fileCwd;
@@ -6680,7 +6983,7 @@ function backfillCodexModels(db) {
6680
6983
  );
6681
6984
  for (const [sourceFile, fileRows] of byFile) {
6682
6985
  try {
6683
- const content = readFileSync7(sourceFile, "utf-8");
6986
+ const content = readFileSync8(sourceFile, "utf-8");
6684
6987
  const lines = content.split("\n");
6685
6988
  const offsetToModel = /* @__PURE__ */ new Map();
6686
6989
  let currentModel = "";
@@ -6728,7 +7031,7 @@ function backfillMissingToolCalls(db, exchangeRate) {
6728
7031
  }
6729
7032
  for (const [sourceFile, fileRows] of rowsByFile) {
6730
7033
  try {
6731
- const content = readFileSync7(sourceFile, "utf-8");
7034
+ const content = readFileSync8(sourceFile, "utf-8");
6732
7035
  const lines = content.split("\n");
6733
7036
  const aggregator = new Aggregator();
6734
7037
  const recordIdByOffset = /* @__PURE__ */ new Map();
@@ -6907,6 +7210,7 @@ var RECORD_ID_SYNC_TOOLS = /* @__PURE__ */ new Set([
6907
7210
  "qoder",
6908
7211
  "cursor",
6909
7212
  "kilocode",
7213
+ "kelivo",
6910
7214
  "goose",
6911
7215
  "zed",
6912
7216
  "kiro",
@@ -7015,15 +7319,15 @@ var SyncOrchestrator = class {
7015
7319
  });
7016
7320
  if (paths.length === 0) return 0;
7017
7321
  let totalPulled = 0;
7018
- for (const [index, path2] of paths.entries()) {
7322
+ for (const [index, path3] of paths.entries()) {
7019
7323
  this.options.onProgress?.({
7020
7324
  phase: "pulling",
7021
- currentPath: path2,
7325
+ currentPath: path3,
7022
7326
  completedFiles: index,
7023
7327
  totalFiles: paths.length,
7024
7328
  pulledCount: totalPulled
7025
7329
  });
7026
- const content = await this.backend.readFile(path2);
7330
+ const content = await this.backend.readFile(path3);
7027
7331
  if (!content) continue;
7028
7332
  for (const line of content.split("\n").filter(Boolean)) {
7029
7333
  try {
@@ -7041,7 +7345,7 @@ var SyncOrchestrator = class {
7041
7345
  }
7042
7346
  this.options.onProgress?.({
7043
7347
  phase: "pulling",
7044
- currentPath: path2,
7348
+ currentPath: path3,
7045
7349
  completedFiles: index + 1,
7046
7350
  totalFiles: paths.length,
7047
7351
  pulledCount: totalPulled
@@ -7049,13 +7353,13 @@ var SyncOrchestrator = class {
7049
7353
  }
7050
7354
  return totalPulled;
7051
7355
  }
7052
- async uploadFile(path2, localSyncRecords) {
7053
- const existingContent = await this.backend.readFile(path2);
7356
+ async uploadFile(path3, localSyncRecords) {
7357
+ const existingContent = await this.backend.readFile(path3);
7054
7358
  const existing = existingContent ? parseNdjson(existingContent) : /* @__PURE__ */ new Map();
7055
7359
  const changedCount = mergeRecords(existing, localSyncRecords);
7056
7360
  if (changedCount === 0) return 0;
7057
7361
  const content = Array.from(existing.values()).map((r) => JSON.stringify(r)).join("\n") + "\n";
7058
- await this.backend.writeFile(path2, content);
7362
+ await this.backend.writeFile(path3, content);
7059
7363
  return changedCount;
7060
7364
  }
7061
7365
  async upload() {
@@ -7063,9 +7367,9 @@ var SyncOrchestrator = class {
7063
7367
  if (unsynced.length === 0) return 0;
7064
7368
  const byPath = /* @__PURE__ */ new Map();
7065
7369
  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);
7370
+ const path3 = getSyncPath(record.ts, this.options.deviceInstanceId);
7371
+ if (!byPath.has(path3)) byPath.set(path3, []);
7372
+ byPath.get(path3).push(record);
7069
7373
  }
7070
7374
  let totalUploaded = 0;
7071
7375
  const uploads = Array.from(byPath.entries());
@@ -7075,20 +7379,20 @@ var SyncOrchestrator = class {
7075
7379
  totalFiles: uploads.length,
7076
7380
  uploadedCount: 0
7077
7381
  });
7078
- for (const [index, [path2, localRecords]] of uploads.entries()) {
7382
+ for (const [index, [path3, localRecords]] of uploads.entries()) {
7079
7383
  this.options.onProgress?.({
7080
7384
  phase: "uploading",
7081
- currentPath: path2,
7385
+ currentPath: path3,
7082
7386
  completedFiles: index,
7083
7387
  totalFiles: uploads.length,
7084
7388
  uploadedCount: totalUploaded
7085
7389
  });
7086
7390
  const localSyncRecords = localRecords.map(mapStatsRecordToSyncRecord);
7087
- await this.uploadFile(path2, localSyncRecords);
7391
+ await this.uploadFile(path3, localSyncRecords);
7088
7392
  totalUploaded += localRecords.length;
7089
7393
  this.options.onProgress?.({
7090
7394
  phase: "uploading",
7091
- currentPath: path2,
7395
+ currentPath: path3,
7092
7396
  completedFiles: index + 1,
7093
7397
  totalFiles: uploads.length,
7094
7398
  uploadedCount: totalUploaded
@@ -7113,14 +7417,14 @@ var CloudSyncError = class extends Error {
7113
7417
  code;
7114
7418
  retryAfter;
7115
7419
  };
7116
- function buildHeaders(method, path2, body) {
7420
+ function buildHeaders(method, path3, body) {
7117
7421
  const creds = loadCredentials();
7118
7422
  if (!creds) throw new CloudSyncError("Not logged in. Run `aiusage login` first.", "not_logged_in");
7119
7423
  const bodyHash = sha256(body);
7120
7424
  const timestamp = Date.now().toString();
7121
7425
  const nonce = generateNonce();
7122
7426
  const idempotencyKey = generateIdempotencyKey();
7123
- 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);
7124
7428
  const signature = computeHmac(creds.device_secret, canonical);
7125
7429
  return {
7126
7430
  "Content-Type": "application/json",
@@ -7179,9 +7483,9 @@ async function cloudPull(cursor, limit = 1e3) {
7179
7483
  if (cursor) params.set("cursor", cursor);
7180
7484
  params.set("limit", String(limit));
7181
7485
  const query = params.toString();
7182
- const path2 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
7486
+ const path3 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
7183
7487
  const headers = buildHeaders("GET", SYNC_PULL_PATH, "");
7184
- const response = await fetch(`${serverUrl}${path2}`, { method: "GET", headers });
7488
+ const response = await fetch(`${serverUrl}${path3}`, { method: "GET", headers });
7185
7489
  const data = await readJsonOrNull2(response);
7186
7490
  if (!response.ok) {
7187
7491
  const errObj = data?.error;
@@ -7198,12 +7502,32 @@ async function cloudPull(cursor, limit = 1e3) {
7198
7502
  syncGeneration: data.sync_generation || 1
7199
7503
  };
7200
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
+ }
7201
7525
  function getClientVersion() {
7202
7526
  try {
7203
- const { readFileSync: readFileSync11 } = __require("fs");
7527
+ const { readFileSync: readFileSync12 } = __require("fs");
7204
7528
  const { join: join16 } = __require("path");
7205
7529
  const pkgPath = join16(__dirname, "../../package.json");
7206
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7530
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7207
7531
  return pkg.version || "0.0.0";
7208
7532
  } catch {
7209
7533
  return "0.0.0";
@@ -7293,7 +7617,7 @@ var CloudSyncOrchestrator = class {
7293
7617
  // src/sync/git.ts
7294
7618
  import { execFile } from "child_process";
7295
7619
  import { promisify } from "util";
7296
- import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
7620
+ import { readFile, writeFile, mkdir, readdir, stat, unlink, rm } from "fs/promises";
7297
7621
  import { join as join8 } from "path";
7298
7622
  var exec = promisify(execFile);
7299
7623
  var GitSyncBackend = class _GitSyncBackend {
@@ -7353,16 +7677,16 @@ var GitSyncBackend = class _GitSyncBackend {
7353
7677
  }
7354
7678
  }
7355
7679
  }
7356
- async readFile(path2) {
7680
+ async readFile(path3) {
7357
7681
  try {
7358
- const fullPath = join8(this.dataDir, path2);
7682
+ const fullPath = join8(this.dataDir, path3);
7359
7683
  return await readFile(fullPath, "utf-8");
7360
7684
  } catch {
7361
7685
  return null;
7362
7686
  }
7363
7687
  }
7364
- async writeFile(path2, content) {
7365
- const fullPath = join8(this.dataDir, path2);
7688
+ async writeFile(path3, content) {
7689
+ const fullPath = join8(this.dataDir, path3);
7366
7690
  await mkdir(join8(fullPath, ".."), { recursive: true });
7367
7691
  await writeFile(fullPath, content, "utf-8");
7368
7692
  }
@@ -7373,6 +7697,22 @@ var GitSyncBackend = class _GitSyncBackend {
7373
7697
  return [];
7374
7698
  }
7375
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
+ }
7376
7716
  static MAX_PUSH_RETRIES = 3;
7377
7717
  /** Commit and push all changes. Returns true if anything was pushed. */
7378
7718
  async flush() {
@@ -7407,7 +7747,7 @@ var GitSyncBackend = class _GitSyncBackend {
7407
7747
  };
7408
7748
 
7409
7749
  // src/sync/s3.ts
7410
- 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";
7411
7751
  var S3SyncBackend = class {
7412
7752
  client;
7413
7753
  bucket;
@@ -7424,11 +7764,11 @@ var S3SyncBackend = class {
7424
7764
  }
7425
7765
  });
7426
7766
  }
7427
- getObjectKey(path2) {
7428
- return `${this.prefix}${path2}`;
7767
+ getObjectKey(path3) {
7768
+ return `${this.prefix}${path3}`;
7429
7769
  }
7430
- async readFile(path2) {
7431
- const key = this.getObjectKey(path2);
7770
+ async readFile(path3) {
7771
+ const key = this.getObjectKey(path3);
7432
7772
  try {
7433
7773
  const command = new GetObjectCommand({
7434
7774
  Bucket: this.bucket,
@@ -7444,8 +7784,8 @@ var S3SyncBackend = class {
7444
7784
  throw error;
7445
7785
  }
7446
7786
  }
7447
- async writeFile(path2, content) {
7448
- const key = this.getObjectKey(path2);
7787
+ async writeFile(path3, content) {
7788
+ const key = this.getObjectKey(path3);
7449
7789
  const command = new PutObjectCommand({
7450
7790
  Bucket: this.bucket,
7451
7791
  Key: key,
@@ -7477,8 +7817,8 @@ var S3SyncBackend = class {
7477
7817
  } while (continuationToken);
7478
7818
  return files.sort();
7479
7819
  }
7480
- async fileExists(path2) {
7481
- const key = this.getObjectKey(path2);
7820
+ async fileExists(path3) {
7821
+ const key = this.getObjectKey(path3);
7482
7822
  try {
7483
7823
  const command = new HeadObjectCommand({
7484
7824
  Bucket: this.bucket,
@@ -7493,6 +7833,31 @@ var S3SyncBackend = class {
7493
7833
  throw error;
7494
7834
  }
7495
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
+ }
7496
7861
  };
7497
7862
 
7498
7863
  // src/commands/sync.ts
@@ -7601,6 +7966,8 @@ async function runSync(db, options) {
7601
7966
  }
7602
7967
 
7603
7968
  // src/commands/clean.ts
7969
+ import { unlinkSync as unlinkSync2, existsSync as existsSync9 } from "fs";
7970
+ import { join as join10 } from "path";
7604
7971
  function cleanOldData(db, days) {
7605
7972
  const cutoff = Date.now() - days * 864e5;
7606
7973
  const recordsResult = db.prepare("DELETE FROM records WHERE ts < ?").run(cutoff);
@@ -7615,6 +7982,172 @@ function cleanOldData(db, days) {
7615
7982
  deletedOrphanToolCalls
7616
7983
  };
7617
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
+ }
7618
8151
 
7619
8152
  // src/sync/runtime.ts
7620
8153
  var SyncRuntimeController = class {
@@ -7817,7 +8350,7 @@ var RuntimeSettingsController = class {
7817
8350
 
7818
8351
  // src/commands/serve.ts
7819
8352
  var MAX_PORT_ATTEMPTS = 10;
7820
- var PORT_FILE = join10(AIUSAGE_DIR, ".serve-port");
8353
+ var PORT_FILE = join11(AIUSAGE_DIR, ".serve-port");
7821
8354
  var MIME_TYPES = {
7822
8355
  ".html": "text/html",
7823
8356
  ".js": "application/javascript",
@@ -7879,11 +8412,11 @@ function serve(options) {
7879
8412
  onConfigUpdated: () => runtimeSettings.reload()
7880
8413
  });
7881
8414
  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");
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");
7885
8418
  })();
7886
- if (!existsSync9(webBuildDir)) {
8419
+ if (!existsSync10(webBuildDir)) {
7887
8420
  console.error("Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage");
7888
8421
  process.exit(1);
7889
8422
  }
@@ -7893,19 +8426,19 @@ function serve(options) {
7893
8426
  apiServer.emit("request", req, res);
7894
8427
  return;
7895
8428
  }
7896
- if (existsSync9(webBuildDir)) {
7897
- let filePath = join10(webBuildDir, url.pathname);
8429
+ if (existsSync10(webBuildDir)) {
8430
+ let filePath = join11(webBuildDir, url.pathname);
7898
8431
  try {
7899
8432
  if (statSync3(filePath).isDirectory()) {
7900
- filePath = join10(filePath, "index.html");
8433
+ filePath = join11(filePath, "index.html");
7901
8434
  }
7902
8435
  } catch {
7903
8436
  }
7904
- if (!existsSync9(filePath)) {
7905
- filePath = join10(webBuildDir, "index.html");
8437
+ if (!existsSync10(filePath)) {
8438
+ filePath = join11(webBuildDir, "index.html");
7906
8439
  }
7907
8440
  try {
7908
- const content = readFileSync8(filePath);
8441
+ const content = readFileSync9(filePath);
7909
8442
  const ext = extname2(filePath);
7910
8443
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7911
8444
  res.writeHead(200, { "Content-Type": contentType });
@@ -7925,7 +8458,7 @@ function serve(options) {
7925
8458
  };
7926
8459
  server.on("listening", () => {
7927
8460
  started = true;
7928
- writeFileSync5(PORT_FILE, String(currentPort), "utf-8");
8461
+ writeFileSync6(PORT_FILE, String(currentPort), "utf-8");
7929
8462
  console.log(`aiusage serve listening on http://localhost:${currentPort}`);
7930
8463
  });
7931
8464
  server.on("error", (error) => {
@@ -7943,7 +8476,7 @@ function serve(options) {
7943
8476
  listenOnPort(options.port);
7944
8477
  const cleanup = () => {
7945
8478
  try {
7946
- unlinkSync2(PORT_FILE);
8479
+ unlinkSync3(PORT_FILE);
7947
8480
  } catch {
7948
8481
  }
7949
8482
  };
@@ -8087,9 +8620,9 @@ Consent recorded for schema v1.`
8087
8620
  }
8088
8621
 
8089
8622
  // src/commands/status.ts
8090
- import { statSync as statSync4, existsSync as existsSync10 } from "fs";
8623
+ import { statSync as statSync4, existsSync as existsSync11 } from "fs";
8091
8624
  import { hostname as hostname3 } from "os";
8092
- import { join as join11 } from "path";
8625
+ import { join as join12 } from "path";
8093
8626
  function formatFileSize(bytes) {
8094
8627
  if (bytes < 1024) return `${bytes} B`;
8095
8628
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -8107,9 +8640,9 @@ function generateStatus(db) {
8107
8640
  const schemaVersion = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get()?.version ?? 0;
8108
8641
  const tableCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get()?.count ?? 0;
8109
8642
  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");
8643
+ const sizePath = join12(AIUSAGE_DIR, "cache.db");
8111
8644
  let databaseSize = "0 B";
8112
- if (existsSync10(sizePath)) {
8645
+ if (existsSync11(sizePath)) {
8113
8646
  try {
8114
8647
  const stat2 = statSync4(sizePath);
8115
8648
  databaseSize = formatFileSize(stat2.size);
@@ -8186,27 +8719,6 @@ function exportData(db, format) {
8186
8719
  return lines.join("\n");
8187
8720
  }
8188
8721
 
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
8722
  // src/commands/recalc.ts
8211
8723
  var BATCH_SIZE = 1e3;
8212
8724
  function recalcPricing(db) {
@@ -8319,7 +8831,7 @@ var SyncProgressReporter = class {
8319
8831
  };
8320
8832
 
8321
8833
  // src/commands/widget.ts
8322
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
8834
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
8323
8835
  import { spawn, execSync as execSync2 } from "child_process";
8324
8836
  import { join as join13 } from "path";
8325
8837
  var WIDGET_PID_PATH = join13(AIUSAGE_DIR, "widget.pid");
@@ -8347,7 +8859,7 @@ function isWidgetRunning() {
8347
8859
  if (!existsSync12(WIDGET_PID_PATH)) return false;
8348
8860
  let pid;
8349
8861
  try {
8350
- pid = parseInt(readFileSync9(WIDGET_PID_PATH, "utf-8").trim(), 10);
8862
+ pid = parseInt(readFileSync10(WIDGET_PID_PATH, "utf-8").trim(), 10);
8351
8863
  } catch {
8352
8864
  return false;
8353
8865
  }
@@ -8391,10 +8903,10 @@ function getDeviceName2() {
8391
8903
  }
8392
8904
  function getCliVersion2() {
8393
8905
  try {
8394
- const { readFileSync: readFileSync11 } = __require("fs");
8906
+ const { readFileSync: readFileSync12 } = __require("fs");
8395
8907
  const { join: join16 } = __require("path");
8396
8908
  const pkgPath = join16(__dirname, "../../package.json");
8397
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
8909
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
8398
8910
  return pkg.version || "0.0.0";
8399
8911
  } catch {
8400
8912
  return "0.0.0";
@@ -8638,7 +9150,7 @@ async function runLeaderboardView(options = {}) {
8638
9150
  // src/commands/menu.ts
8639
9151
  import { createInterface } from "readline";
8640
9152
  import { spawn as spawn2, spawnSync, execSync as execSync4 } from "child_process";
8641
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
9153
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
8642
9154
  import { join as join14 } from "path";
8643
9155
  var DEFAULT_PORT = 3847;
8644
9156
  var PORT_FILE2 = join14(AIUSAGE_DIR, ".serve-port");
@@ -8647,7 +9159,7 @@ function clearScreen() {
8647
9159
  }
8648
9160
  function getPort() {
8649
9161
  if (existsSync13(PORT_FILE2)) {
8650
- const raw = readFileSync10(PORT_FILE2, "utf-8").trim();
9162
+ const raw = readFileSync11(PORT_FILE2, "utf-8").trim();
8651
9163
  const n = parseInt(raw, 10);
8652
9164
  if (!isNaN(n) && n > 0) return n;
8653
9165
  }
@@ -8825,11 +9337,10 @@ async function dataSubmenu(rl) {
8825
9337
  [2] Summary
8826
9338
  [3] Export
8827
9339
  [4] Clean
8828
- [5] Reset
8829
- [6] Recalc
9340
+ [5] Recalc
8830
9341
  [0] Back
8831
9342
  `);
8832
- const choice = await prompt(rl, " Select [0-6]: ");
9343
+ const choice = await prompt(rl, " Select [0-5]: ");
8833
9344
  switch (choice) {
8834
9345
  case "1": {
8835
9346
  const progress = await prompt(rl, " Show progress? [Y/N]: ");
@@ -8865,28 +9376,28 @@ async function dataSubmenu(rl) {
8865
9376
  break;
8866
9377
  }
8867
9378
  case "4": {
8868
- const dur = await prompt(rl, " Duration to keep (default 180d): ");
8869
- const before = dur || "180d";
9379
+ const mode = await prompt(rl, " 1=Old data (180d) 2=Old data (custom) 3=All (=reset): ");
8870
9380
  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"]);
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"]);
8883
9394
  } else {
8884
- console.log("\n Cancelled.");
9395
+ runCommand(["clean", "--before", "180d", "--yes"]);
8885
9396
  }
8886
9397
  await prompt(rl, "\n Press Enter to continue...");
8887
9398
  break;
8888
9399
  }
8889
- case "6": {
9400
+ case "5": {
8890
9401
  console.log("");
8891
9402
  runCommand(["recalc"]);
8892
9403
  await prompt(rl, "\n Press Enter to continue...");
@@ -9101,7 +9612,7 @@ async function runMenu() {
9101
9612
  ${dashboardStatusLine()}
9102
9613
 
9103
9614
  [1] Dashboard (serve/stop/restart/open)
9104
- [2] Data (parse/summary/export/clean/reset/recalc)
9615
+ [2] Data (parse/summary/export/clean/recalc)
9105
9616
  [3] Sync (init/sync)
9106
9617
  [4] Leaderboard (view/login/upload/status/logout)
9107
9618
  [5] System (status/widget/pm2/update)
@@ -9140,7 +9651,7 @@ ${dashboardStatusLine()}
9140
9651
  import { join as join15 } from "path";
9141
9652
  var DB_PATH2 = join15(AIUSAGE_DIR, "cache.db");
9142
9653
  var program = new Command();
9143
- program.name("aiusage").version(true ? "1.5.1" : "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");
9144
9655
  program.action(() => {
9145
9656
  const unknownCommand = program.args.find((arg) => typeof arg === "string");
9146
9657
  if (unknownCommand) {
@@ -9222,7 +9733,7 @@ program.command("export").description("Export data").requiredOption("--format <f
9222
9733
  const db = createDatabase(DB_PATH2);
9223
9734
  const data = exportData(db, format);
9224
9735
  if (options.output) {
9225
- writeFileSync6(options.output, data, "utf-8");
9736
+ writeFileSync7(options.output, data, "utf-8");
9226
9737
  console.log(`Exported to ${options.output}`);
9227
9738
  } else {
9228
9739
  console.log(data);
@@ -9249,32 +9760,109 @@ program.command("parse").description("Parse AI tool session logs").option("--too
9249
9760
  db.close();
9250
9761
  }
9251
9762
  });
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.");
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.");
9256
9769
  process.exit(1);
9257
9770
  }
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) => {
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
+ };
9265
9809
  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);
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
+ }
9270
9830
  }
9271
9831
  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.");
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`);
9276
9844
  }
9277
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.");
9278
9866
  });
9279
9867
  program.command("recalc").description("Recalculate costs").option("--pricing", "Recalculate using latest pricing").action(() => {
9280
9868
  const db = createDatabase(DB_PATH2);
@@ -9351,7 +9939,7 @@ function hasWidget() {
9351
9939
  }
9352
9940
  function buildPm2Config(includeWidget) {
9353
9941
  const wrapperPath = join15(AIUSAGE_DIR, "pm2-server.cjs");
9354
- writeFileSync6(wrapperPath, [
9942
+ writeFileSync7(wrapperPath, [
9355
9943
  `// Auto-generated by aiusage pm2-start \u2014 do not edit`,
9356
9944
  `const { execSync, spawn } = require('child_process')`,
9357
9945
  `const { dirname, join } = require('path')`,
@@ -9398,7 +9986,7 @@ program.command("pm2-setup").description("Generate ecosystem.config.cjs for PM2
9398
9986
  const includeWidget = options.serverOnly ? false : hasWidget();
9399
9987
  const config = buildPm2Config(includeWidget);
9400
9988
  const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
9401
- writeFileSync6(configPath, config, "utf-8");
9989
+ writeFileSync7(configPath, config, "utf-8");
9402
9990
  console.log(`Config created: ${configPath}`);
9403
9991
  if (!includeWidget) {
9404
9992
  console.log("Note: aiusage-widget not found, only server is configured.");
@@ -9408,7 +9996,7 @@ program.command("pm2-start").description("Setup and start PM2 background service
9408
9996
  const includeWidget = options.serverOnly ? false : hasWidget();
9409
9997
  const config = buildPm2Config(includeWidget);
9410
9998
  const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
9411
- writeFileSync6(configPath, config, "utf-8");
9999
+ writeFileSync7(configPath, config, "utf-8");
9412
10000
  console.log(`Config: ${configPath}`);
9413
10001
  try {
9414
10002
  execSync5(`pm2 start "${configPath}"`, { stdio: "inherit" });