@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.
- package/README.md +5 -15
- package/dist/index.js +849 -259
- package/dist/web/_app/immutable/assets/12.C1f-IaHK.css +1 -0
- package/dist/web/_app/immutable/assets/_page.DO7aPEA-.css +1 -0
- package/dist/web/_app/immutable/chunks/{ToolSelector.CofvKVw2.js → ToolSelector.DTPGX4zP.js} +1 -1
- package/dist/web/_app/immutable/chunks/api.DC92Gfvo.js +1 -0
- package/dist/web/_app/immutable/chunks/{entry.PDoHFbXz.js → entry.Cxpjn2cB.js} +2 -2
- package/dist/web/_app/immutable/chunks/i18n.yxU9Ieh-.js +1 -0
- package/dist/web/_app/immutable/chunks/{stores.Botc0ioY.js → stores.CK2Vof2m.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.CoPtpbXI.js → app.BzFxp-0D.js} +2 -2
- package/dist/web/_app/immutable/entry/start.C2M6huNJ.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.CZMa2uzk.js → 0.5419U2xS.js} +2 -2
- package/dist/web/_app/immutable/nodes/{1.DJI2NNga.js → 1.C4em9nwH.js} +1 -1
- package/dist/web/_app/immutable/nodes/{10.DTaETIM6.js → 10.DGXLHPeN.js} +1 -1
- package/dist/web/_app/immutable/nodes/{11.CScZqjVc.js → 11.SDWfdWaC.js} +1 -1
- package/dist/web/_app/immutable/nodes/12.B5loVhie.js +1 -0
- package/dist/web/_app/immutable/nodes/{13.BFaBQy6e.js → 13.9M-J-sT0.js} +1 -1
- package/dist/web/_app/immutable/nodes/{14.BgJEC28r.js → 14.F_F2PTIe.js} +1 -1
- package/dist/web/_app/immutable/nodes/{15.DjuCO6OE.js → 15.B0ylahpi.js} +1 -1
- package/dist/web/_app/immutable/nodes/{2.CoxR54dV.js → 2.oTDdkLn9.js} +1 -1
- package/dist/web/_app/immutable/nodes/{3.Bua2oewW.js → 3.Oyghjrgn.js} +1 -1
- package/dist/web/_app/immutable/nodes/{4.DJwmYYKI.js → 4.DiPBEiKy.js} +1 -1
- package/dist/web/_app/immutable/nodes/{5.CXmtjVJL.js → 5.CtE-oF5k.js} +1 -1
- package/dist/web/_app/immutable/nodes/{6.kpxBlrHt.js → 6.tVPvf7jD.js} +1 -1
- package/dist/web/_app/immutable/nodes/{7.LSfdx4vz.js → 7.CAfC8ItM.js} +1 -1
- package/dist/web/_app/immutable/nodes/{8.ByAI01dT.js → 8.DJsWN3X2.js} +1 -1
- package/dist/web/_app/immutable/nodes/{9.Ubt4Bfxd.js → 9.30YXO0yd.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +7 -5
- package/dist/web/_app/immutable/assets/12.nNHFRI35.css +0 -1
- package/dist/web/_app/immutable/assets/_page.ZSDsIDry.css +0 -1
- package/dist/web/_app/immutable/chunks/api.Mv_KcGhp.js +0 -1
- package/dist/web/_app/immutable/chunks/i18n.CwV3KceS.js +0 -1
- package/dist/web/_app/immutable/entry/start.CaOJNprV.js +0 -1
- 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
|
|
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
|
|
17
|
-
import { join as
|
|
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
|
|
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
|
|
1930
|
-
if (!
|
|
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,
|
|
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:
|
|
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,
|
|
2600
|
-
return [method,
|
|
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(
|
|
2674
|
-
return `Leaderboard API endpoint ${
|
|
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(
|
|
3170
|
-
mkdirSync3(dirname2(
|
|
3171
|
-
const db = new Database(
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
5124
|
-
this.path =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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,
|
|
7322
|
+
for (const [index, path3] of paths.entries()) {
|
|
7021
7323
|
this.options.onProgress?.({
|
|
7022
7324
|
phase: "pulling",
|
|
7023
|
-
currentPath:
|
|
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(
|
|
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:
|
|
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(
|
|
7055
|
-
const existingContent = await this.backend.readFile(
|
|
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(
|
|
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
|
|
7069
|
-
if (!byPath.has(
|
|
7070
|
-
byPath.get(
|
|
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, [
|
|
7382
|
+
for (const [index, [path3, localRecords]] of uploads.entries()) {
|
|
7081
7383
|
this.options.onProgress?.({
|
|
7082
7384
|
phase: "uploading",
|
|
7083
|
-
currentPath:
|
|
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(
|
|
7391
|
+
await this.uploadFile(path3, localSyncRecords);
|
|
7090
7392
|
totalUploaded += localRecords.length;
|
|
7091
7393
|
this.options.onProgress?.({
|
|
7092
7394
|
phase: "uploading",
|
|
7093
|
-
currentPath:
|
|
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,
|
|
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,
|
|
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
|
|
7486
|
+
const path3 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
|
|
7185
7487
|
const headers = buildHeaders("GET", SYNC_PULL_PATH, "");
|
|
7186
|
-
const response = await fetch(`${serverUrl}${
|
|
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:
|
|
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(
|
|
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(
|
|
7680
|
+
async readFile(path3) {
|
|
7359
7681
|
try {
|
|
7360
|
-
const fullPath = join8(this.dataDir,
|
|
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(
|
|
7367
|
-
const fullPath = join8(this.dataDir,
|
|
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(
|
|
7430
|
-
return `${this.prefix}${
|
|
7767
|
+
getObjectKey(path3) {
|
|
7768
|
+
return `${this.prefix}${path3}`;
|
|
7431
7769
|
}
|
|
7432
|
-
async readFile(
|
|
7433
|
-
const key = this.getObjectKey(
|
|
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(
|
|
7450
|
-
const key = this.getObjectKey(
|
|
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(
|
|
7483
|
-
const key = this.getObjectKey(
|
|
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 =
|
|
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 =
|
|
7885
|
-
if (
|
|
7886
|
-
return
|
|
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 (
|
|
7895
|
-
let filePath =
|
|
8429
|
+
if (existsSync10(webBuildDir)) {
|
|
8430
|
+
let filePath = join11(webBuildDir, url.pathname);
|
|
7896
8431
|
try {
|
|
7897
8432
|
if (statSync3(filePath).isDirectory()) {
|
|
7898
|
-
filePath =
|
|
8433
|
+
filePath = join11(filePath, "index.html");
|
|
7899
8434
|
}
|
|
7900
8435
|
} catch {
|
|
7901
8436
|
}
|
|
7902
|
-
if (!
|
|
7903
|
-
filePath =
|
|
8437
|
+
if (!existsSync10(filePath)) {
|
|
8438
|
+
filePath = join11(webBuildDir, "index.html");
|
|
7904
8439
|
}
|
|
7905
8440
|
try {
|
|
7906
|
-
const content =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8623
|
+
import { statSync as statSync4, existsSync as existsSync11 } from "fs";
|
|
8089
8624
|
import { hostname as hostname3 } from "os";
|
|
8090
|
-
import { join as
|
|
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 =
|
|
8643
|
+
const sizePath = join12(AIUSAGE_DIR, "cache.db");
|
|
8109
8644
|
let databaseSize = "0 B";
|
|
8110
|
-
if (
|
|
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
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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 =
|
|
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]
|
|
8827
|
-
[6] Recalc
|
|
9340
|
+
[5] Recalc
|
|
8828
9341
|
[0] Back
|
|
8829
9342
|
`);
|
|
8830
|
-
const choice = await prompt(rl, " Select [0-
|
|
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
|
|
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
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
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
|
-
|
|
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 "
|
|
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/
|
|
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.
|
|
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
|
-
|
|
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>", "
|
|
9251
|
-
const
|
|
9252
|
-
|
|
9253
|
-
|
|
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
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
}
|
|
9262
|
-
|
|
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
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
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
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9999
|
+
writeFileSync7(configPath, config, "utf-8");
|
|
9410
10000
|
console.log(`Config: ${configPath}`);
|
|
9411
10001
|
try {
|
|
9412
10002
|
execSync5(`pm2 start "${configPath}"`, { stdio: "inherit" });
|