tokentracker-cli 0.12.1 → 0.13.0
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 +4 -2
- package/dashboard/dist/assets/{main-CjKIAPGE.js → main-Dc116CZl.js} +3 -1
- package/dashboard/dist/brand-logos/kilo.svg +4 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +28 -0
- package/src/commands/status.js +18 -0
- package/src/commands/sync.js +66 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +251 -2
package/src/lib/rollout.js
CHANGED
|
@@ -2427,6 +2427,7 @@ async function parseOpencodeDbIncremental({
|
|
|
2427
2427
|
projectQueuePath,
|
|
2428
2428
|
onProgress,
|
|
2429
2429
|
source,
|
|
2430
|
+
cursorKey,
|
|
2430
2431
|
publicRepoResolver,
|
|
2431
2432
|
}) {
|
|
2432
2433
|
await ensureDir(path.dirname(queuePath));
|
|
@@ -2442,7 +2443,8 @@ async function parseOpencodeDbIncremental({
|
|
|
2442
2443
|
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
2443
2444
|
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
2444
2445
|
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
2445
|
-
const
|
|
2446
|
+
const cursorNamespace = typeof cursorKey === "string" && cursorKey.length > 0 ? cursorKey : "opencode";
|
|
2447
|
+
const opencodeState = normalizeOpencodeState(cursors?.[cursorNamespace]);
|
|
2446
2448
|
const messageIndex = opencodeState.messages;
|
|
2447
2449
|
const touchedBuckets = new Set();
|
|
2448
2450
|
const defaultSource = normalizeSourceInput(source) || "opencode";
|
|
@@ -2561,7 +2563,7 @@ async function parseOpencodeDbIncremental({
|
|
|
2561
2563
|
hourlyState.updatedAt = new Date().toISOString();
|
|
2562
2564
|
cursors.hourly = hourlyState;
|
|
2563
2565
|
opencodeState.updatedAt = new Date().toISOString();
|
|
2564
|
-
cursors
|
|
2566
|
+
cursors[cursorNamespace] = opencodeState;
|
|
2565
2567
|
if (projectState) {
|
|
2566
2568
|
projectState.updatedAt = new Date().toISOString();
|
|
2567
2569
|
cursors.projectHourly = projectState;
|
|
@@ -4569,6 +4571,249 @@ function resolveOmpDefaultModel() {
|
|
|
4569
4571
|
return "omp-unknown";
|
|
4570
4572
|
}
|
|
4571
4573
|
|
|
4574
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4575
|
+
// Kilo Code VS Code extension — passive reader for VS Code-family
|
|
4576
|
+
// globalStorage/kilocode.kilo-code/tasks/<uuid>/ui_messages.json files.
|
|
4577
|
+
//
|
|
4578
|
+
// Each task folder contains a ui_messages.json (JSON array, not JSONL). Token
|
|
4579
|
+
// usage records are messages where `say == "api_req_started"`; the `text`
|
|
4580
|
+
// field is a JSON-stringified payload:
|
|
4581
|
+
//
|
|
4582
|
+
// {
|
|
4583
|
+
// "apiProtocol": "openai" | "anthropic" | ...,
|
|
4584
|
+
// "tokensIn": 28673, // request input (already excludes cache)
|
|
4585
|
+
// "tokensOut": 31, // completion
|
|
4586
|
+
// "cacheWrites": 0,
|
|
4587
|
+
// "cacheReads": 5120,
|
|
4588
|
+
// "cost": 0,
|
|
4589
|
+
// "usageMissing": false,
|
|
4590
|
+
// "inferenceProvider": "Moonshot AI" | "minimax" | ...,
|
|
4591
|
+
// }
|
|
4592
|
+
//
|
|
4593
|
+
// We scan every supported VS Code-family install (Cursor, Code, CodeBuddy,
|
|
4594
|
+
// Windsurf, …) under both Library/Application Support (macOS) and Linux/Win
|
|
4595
|
+
// equivalents. Files are small (median ~30KB) and rewritten on each turn — we
|
|
4596
|
+
// can't byte-tail them, so we read the whole file on every sync and dedupe by
|
|
4597
|
+
// (taskId, ts). Per-file mtime caching skips unchanged files.
|
|
4598
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4599
|
+
|
|
4600
|
+
function resolveKilocodeRoots(env = process.env) {
|
|
4601
|
+
if (typeof env.TOKENTRACKER_KILOCODE_ROOTS === "string" && env.TOKENTRACKER_KILOCODE_ROOTS.trim()) {
|
|
4602
|
+
return env.TOKENTRACKER_KILOCODE_ROOTS.split(":")
|
|
4603
|
+
.map((r) => r.trim())
|
|
4604
|
+
.filter(Boolean);
|
|
4605
|
+
}
|
|
4606
|
+
const home = env.HOME || require("node:os").homedir();
|
|
4607
|
+
const candidates = [];
|
|
4608
|
+
if (process.platform === "darwin") {
|
|
4609
|
+
const base = path.join(home, "Library", "Application Support");
|
|
4610
|
+
candidates.push(
|
|
4611
|
+
path.join(base, "Code"),
|
|
4612
|
+
path.join(base, "Code - Insiders"),
|
|
4613
|
+
path.join(base, "Cursor"),
|
|
4614
|
+
path.join(base, "CodeBuddy"),
|
|
4615
|
+
path.join(base, "Windsurf"),
|
|
4616
|
+
path.join(base, "VSCodium"),
|
|
4617
|
+
path.join(base, "Trae"),
|
|
4618
|
+
path.join(base, "Trae CN"),
|
|
4619
|
+
);
|
|
4620
|
+
} else if (process.platform === "win32") {
|
|
4621
|
+
const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
4622
|
+
candidates.push(
|
|
4623
|
+
path.join(appData, "Code"),
|
|
4624
|
+
path.join(appData, "Code - Insiders"),
|
|
4625
|
+
path.join(appData, "Cursor"),
|
|
4626
|
+
path.join(appData, "CodeBuddy"),
|
|
4627
|
+
path.join(appData, "Windsurf"),
|
|
4628
|
+
path.join(appData, "VSCodium"),
|
|
4629
|
+
);
|
|
4630
|
+
} else {
|
|
4631
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
4632
|
+
candidates.push(
|
|
4633
|
+
path.join(xdg, "Code"),
|
|
4634
|
+
path.join(xdg, "Code - Insiders"),
|
|
4635
|
+
path.join(xdg, "Cursor"),
|
|
4636
|
+
path.join(xdg, "CodeBuddy"),
|
|
4637
|
+
path.join(xdg, "Windsurf"),
|
|
4638
|
+
path.join(xdg, "VSCodium"),
|
|
4639
|
+
);
|
|
4640
|
+
}
|
|
4641
|
+
return candidates;
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
function resolveKilocodeTaskFiles(env = process.env) {
|
|
4645
|
+
const roots = resolveKilocodeRoots(env);
|
|
4646
|
+
const out = [];
|
|
4647
|
+
for (const root of roots) {
|
|
4648
|
+
const tasksDir = path.join(root, "User", "globalStorage", "kilocode.kilo-code", "tasks");
|
|
4649
|
+
if (!fssync.existsSync(tasksDir)) continue;
|
|
4650
|
+
let entries;
|
|
4651
|
+
try { entries = fssync.readdirSync(tasksDir); } catch { continue; }
|
|
4652
|
+
for (const taskUuid of entries) {
|
|
4653
|
+
const filePath = path.join(tasksDir, taskUuid, "ui_messages.json");
|
|
4654
|
+
if (!fssync.existsSync(filePath)) continue;
|
|
4655
|
+
out.push({ filePath, taskUuid, ide: path.basename(root) });
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
out.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
4659
|
+
return out;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
// Kilo Code only persists the inference provider (e.g. "minimax",
|
|
4663
|
+
// "Moonshot AI", "Stealth") in ui_messages.json — the actual model id is
|
|
4664
|
+
// stored in workspace state but isn't attributed to individual turns and may
|
|
4665
|
+
// change across sessions, so we cannot map a row back to a model id reliably.
|
|
4666
|
+
// We surface the provider explicitly so the dashboard's Model column doesn't
|
|
4667
|
+
// imply this is a model.
|
|
4668
|
+
function normalizeKilocodeProviderToModel(providerName) {
|
|
4669
|
+
if (typeof providerName !== "string" || !providerName.trim()) return "provider:unknown";
|
|
4670
|
+
const slug = providerName
|
|
4671
|
+
.trim()
|
|
4672
|
+
.toLowerCase()
|
|
4673
|
+
.replace(/\s+/g, "-")
|
|
4674
|
+
.replace(/[^a-z0-9._-]/g, "");
|
|
4675
|
+
// A slug consisting only of separators (dashes/dots/underscores) carries no
|
|
4676
|
+
// information — treat it as unknown.
|
|
4677
|
+
if (!slug || !/[a-z0-9]/.test(slug)) return "provider:unknown";
|
|
4678
|
+
return `provider:${slug}`;
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
async function parseKilocodeIncremental({
|
|
4682
|
+
taskFiles,
|
|
4683
|
+
cursors,
|
|
4684
|
+
queuePath,
|
|
4685
|
+
onProgress,
|
|
4686
|
+
env,
|
|
4687
|
+
} = {}) {
|
|
4688
|
+
await ensureDir(path.dirname(queuePath));
|
|
4689
|
+
const kilocodeState =
|
|
4690
|
+
cursors.kilocode && typeof cursors.kilocode === "object" ? cursors.kilocode : {};
|
|
4691
|
+
const seenIds = new Set(
|
|
4692
|
+
Array.isArray(kilocodeState.seenIds) ? kilocodeState.seenIds : [],
|
|
4693
|
+
);
|
|
4694
|
+
const fileOffsets =
|
|
4695
|
+
kilocodeState.fileOffsets && typeof kilocodeState.fileOffsets === "object"
|
|
4696
|
+
? { ...kilocodeState.fileOffsets }
|
|
4697
|
+
: {};
|
|
4698
|
+
|
|
4699
|
+
const files = Array.isArray(taskFiles)
|
|
4700
|
+
? taskFiles
|
|
4701
|
+
: resolveKilocodeTaskFiles(env || process.env);
|
|
4702
|
+
|
|
4703
|
+
if (files.length === 0) {
|
|
4704
|
+
cursors.kilocode = {
|
|
4705
|
+
...kilocodeState,
|
|
4706
|
+
seenIds: Array.from(seenIds),
|
|
4707
|
+
fileOffsets,
|
|
4708
|
+
updatedAt: new Date().toISOString(),
|
|
4709
|
+
};
|
|
4710
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4714
|
+
const touchedBuckets = new Set();
|
|
4715
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
4716
|
+
let recordsProcessed = 0;
|
|
4717
|
+
let eventsAggregated = 0;
|
|
4718
|
+
|
|
4719
|
+
for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
|
|
4720
|
+
const entry = files[fileIdx];
|
|
4721
|
+
const { filePath, taskUuid } = entry;
|
|
4722
|
+
let stat;
|
|
4723
|
+
try { stat = fssync.statSync(filePath); } catch { continue; }
|
|
4724
|
+
|
|
4725
|
+
const prevEntry = fileOffsets[filePath];
|
|
4726
|
+
if (
|
|
4727
|
+
prevEntry &&
|
|
4728
|
+
Number(prevEntry.size) === stat.size &&
|
|
4729
|
+
Number(prevEntry.mtimeMs) === stat.mtimeMs
|
|
4730
|
+
) {
|
|
4731
|
+
continue;
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
let raw;
|
|
4735
|
+
try { raw = fssync.readFileSync(filePath, "utf8"); } catch { continue; }
|
|
4736
|
+
let data;
|
|
4737
|
+
try { data = JSON.parse(raw); } catch { continue; }
|
|
4738
|
+
if (!Array.isArray(data)) continue;
|
|
4739
|
+
|
|
4740
|
+
for (const msg of data) {
|
|
4741
|
+
if (!msg || typeof msg !== "object") continue;
|
|
4742
|
+
// `api_req_started` is the live billing record; `api_req_deleted` keeps
|
|
4743
|
+
// the same payload when a user removes a turn from the task (Cline-style
|
|
4744
|
+
// edit-and-retry) — tokens were already consumed by the provider, so we
|
|
4745
|
+
// still count them.
|
|
4746
|
+
if (msg.say !== "api_req_started" && msg.say !== "api_req_deleted") continue;
|
|
4747
|
+
if (typeof msg.text !== "string" || !msg.text.startsWith("{")) continue;
|
|
4748
|
+
|
|
4749
|
+
let payload;
|
|
4750
|
+
try { payload = JSON.parse(msg.text); } catch { continue; }
|
|
4751
|
+
if (!payload || typeof payload !== "object") continue;
|
|
4752
|
+
|
|
4753
|
+
const ts = Number(msg.ts);
|
|
4754
|
+
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
4755
|
+
|
|
4756
|
+
const dedupKey = `${taskUuid}:${ts}`;
|
|
4757
|
+
recordsProcessed++;
|
|
4758
|
+
if (seenIds.has(dedupKey)) continue;
|
|
4759
|
+
|
|
4760
|
+
const tokensIn = toNonNegativeInt(payload.tokensIn);
|
|
4761
|
+
const tokensOut = toNonNegativeInt(payload.tokensOut);
|
|
4762
|
+
const cacheReads = toNonNegativeInt(payload.cacheReads);
|
|
4763
|
+
const cacheWrites = toNonNegativeInt(payload.cacheWrites);
|
|
4764
|
+
if (tokensIn === 0 && tokensOut === 0 && cacheReads === 0 && cacheWrites === 0) {
|
|
4765
|
+
seenIds.add(dedupKey);
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
const tsIso = new Date(ts).toISOString();
|
|
4770
|
+
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
4771
|
+
if (!bucketStart) continue;
|
|
4772
|
+
|
|
4773
|
+
const delta = {
|
|
4774
|
+
input_tokens: tokensIn,
|
|
4775
|
+
cached_input_tokens: cacheReads,
|
|
4776
|
+
cache_creation_input_tokens: cacheWrites,
|
|
4777
|
+
output_tokens: tokensOut,
|
|
4778
|
+
reasoning_output_tokens: 0,
|
|
4779
|
+
total_tokens: tokensIn + tokensOut + cacheReads + cacheWrites,
|
|
4780
|
+
conversation_count: 1,
|
|
4781
|
+
};
|
|
4782
|
+
|
|
4783
|
+
const model = normalizeKilocodeProviderToModel(payload.inferenceProvider);
|
|
4784
|
+
const bucket = getHourlyBucket(hourlyState, "kilo-code", model, bucketStart);
|
|
4785
|
+
addTotals(bucket.totals, delta);
|
|
4786
|
+
touchedBuckets.add(bucketKey("kilo-code", model, bucketStart));
|
|
4787
|
+
seenIds.add(dedupKey);
|
|
4788
|
+
eventsAggregated++;
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
fileOffsets[filePath] = { size: stat.size, mtimeMs: stat.mtimeMs, ino: stat.ino };
|
|
4792
|
+
|
|
4793
|
+
if (cb) {
|
|
4794
|
+
cb({
|
|
4795
|
+
index: fileIdx + 1,
|
|
4796
|
+
total: files.length,
|
|
4797
|
+
recordsProcessed,
|
|
4798
|
+
eventsAggregated,
|
|
4799
|
+
bucketsQueued: touchedBuckets.size,
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
// Cap seenIds to last 50k to bound cursor state size
|
|
4805
|
+
const seenArr = Array.from(seenIds);
|
|
4806
|
+
const cappedSeen = seenArr.length > 50_000 ? seenArr.slice(seenArr.length - 50_000) : seenArr;
|
|
4807
|
+
|
|
4808
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
4809
|
+
const updatedAt = new Date().toISOString();
|
|
4810
|
+
hourlyState.updatedAt = updatedAt;
|
|
4811
|
+
cursors.hourly = hourlyState;
|
|
4812
|
+
cursors.kilocode = { ...kilocodeState, seenIds: cappedSeen, fileOffsets, updatedAt };
|
|
4813
|
+
|
|
4814
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4572
4817
|
async function parseOmpIncremental({
|
|
4573
4818
|
sessionFiles,
|
|
4574
4819
|
cursors,
|
|
@@ -5542,6 +5787,10 @@ module.exports = {
|
|
|
5542
5787
|
resolveOmpSessionFiles,
|
|
5543
5788
|
resolveOmpDefaultModel,
|
|
5544
5789
|
parseOmpIncremental,
|
|
5790
|
+
resolveKilocodeRoots,
|
|
5791
|
+
resolveKilocodeTaskFiles,
|
|
5792
|
+
normalizeKilocodeProviderToModel,
|
|
5793
|
+
parseKilocodeIncremental,
|
|
5545
5794
|
resolvePiHome,
|
|
5546
5795
|
resolvePiAgentDir,
|
|
5547
5796
|
resolvePiSessionFiles,
|