@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.
- package/README.md +5 -15
- package/dist/index.js +844 -256
- 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.Cxpjn2cB.js +3 -0
- package/dist/web/_app/immutable/chunks/i18n.yxU9Ieh-.js +1 -0
- package/dist/web/_app/immutable/chunks/{stores.COyyqc3i.js → stores.CK2Vof2m.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.fIWuRyD7.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.D19pLRbx.js → 0.5419U2xS.js} +2 -2
- package/dist/web/_app/immutable/nodes/{1.Cr_OSeqa.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.BbAcHr8g.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/entry.BdArkS4U.js +0 -3
- package/dist/web/_app/immutable/chunks/i18n.CwV3KceS.js +0 -1
- package/dist/web/_app/immutable/entry/start.B0xmplla.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")) {
|
|
@@ -2594,8 +2628,8 @@ function generateNonce() {
|
|
|
2594
2628
|
function generateIdempotencyKey() {
|
|
2595
2629
|
return randomBytes(16).toString("hex");
|
|
2596
2630
|
}
|
|
2597
|
-
function buildCanonicalString(method,
|
|
2598
|
-
return [method,
|
|
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(
|
|
2672
|
-
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.`;
|
|
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(
|
|
3168
|
-
mkdirSync3(dirname2(
|
|
3169
|
-
const db = new Database(
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
5122
|
-
this.path =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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,
|
|
7322
|
+
for (const [index, path3] of paths.entries()) {
|
|
7019
7323
|
this.options.onProgress?.({
|
|
7020
7324
|
phase: "pulling",
|
|
7021
|
-
currentPath:
|
|
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(
|
|
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:
|
|
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(
|
|
7053
|
-
const existingContent = await this.backend.readFile(
|
|
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(
|
|
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
|
|
7067
|
-
if (!byPath.has(
|
|
7068
|
-
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);
|
|
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, [
|
|
7382
|
+
for (const [index, [path3, localRecords]] of uploads.entries()) {
|
|
7079
7383
|
this.options.onProgress?.({
|
|
7080
7384
|
phase: "uploading",
|
|
7081
|
-
currentPath:
|
|
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(
|
|
7391
|
+
await this.uploadFile(path3, localSyncRecords);
|
|
7088
7392
|
totalUploaded += localRecords.length;
|
|
7089
7393
|
this.options.onProgress?.({
|
|
7090
7394
|
phase: "uploading",
|
|
7091
|
-
currentPath:
|
|
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,
|
|
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,
|
|
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
|
|
7486
|
+
const path3 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
|
|
7183
7487
|
const headers = buildHeaders("GET", SYNC_PULL_PATH, "");
|
|
7184
|
-
const response = await fetch(`${serverUrl}${
|
|
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:
|
|
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(
|
|
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(
|
|
7680
|
+
async readFile(path3) {
|
|
7357
7681
|
try {
|
|
7358
|
-
const fullPath = join8(this.dataDir,
|
|
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(
|
|
7365
|
-
const fullPath = join8(this.dataDir,
|
|
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(
|
|
7428
|
-
return `${this.prefix}${
|
|
7767
|
+
getObjectKey(path3) {
|
|
7768
|
+
return `${this.prefix}${path3}`;
|
|
7429
7769
|
}
|
|
7430
|
-
async readFile(
|
|
7431
|
-
const key = this.getObjectKey(
|
|
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(
|
|
7448
|
-
const key = this.getObjectKey(
|
|
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(
|
|
7481
|
-
const key = this.getObjectKey(
|
|
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 =
|
|
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 =
|
|
7883
|
-
if (
|
|
7884
|
-
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");
|
|
7885
8418
|
})();
|
|
7886
|
-
if (!
|
|
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 (
|
|
7897
|
-
let filePath =
|
|
8429
|
+
if (existsSync10(webBuildDir)) {
|
|
8430
|
+
let filePath = join11(webBuildDir, url.pathname);
|
|
7898
8431
|
try {
|
|
7899
8432
|
if (statSync3(filePath).isDirectory()) {
|
|
7900
|
-
filePath =
|
|
8433
|
+
filePath = join11(filePath, "index.html");
|
|
7901
8434
|
}
|
|
7902
8435
|
} catch {
|
|
7903
8436
|
}
|
|
7904
|
-
if (!
|
|
7905
|
-
filePath =
|
|
8437
|
+
if (!existsSync10(filePath)) {
|
|
8438
|
+
filePath = join11(webBuildDir, "index.html");
|
|
7906
8439
|
}
|
|
7907
8440
|
try {
|
|
7908
|
-
const content =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8623
|
+
import { statSync as statSync4, existsSync as existsSync11 } from "fs";
|
|
8091
8624
|
import { hostname as hostname3 } from "os";
|
|
8092
|
-
import { join as
|
|
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 =
|
|
8643
|
+
const sizePath = join12(AIUSAGE_DIR, "cache.db");
|
|
8111
8644
|
let databaseSize = "0 B";
|
|
8112
|
-
if (
|
|
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
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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 =
|
|
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]
|
|
8829
|
-
[6] Recalc
|
|
9340
|
+
[5] Recalc
|
|
8830
9341
|
[0] Back
|
|
8831
9342
|
`);
|
|
8832
|
-
const choice = await prompt(rl, " Select [0-
|
|
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
|
|
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
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
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
|
-
|
|
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 "
|
|
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/
|
|
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.
|
|
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
|
-
|
|
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>", "
|
|
9253
|
-
const
|
|
9254
|
-
|
|
9255
|
-
|
|
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
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
}
|
|
9264
|
-
|
|
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
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
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
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
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`);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9999
|
+
writeFileSync7(configPath, config, "utf-8");
|
|
9412
10000
|
console.log(`Config: ${configPath}`);
|
|
9413
10001
|
try {
|
|
9414
10002
|
execSync5(`pm2 start "${configPath}"`, { stdio: "inherit" });
|