tokentracker-cli 0.5.91 → 0.5.93
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 +7 -6
- package/dashboard/dist/assets/{main-CHSJgtKj.js → main-BvlEZHQ6.js} +179 -178
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +3 -1
- package/src/commands/init.js +41 -2
- package/src/commands/serve.js +11 -0
- package/src/commands/status.js +31 -2
- package/src/commands/sync.js +28 -0
- package/src/commands/uninstall.js +18 -1
- package/src/lib/claude-config.js +16 -2
- package/src/lib/local-api.js +11 -116
- package/src/lib/pricing/curated-overrides.json +33 -0
- package/src/lib/pricing/index.js +135 -0
- package/src/lib/pricing/litellm-fetcher.js +172 -0
- package/src/lib/pricing/matcher.js +149 -0
- package/src/lib/pricing/seed-snapshot.json +1 -0
- package/src/lib/rollout.js +284 -0
- package/src/lib/tracker-paths.js +1 -0
package/src/lib/rollout.js
CHANGED
|
@@ -4073,6 +4073,286 @@ async function parseKimiIncremental({ wireFiles, cursors, queuePath, onProgress,
|
|
|
4073
4073
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4074
4074
|
}
|
|
4075
4075
|
|
|
4076
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4077
|
+
// CodeBuddy CLI — passive JSONL reader (~/.codebuddy/projects/<cwd>/<sid>.jsonl)
|
|
4078
|
+
//
|
|
4079
|
+
// Tencent's CodeBuddy CLI is structurally cloned from Claude Code:
|
|
4080
|
+
// ~/.codebuddy/projects/<encoded-cwd>/<sessionId>.jsonl — conversation log
|
|
4081
|
+
// ~/.codebuddy/sessions/<pid>.json — session metadata
|
|
4082
|
+
// ~/.codebuddy/settings.json — `{"model": "..."}`
|
|
4083
|
+
//
|
|
4084
|
+
// CodeBuddy ships NO hook system — we incrementally tail the JSONL files on
|
|
4085
|
+
// each sync (passive scan only, same shape as Kimi's wire.jsonl reader).
|
|
4086
|
+
//
|
|
4087
|
+
// Per-line record types: message, reasoning, topic, file-history-snapshot.
|
|
4088
|
+
// Only `type=="message" && role=="assistant"` carry token usage. The shape:
|
|
4089
|
+
//
|
|
4090
|
+
// providerData.rawUsage = {
|
|
4091
|
+
// prompt_tokens: 22223, // OpenAI-style — INCLUDES cached
|
|
4092
|
+
// completion_tokens: 250,
|
|
4093
|
+
// prompt_tokens_details: { cached_tokens: 512, reasoning_tokens?: number },
|
|
4094
|
+
// cache_read_input_tokens: 0, // Anthropic-style mirror (often 0)
|
|
4095
|
+
// cache_creation_input_tokens: 0,
|
|
4096
|
+
// }
|
|
4097
|
+
//
|
|
4098
|
+
// Token math (matches the repo's queue convention; do NOT pass prompt_tokens
|
|
4099
|
+
// through unchanged — that double-counts cached input):
|
|
4100
|
+
// input_tokens = prompt_tokens - prompt_tokens_details.cached_tokens
|
|
4101
|
+
// cached_input_tokens = prompt_tokens_details.cached_tokens
|
|
4102
|
+
// cache_creation_input_tokens = cache_creation_input_tokens (often 0)
|
|
4103
|
+
// output_tokens = completion_tokens
|
|
4104
|
+
// reasoning_output_tokens = prompt_tokens_details.reasoning_tokens || 0
|
|
4105
|
+
// total_tokens = sum of the above
|
|
4106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4107
|
+
|
|
4108
|
+
function resolveCodebuddyHome(env = process.env) {
|
|
4109
|
+
const home = env.HOME || require("node:os").homedir();
|
|
4110
|
+
return env.CODEBUDDY_HOME || path.join(home, ".codebuddy");
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
function resolveCodebuddyDefaultModel(env = process.env) {
|
|
4114
|
+
const fallback = "codebuddy-unknown";
|
|
4115
|
+
try {
|
|
4116
|
+
const settingsPath = path.join(resolveCodebuddyHome(env), "settings.json");
|
|
4117
|
+
const raw = fssync.readFileSync(settingsPath, "utf8");
|
|
4118
|
+
const parsed = JSON.parse(raw);
|
|
4119
|
+
if (parsed && typeof parsed === "object" && typeof parsed.model === "string" && parsed.model.trim()) {
|
|
4120
|
+
return parsed.model.trim();
|
|
4121
|
+
}
|
|
4122
|
+
} catch (_e) {
|
|
4123
|
+
// settings missing or malformed — fall through
|
|
4124
|
+
}
|
|
4125
|
+
return fallback;
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
function resolveCodebuddyProjectFiles(env = process.env) {
|
|
4129
|
+
const projectsDir = path.join(resolveCodebuddyHome(env), "projects");
|
|
4130
|
+
if (!fssync.existsSync(projectsDir)) return [];
|
|
4131
|
+
const files = [];
|
|
4132
|
+
try {
|
|
4133
|
+
for (const cwd of fssync.readdirSync(projectsDir)) {
|
|
4134
|
+
const cwdDir = path.join(projectsDir, cwd);
|
|
4135
|
+
let stat;
|
|
4136
|
+
try { stat = fssync.statSync(cwdDir); } catch { continue; }
|
|
4137
|
+
if (!stat.isDirectory()) continue;
|
|
4138
|
+
let entries;
|
|
4139
|
+
try { entries = fssync.readdirSync(cwdDir); } catch { continue; }
|
|
4140
|
+
for (const entry of entries) {
|
|
4141
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
4142
|
+
files.push(path.join(cwdDir, entry));
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
} catch {
|
|
4146
|
+
// ignore — return what we have
|
|
4147
|
+
}
|
|
4148
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
4149
|
+
return files;
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
async function parseCodebuddyIncremental({
|
|
4153
|
+
projectFiles,
|
|
4154
|
+
cursors,
|
|
4155
|
+
queuePath,
|
|
4156
|
+
onProgress,
|
|
4157
|
+
env,
|
|
4158
|
+
defaultModel,
|
|
4159
|
+
} = {}) {
|
|
4160
|
+
await ensureDir(path.dirname(queuePath));
|
|
4161
|
+
const codebuddyState =
|
|
4162
|
+
cursors.codebuddy && typeof cursors.codebuddy === "object" ? cursors.codebuddy : {};
|
|
4163
|
+
const seenIds = new Set(
|
|
4164
|
+
Array.isArray(codebuddyState.seenIds) ? codebuddyState.seenIds : [],
|
|
4165
|
+
);
|
|
4166
|
+
const fileOffsets =
|
|
4167
|
+
codebuddyState.fileOffsets && typeof codebuddyState.fileOffsets === "object"
|
|
4168
|
+
? { ...codebuddyState.fileOffsets }
|
|
4169
|
+
: {};
|
|
4170
|
+
|
|
4171
|
+
const files = Array.isArray(projectFiles)
|
|
4172
|
+
? projectFiles
|
|
4173
|
+
: resolveCodebuddyProjectFiles(env || process.env);
|
|
4174
|
+
const fallbackModel = defaultModel || resolveCodebuddyDefaultModel(env || process.env);
|
|
4175
|
+
|
|
4176
|
+
if (files.length === 0) {
|
|
4177
|
+
cursors.codebuddy = {
|
|
4178
|
+
...codebuddyState,
|
|
4179
|
+
seenIds: Array.from(seenIds),
|
|
4180
|
+
fileOffsets,
|
|
4181
|
+
updatedAt: new Date().toISOString(),
|
|
4182
|
+
};
|
|
4183
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4187
|
+
const touchedBuckets = new Set();
|
|
4188
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
4189
|
+
let recordsProcessed = 0;
|
|
4190
|
+
let eventsAggregated = 0;
|
|
4191
|
+
|
|
4192
|
+
for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
|
|
4193
|
+
const filePath = files[fileIdx];
|
|
4194
|
+
let stat;
|
|
4195
|
+
try { stat = fssync.statSync(filePath); } catch { continue; }
|
|
4196
|
+
|
|
4197
|
+
const prevEntry = fileOffsets[filePath] || {};
|
|
4198
|
+
const prevSize = Number(prevEntry.size) || 0;
|
|
4199
|
+
const prevIno = prevEntry.ino;
|
|
4200
|
+
// Re-read from start if file shrunk (truncate/rewrite) or inode changed
|
|
4201
|
+
// (file deleted + recreated). Otherwise pick up after the last read offset.
|
|
4202
|
+
const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
|
|
4203
|
+
const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
|
|
4204
|
+
if (stat.size <= startOffset) continue;
|
|
4205
|
+
|
|
4206
|
+
let stream;
|
|
4207
|
+
try {
|
|
4208
|
+
stream = fssync.createReadStream(filePath, {
|
|
4209
|
+
encoding: "utf8",
|
|
4210
|
+
start: startOffset,
|
|
4211
|
+
});
|
|
4212
|
+
} catch { continue; }
|
|
4213
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4214
|
+
|
|
4215
|
+
for await (const line of rl) {
|
|
4216
|
+
if (!line || !line.trim()) continue;
|
|
4217
|
+
let entry;
|
|
4218
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
4219
|
+
|
|
4220
|
+
// Only assistant message events carry token usage.
|
|
4221
|
+
if (!entry || entry.type !== "message" || entry.role !== "assistant") continue;
|
|
4222
|
+
|
|
4223
|
+
const provider = entry.providerData;
|
|
4224
|
+
const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
|
|
4225
|
+
if (!rawUsage || typeof rawUsage !== "object") continue;
|
|
4226
|
+
|
|
4227
|
+
// Dedup per-message — message id (uuid) is most stable, then session +
|
|
4228
|
+
// timestamp as fallback.
|
|
4229
|
+
const sessionId =
|
|
4230
|
+
typeof entry.sessionId === "string" && entry.sessionId
|
|
4231
|
+
? entry.sessionId
|
|
4232
|
+
: path.basename(filePath, ".jsonl");
|
|
4233
|
+
const tsMs =
|
|
4234
|
+
Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
|
|
4235
|
+
? Number(entry.timestamp)
|
|
4236
|
+
: null;
|
|
4237
|
+
const messageId =
|
|
4238
|
+
typeof entry.uuid === "string" && entry.uuid
|
|
4239
|
+
? entry.uuid
|
|
4240
|
+
: typeof entry.id === "string" && entry.id
|
|
4241
|
+
? entry.id
|
|
4242
|
+
: tsMs != null
|
|
4243
|
+
? `${sessionId}:${tsMs}`
|
|
4244
|
+
: null;
|
|
4245
|
+
if (!messageId) continue;
|
|
4246
|
+
if (seenIds.has(messageId)) continue;
|
|
4247
|
+
|
|
4248
|
+
recordsProcessed++;
|
|
4249
|
+
|
|
4250
|
+
const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
|
|
4251
|
+
const completionTokens = toNonNegativeInt(rawUsage.completion_tokens);
|
|
4252
|
+
const details =
|
|
4253
|
+
rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
|
|
4254
|
+
? rawUsage.prompt_tokens_details
|
|
4255
|
+
: {};
|
|
4256
|
+
const cachedTokens = toNonNegativeInt(details.cached_tokens);
|
|
4257
|
+
// Anthropic-style mirror; CodeBuddy emits these too even if usually 0.
|
|
4258
|
+
const cacheReadAlt = toNonNegativeInt(rawUsage.cache_read_input_tokens);
|
|
4259
|
+
const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
|
|
4260
|
+
const reasoningTokens = toNonNegativeInt(details.reasoning_tokens);
|
|
4261
|
+
|
|
4262
|
+
// CRITICAL: prompt_tokens is OpenAI-style and INCLUDES cached.
|
|
4263
|
+
// Subtract cached so input_tokens is pure non-cached input — matches
|
|
4264
|
+
// the repo's normalization convention (see CLAUDE.md "Token
|
|
4265
|
+
// Normalization Convention"). cache_read takes the larger of the two
|
|
4266
|
+
// mirrored fields (rawUsage.cache_read_input_tokens vs
|
|
4267
|
+
// prompt_tokens_details.cached_tokens) since CodeBuddy populates one
|
|
4268
|
+
// or the other depending on upstream provider.
|
|
4269
|
+
const cacheRead = Math.max(cachedTokens, cacheReadAlt);
|
|
4270
|
+
const inputTokens = Math.max(0, promptTokens - cacheRead);
|
|
4271
|
+
|
|
4272
|
+
if (
|
|
4273
|
+
inputTokens === 0 &&
|
|
4274
|
+
completionTokens === 0 &&
|
|
4275
|
+
cacheRead === 0 &&
|
|
4276
|
+
cacheCreation === 0
|
|
4277
|
+
) {
|
|
4278
|
+
seenIds.add(messageId);
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
if (tsMs == null) {
|
|
4283
|
+
seenIds.add(messageId);
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
const tsIso = new Date(tsMs).toISOString();
|
|
4287
|
+
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
4288
|
+
if (!bucketStart) continue;
|
|
4289
|
+
|
|
4290
|
+
const model =
|
|
4291
|
+
normalizeModelInput(provider?.model) ||
|
|
4292
|
+
normalizeModelInput(entry.model) ||
|
|
4293
|
+
fallbackModel;
|
|
4294
|
+
|
|
4295
|
+
const delta = {
|
|
4296
|
+
input_tokens: inputTokens,
|
|
4297
|
+
cached_input_tokens: cacheRead,
|
|
4298
|
+
cache_creation_input_tokens: cacheCreation,
|
|
4299
|
+
output_tokens: completionTokens,
|
|
4300
|
+
reasoning_output_tokens: reasoningTokens,
|
|
4301
|
+
total_tokens:
|
|
4302
|
+
inputTokens + completionTokens + cacheRead + cacheCreation + reasoningTokens,
|
|
4303
|
+
conversation_count: 1,
|
|
4304
|
+
};
|
|
4305
|
+
|
|
4306
|
+
const bucket = getHourlyBucket(hourlyState, "codebuddy", model, bucketStart);
|
|
4307
|
+
addTotals(bucket.totals, delta);
|
|
4308
|
+
touchedBuckets.add(bucketKey("codebuddy", model, bucketStart));
|
|
4309
|
+
seenIds.add(messageId);
|
|
4310
|
+
eventsAggregated++;
|
|
4311
|
+
|
|
4312
|
+
if (cb) {
|
|
4313
|
+
cb({
|
|
4314
|
+
index: fileIdx + 1,
|
|
4315
|
+
total: files.length,
|
|
4316
|
+
recordsProcessed,
|
|
4317
|
+
eventsAggregated,
|
|
4318
|
+
bucketsQueued: touchedBuckets.size,
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
let postStat = stat;
|
|
4324
|
+
try { postStat = fssync.statSync(filePath); } catch {}
|
|
4325
|
+
fileOffsets[filePath] = {
|
|
4326
|
+
size: postStat.size,
|
|
4327
|
+
mtimeMs: postStat.mtimeMs,
|
|
4328
|
+
ino: postStat.ino,
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
// Cap dedup set to last 10k IDs to bound cursor state size — same convention
|
|
4333
|
+
// as Kimi/Copilot so cursors.json doesn't grow unbounded.
|
|
4334
|
+
const seenArr = Array.from(seenIds);
|
|
4335
|
+
const cappedSeen =
|
|
4336
|
+
seenArr.length > 10_000 ? seenArr.slice(seenArr.length - 10_000) : seenArr;
|
|
4337
|
+
|
|
4338
|
+
const bucketsQueued = await enqueueTouchedBuckets({
|
|
4339
|
+
queuePath,
|
|
4340
|
+
hourlyState,
|
|
4341
|
+
touchedBuckets,
|
|
4342
|
+
});
|
|
4343
|
+
const updatedAt = new Date().toISOString();
|
|
4344
|
+
hourlyState.updatedAt = updatedAt;
|
|
4345
|
+
cursors.hourly = hourlyState;
|
|
4346
|
+
cursors.codebuddy = {
|
|
4347
|
+
...codebuddyState,
|
|
4348
|
+
seenIds: cappedSeen,
|
|
4349
|
+
fileOffsets,
|
|
4350
|
+
updatedAt,
|
|
4351
|
+
};
|
|
4352
|
+
|
|
4353
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4076
4356
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
4077
4357
|
// GitHub Copilot CLI — OpenTelemetry JSONL exporter
|
|
4078
4358
|
// User must opt in by setting:
|
|
@@ -4286,6 +4566,10 @@ module.exports = {
|
|
|
4286
4566
|
resolveKimiWireFiles,
|
|
4287
4567
|
resolveKimiDefaultModel,
|
|
4288
4568
|
parseKimiIncremental,
|
|
4569
|
+
resolveCodebuddyHome,
|
|
4570
|
+
resolveCodebuddyProjectFiles,
|
|
4571
|
+
resolveCodebuddyDefaultModel,
|
|
4572
|
+
parseCodebuddyIncremental,
|
|
4289
4573
|
resolveKiroCliSessionFiles,
|
|
4290
4574
|
resolveKiroCliDbPath,
|
|
4291
4575
|
parseKiroCliIncremental,
|