ccclub 0.3.2 → 0.3.3
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/dist/index.js +879 -104
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command, Option } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import { createInterface } from "readline/promises";
|
|
7
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
8
8
|
import { stdin, stdout } from "process";
|
|
9
9
|
import chalk4 from "chalk";
|
|
10
10
|
import ora2 from "ora";
|
|
@@ -17,11 +17,31 @@ import { existsSync } from "fs";
|
|
|
17
17
|
import { randomBytes } from "crypto";
|
|
18
18
|
import { execSync } from "child_process";
|
|
19
19
|
|
|
20
|
+
// ../shared/dist/types.js
|
|
21
|
+
var AGENT_SOURCES = ["claude", "codex", "opencode", "amp", "pi"];
|
|
22
|
+
var AGENT_LABELS = {
|
|
23
|
+
claude: "Claude",
|
|
24
|
+
codex: "Codex",
|
|
25
|
+
opencode: "OpenCode",
|
|
26
|
+
amp: "Amp",
|
|
27
|
+
pi: "pi-agent"
|
|
28
|
+
};
|
|
29
|
+
|
|
20
30
|
// ../shared/dist/constants.js
|
|
21
31
|
var BLOCK_DURATION_MIN = 30;
|
|
22
32
|
var BLOCK_DURATION_MS = BLOCK_DURATION_MIN * 60 * 1e3;
|
|
23
33
|
var DEFAULT_API_URL = "https://ccclub.dev";
|
|
24
34
|
var CLAUDE_PROJECTS_DIR = ".claude/projects";
|
|
35
|
+
var CLAUDE_CONFIG_PROJECTS_DIR = ".config/claude/projects";
|
|
36
|
+
var CLAUDE_CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
|
|
37
|
+
var CODEX_HOME_ENV = "CODEX_HOME";
|
|
38
|
+
var OPENCODE_DATA_DIR_ENV = "OPENCODE_DATA_DIR";
|
|
39
|
+
var AMP_DATA_DIR_ENV = "AMP_DATA_DIR";
|
|
40
|
+
var PI_AGENT_DIR_ENV = "PI_AGENT_DIR";
|
|
41
|
+
var DEFAULT_CODEX_DIR = ".codex";
|
|
42
|
+
var DEFAULT_OPENCODE_DIR = ".local/share/opencode";
|
|
43
|
+
var DEFAULT_AMP_DIR = ".local/share/amp";
|
|
44
|
+
var DEFAULT_PI_AGENT_SESSIONS_DIR = ".pi/agent/sessions";
|
|
25
45
|
var CCCLUB_CONFIG_DIR = ".ccclub";
|
|
26
46
|
var PLAN_PRICES = {
|
|
27
47
|
pro: 20,
|
|
@@ -47,12 +67,25 @@ var MODEL_PRICING = {
|
|
|
47
67
|
"claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
|
|
48
68
|
// Haiku
|
|
49
69
|
"claude-haiku-4-5-20251001": { input: 1, output: 5, cacheCreation: 1.25, cacheRead: 0.1 },
|
|
50
|
-
"claude-3-5-haiku-20241022": { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 }
|
|
70
|
+
"claude-3-5-haiku-20241022": { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 },
|
|
71
|
+
// OpenAI GPT family fallbacks. Many agent logs provide exact costs; these are best-effort
|
|
72
|
+
// estimates for sources that only expose tokens.
|
|
73
|
+
"gpt-5": { input: 1.25, output: 10, cacheCreation: 0, cacheRead: 0.125 },
|
|
74
|
+
"gpt-5-mini": { input: 0.25, output: 2, cacheCreation: 0, cacheRead: 0.025 },
|
|
75
|
+
"gpt-5-nano": { input: 0.05, output: 0.4, cacheCreation: 0, cacheRead: 5e-3 }
|
|
51
76
|
};
|
|
52
77
|
var FAMILY_FALLBACK = {
|
|
53
78
|
opus: MODEL_PRICING["claude-opus-4-6"],
|
|
54
79
|
sonnet: MODEL_PRICING["claude-sonnet-4-5-20250929"],
|
|
55
|
-
haiku: MODEL_PRICING["claude-haiku-4-5-20251001"]
|
|
80
|
+
haiku: MODEL_PRICING["claude-haiku-4-5-20251001"],
|
|
81
|
+
"gpt-5-nano": MODEL_PRICING["gpt-5-nano"],
|
|
82
|
+
"gpt-5-mini": MODEL_PRICING["gpt-5-mini"],
|
|
83
|
+
"gpt-5": MODEL_PRICING["gpt-5"],
|
|
84
|
+
gpt: MODEL_PRICING["gpt-5"],
|
|
85
|
+
o3: MODEL_PRICING["gpt-5"],
|
|
86
|
+
o4: MODEL_PRICING["gpt-5"],
|
|
87
|
+
gemini: { input: 1.25, output: 10, cacheCreation: 0, cacheRead: 0.125 },
|
|
88
|
+
deepseek: { input: 0.27, output: 1.1, cacheCreation: 0, cacheRead: 0.07 }
|
|
56
89
|
};
|
|
57
90
|
function getPricing(model) {
|
|
58
91
|
if (MODEL_PRICING[model])
|
|
@@ -64,9 +97,9 @@ function getPricing(model) {
|
|
|
64
97
|
}
|
|
65
98
|
return FAMILY_FALLBACK.sonnet;
|
|
66
99
|
}
|
|
67
|
-
function calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
100
|
+
function calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, reasoningTokens = 0) {
|
|
68
101
|
const pricing = getPricing(model);
|
|
69
|
-
return (inputTokens * pricing.input + outputTokens * pricing.output + cacheCreationTokens * pricing.cacheCreation + cacheReadTokens * pricing.cacheRead) / 1e6;
|
|
102
|
+
return (inputTokens * pricing.input + (outputTokens + reasoningTokens) * pricing.output + cacheCreationTokens * pricing.cacheCreation + cacheReadTokens * pricing.cacheRead) / 1e6;
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
// src/config.ts
|
|
@@ -204,81 +237,721 @@ function isHookInstalled() {
|
|
|
204
237
|
}
|
|
205
238
|
}
|
|
206
239
|
|
|
240
|
+
// src/heartbeat.ts
|
|
241
|
+
import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
242
|
+
import { join as join3 } from "path";
|
|
243
|
+
import { homedir as homedir3 } from "os";
|
|
244
|
+
import { existsSync as existsSync3 } from "fs";
|
|
245
|
+
import { execFile } from "child_process";
|
|
246
|
+
var PLIST_NAME = "dev.ccclub.sync";
|
|
247
|
+
var LAUNCH_AGENTS_DIR = join3(homedir3(), "Library", "LaunchAgents");
|
|
248
|
+
var PLIST_PATH = join3(LAUNCH_AGENTS_DIR, `${PLIST_NAME}.plist`);
|
|
249
|
+
function getPlist() {
|
|
250
|
+
const logPath = join3(homedir3(), ".ccclub", "sync.log");
|
|
251
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
252
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
253
|
+
<plist version="1.0">
|
|
254
|
+
<dict>
|
|
255
|
+
<key>Label</key>
|
|
256
|
+
<string>${PLIST_NAME}</string>
|
|
257
|
+
<key>ProgramArguments</key>
|
|
258
|
+
<array>
|
|
259
|
+
<string>/usr/bin/env</string>
|
|
260
|
+
<string>npx</string>
|
|
261
|
+
<string>ccclub</string>
|
|
262
|
+
<string>sync</string>
|
|
263
|
+
<string>--silent</string>
|
|
264
|
+
</array>
|
|
265
|
+
<key>StartInterval</key>
|
|
266
|
+
<integer>300</integer>
|
|
267
|
+
<key>StandardOutPath</key>
|
|
268
|
+
<string>${logPath}</string>
|
|
269
|
+
<key>StandardErrorPath</key>
|
|
270
|
+
<string>${logPath}</string>
|
|
271
|
+
<key>RunAtLoad</key>
|
|
272
|
+
<true/>
|
|
273
|
+
<key>EnvironmentVariables</key>
|
|
274
|
+
<dict>
|
|
275
|
+
<key>PATH</key>
|
|
276
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
277
|
+
</dict>
|
|
278
|
+
</dict>
|
|
279
|
+
</plist>`;
|
|
280
|
+
}
|
|
281
|
+
async function installHeartbeat() {
|
|
282
|
+
if (process.platform !== "darwin") {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (existsSync3(PLIST_PATH)) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (!existsSync3(LAUNCH_AGENTS_DIR)) {
|
|
289
|
+
await mkdir3(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
await writeFile3(PLIST_PATH, getPlist());
|
|
292
|
+
try {
|
|
293
|
+
await new Promise((resolve2, reject) => {
|
|
294
|
+
execFile("launchctl", ["load", PLIST_PATH], (err) => err ? reject(err) : resolve2());
|
|
295
|
+
});
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
function isHeartbeatInstalled() {
|
|
301
|
+
return existsSync3(PLIST_PATH);
|
|
302
|
+
}
|
|
303
|
+
|
|
207
304
|
// src/commands/sync.ts
|
|
208
|
-
import { readFile as readFile4, writeFile as
|
|
209
|
-
import { existsSync as
|
|
210
|
-
import { join as
|
|
211
|
-
import { homedir as
|
|
305
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
306
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
307
|
+
import { join as join11 } from "path";
|
|
308
|
+
import { homedir as homedir11 } from "os";
|
|
212
309
|
import chalk2 from "chalk";
|
|
213
310
|
import ora from "ora";
|
|
214
311
|
|
|
215
|
-
// src/
|
|
216
|
-
import {
|
|
217
|
-
import {
|
|
218
|
-
|
|
312
|
+
// src/sources/amp.ts
|
|
313
|
+
import { join as join5 } from "path";
|
|
314
|
+
import { homedir as homedir5 } from "os";
|
|
315
|
+
|
|
316
|
+
// src/sources/shared.ts
|
|
317
|
+
import { createReadStream } from "fs";
|
|
318
|
+
import { stat, readFile as readFile3 } from "fs/promises";
|
|
319
|
+
import { homedir as homedir4 } from "os";
|
|
320
|
+
import { resolve, join as join4 } from "path";
|
|
321
|
+
import { createInterface } from "readline";
|
|
219
322
|
import { glob } from "glob";
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
323
|
+
function resolveHomePath(path) {
|
|
324
|
+
if (path === "~") return homedir4();
|
|
325
|
+
if (path.startsWith("~/")) return join4(homedir4(), path.slice(2));
|
|
326
|
+
return path;
|
|
327
|
+
}
|
|
328
|
+
function parsePathList(value, fallback) {
|
|
329
|
+
const raw = value?.trim() ? value.split(",").map((p) => p.trim()).filter(Boolean) : fallback;
|
|
330
|
+
return Array.from(new Set(raw.map((p) => resolve(resolveHomePath(p)))));
|
|
331
|
+
}
|
|
332
|
+
async function existingDirectories(paths) {
|
|
333
|
+
const results = await Promise.all(
|
|
334
|
+
paths.map(async (path) => {
|
|
335
|
+
try {
|
|
336
|
+
return (await stat(path)).isDirectory() ? path : null;
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
return results.filter((path) => path !== null);
|
|
343
|
+
}
|
|
344
|
+
async function globFiles(directories, pattern) {
|
|
345
|
+
const groups = await Promise.all(
|
|
346
|
+
directories.map((cwd) => glob(pattern, { cwd, absolute: true }).catch(() => []))
|
|
347
|
+
);
|
|
348
|
+
return groups.flat().sort();
|
|
349
|
+
}
|
|
350
|
+
async function readJsonFile(file) {
|
|
351
|
+
try {
|
|
352
|
+
return JSON.parse(await readFile3(file, "utf-8"));
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function readJsonlFile(file, onValue) {
|
|
358
|
+
const stream = createReadStream(file, { encoding: "utf-8" });
|
|
359
|
+
const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
360
|
+
for await (const line of rl) {
|
|
361
|
+
const trimmed = line.trim();
|
|
362
|
+
if (!trimmed) continue;
|
|
363
|
+
try {
|
|
364
|
+
await onValue(JSON.parse(trimmed), trimmed);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function asRecord(value) {
|
|
370
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
371
|
+
}
|
|
372
|
+
function asNumber(value) {
|
|
373
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
374
|
+
}
|
|
375
|
+
function asString(value) {
|
|
376
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
377
|
+
}
|
|
378
|
+
function toIsoTimestamp(value) {
|
|
379
|
+
if (typeof value === "string") {
|
|
380
|
+
const date = new Date(value);
|
|
381
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
382
|
+
}
|
|
383
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
384
|
+
const ms = value < 1e10 ? value * 1e3 : value;
|
|
385
|
+
const date = new Date(ms);
|
|
386
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/sources/amp.ts
|
|
392
|
+
function getAmpDirs() {
|
|
393
|
+
const dirs = parsePathList(process.env[AMP_DATA_DIR_ENV], [join5(homedir5(), DEFAULT_AMP_DIR)]);
|
|
394
|
+
return existingDirectories(dirs);
|
|
395
|
+
}
|
|
396
|
+
function getAmpCacheTokens(messages, toMessageId) {
|
|
397
|
+
if (!Array.isArray(messages)) return { cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
398
|
+
for (const message of messages) {
|
|
399
|
+
const record = asRecord(message);
|
|
400
|
+
if (record?.role !== "assistant" || asNumber(record.messageId) !== toMessageId) continue;
|
|
401
|
+
const usage = asRecord(record.usage);
|
|
402
|
+
return {
|
|
403
|
+
cacheCreationTokens: asNumber(usage?.cacheCreationInputTokens),
|
|
404
|
+
cacheReadTokens: asNumber(usage?.cacheReadInputTokens)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return { cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
408
|
+
}
|
|
409
|
+
function parseAmpThread(data) {
|
|
410
|
+
const source = "amp";
|
|
411
|
+
const thread = asRecord(data);
|
|
412
|
+
const threadId = asString(thread?.id) ?? "unknown";
|
|
413
|
+
const usageLedger = asRecord(thread?.usageLedger);
|
|
414
|
+
const events = usageLedger?.events;
|
|
415
|
+
if (!Array.isArray(events)) return [];
|
|
416
|
+
const entries = [];
|
|
417
|
+
for (const rawEvent of events) {
|
|
418
|
+
const event = asRecord(rawEvent);
|
|
419
|
+
if (event == null) continue;
|
|
420
|
+
const timestamp = toIsoTimestamp(event.timestamp);
|
|
421
|
+
const model = asString(event.model);
|
|
422
|
+
const tokens = asRecord(event.tokens);
|
|
423
|
+
if (timestamp == null || model == null || tokens == null) continue;
|
|
424
|
+
const inputTokens = asNumber(tokens.input);
|
|
425
|
+
const outputTokens = asNumber(tokens.output);
|
|
426
|
+
const toMessageId = asNumber(event.toMessageId);
|
|
427
|
+
const { cacheCreationTokens, cacheReadTokens } = getAmpCacheTokens(thread?.messages, toMessageId);
|
|
428
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheCreationTokens === 0 && cacheReadTokens === 0) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const requestId = [
|
|
432
|
+
source,
|
|
433
|
+
threadId,
|
|
434
|
+
timestamp,
|
|
435
|
+
model,
|
|
436
|
+
inputTokens,
|
|
437
|
+
outputTokens,
|
|
438
|
+
cacheCreationTokens,
|
|
439
|
+
cacheReadTokens,
|
|
440
|
+
toMessageId
|
|
441
|
+
].join(":");
|
|
442
|
+
entries.push({
|
|
443
|
+
source,
|
|
444
|
+
timestamp,
|
|
445
|
+
sessionId: threadId,
|
|
446
|
+
requestId,
|
|
447
|
+
model,
|
|
448
|
+
inputTokens,
|
|
449
|
+
outputTokens,
|
|
450
|
+
cacheCreationTokens,
|
|
451
|
+
cacheReadTokens,
|
|
452
|
+
totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
|
|
453
|
+
costUSD: calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens)
|
|
454
|
+
});
|
|
225
455
|
}
|
|
456
|
+
return entries;
|
|
457
|
+
}
|
|
458
|
+
async function collectAmpUsage() {
|
|
459
|
+
const source = "amp";
|
|
460
|
+
const dirs = await getAmpDirs();
|
|
461
|
+
const files = await globFiles(dirs.map((dir) => join5(dir, "threads")), "**/*.json");
|
|
226
462
|
const entries = [];
|
|
227
|
-
const
|
|
463
|
+
const turns = [];
|
|
228
464
|
const seen = /* @__PURE__ */ new Set();
|
|
229
|
-
const seenHuman = /* @__PURE__ */ new Set();
|
|
230
465
|
for (const file of files) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
466
|
+
for (const entry of parseAmpThread(await readJsonFile(file))) {
|
|
467
|
+
const key = entry.requestId ?? `${source}:${entry.sessionId}:${entry.timestamp}`;
|
|
468
|
+
if (seen.has(key)) continue;
|
|
469
|
+
seen.add(key);
|
|
470
|
+
entries.push(entry);
|
|
471
|
+
turns.push({ source, timestamp: entry.timestamp, key });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return { source, entries, turns, files: files.length, warnings: [] };
|
|
475
|
+
}
|
|
476
|
+
var ampCollector = {
|
|
477
|
+
source: "amp",
|
|
478
|
+
label: "Amp",
|
|
479
|
+
collect: collectAmpUsage
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// src/sources/claude.ts
|
|
483
|
+
import { join as join6, basename } from "path";
|
|
484
|
+
import { homedir as homedir6 } from "os";
|
|
485
|
+
function isProjectsPath(path) {
|
|
486
|
+
return basename(path) === "projects";
|
|
487
|
+
}
|
|
488
|
+
async function getClaudeProjectDirs() {
|
|
489
|
+
const envPaths = process.env[CLAUDE_CONFIG_DIR_ENV];
|
|
490
|
+
const basePaths = parsePathList(
|
|
491
|
+
envPaths,
|
|
492
|
+
[join6(homedir6(), CLAUDE_CONFIG_PROJECTS_DIR), join6(homedir6(), CLAUDE_PROJECTS_DIR)]
|
|
493
|
+
);
|
|
494
|
+
const candidates = envPaths?.trim() ? basePaths.flatMap((path) => isProjectsPath(path) ? [path] : [join6(path, "projects"), path]) : basePaths;
|
|
495
|
+
return existingDirectories(Array.from(new Set(candidates)));
|
|
496
|
+
}
|
|
497
|
+
function isClaudeUsageEntry(value) {
|
|
498
|
+
const entry = value;
|
|
499
|
+
return entry?.type === "assistant" && typeof entry.timestamp === "string" && entry.message?.usage != null;
|
|
500
|
+
}
|
|
501
|
+
function isClaudeHumanTurn(value) {
|
|
502
|
+
const entry = value;
|
|
503
|
+
if (entry?.type !== "user" || typeof entry.timestamp !== "string") return false;
|
|
504
|
+
const content = entry.message?.content;
|
|
505
|
+
return !(Array.isArray(content) && content.length > 0 && content.every((item) => asRecord(item)?.type === "tool_result"));
|
|
506
|
+
}
|
|
507
|
+
async function collectClaudeUsage() {
|
|
508
|
+
const source = "claude";
|
|
509
|
+
const projectDirs = await getClaudeProjectDirs();
|
|
510
|
+
const files = await globFiles(projectDirs, "**/*.jsonl");
|
|
511
|
+
const entries = [];
|
|
512
|
+
const turns = [];
|
|
513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
514
|
+
const seenTurns = /* @__PURE__ */ new Set();
|
|
515
|
+
for (const file of files) {
|
|
516
|
+
await readJsonlFile(file, (value) => {
|
|
517
|
+
if (isClaudeHumanTurn(value)) {
|
|
518
|
+
const timestamp2 = toIsoTimestamp(value.timestamp);
|
|
519
|
+
if (timestamp2 == null) return;
|
|
520
|
+
const sessionId2 = value.sessionId || "";
|
|
521
|
+
const key = `${source}:${sessionId2}:${timestamp2}`;
|
|
522
|
+
if (!seenTurns.has(key)) {
|
|
523
|
+
seenTurns.add(key);
|
|
524
|
+
turns.push({ source, timestamp: timestamp2, key });
|
|
249
525
|
}
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
if (parsed.type !== "assistant" || !parsed.message?.usage) {
|
|
253
|
-
continue;
|
|
526
|
+
return;
|
|
254
527
|
}
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
528
|
+
if (!isClaudeUsageEntry(value)) return;
|
|
529
|
+
const timestamp = toIsoTimestamp(value.timestamp);
|
|
530
|
+
if (timestamp == null) return;
|
|
531
|
+
const usage = value.message.usage;
|
|
532
|
+
const sessionId = value.sessionId || "";
|
|
533
|
+
const requestId = asString(value.requestId);
|
|
534
|
+
const dedupeKey = requestId ? `${source}:${sessionId}:${requestId}` : [
|
|
535
|
+
source,
|
|
536
|
+
sessionId,
|
|
537
|
+
timestamp,
|
|
538
|
+
usage.input_tokens,
|
|
539
|
+
usage.output_tokens,
|
|
540
|
+
usage.cache_creation_input_tokens ?? 0,
|
|
541
|
+
usage.cache_read_input_tokens ?? 0
|
|
542
|
+
].join(":");
|
|
543
|
+
if (seen.has(dedupeKey)) return;
|
|
260
544
|
seen.add(dedupeKey);
|
|
261
545
|
const inputTokens = usage.input_tokens || 0;
|
|
262
546
|
const outputTokens = usage.output_tokens || 0;
|
|
263
547
|
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
264
548
|
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
549
|
+
const model = value.message.model || "unknown";
|
|
550
|
+
const costUSD = value.costUSD && value.costUSD > 0 ? value.costUSD : calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens);
|
|
265
551
|
entries.push({
|
|
266
|
-
|
|
552
|
+
source,
|
|
553
|
+
timestamp,
|
|
267
554
|
sessionId,
|
|
268
555
|
requestId,
|
|
269
|
-
model
|
|
556
|
+
model,
|
|
270
557
|
inputTokens,
|
|
271
558
|
outputTokens,
|
|
272
559
|
cacheCreationTokens,
|
|
273
560
|
cacheReadTokens,
|
|
274
561
|
totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
|
|
275
|
-
costUSD
|
|
562
|
+
costUSD
|
|
276
563
|
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return { source, entries, turns, files: files.length, warnings: [] };
|
|
567
|
+
}
|
|
568
|
+
var claudeCollector = {
|
|
569
|
+
source: "claude",
|
|
570
|
+
label: "Claude",
|
|
571
|
+
collect: collectClaudeUsage
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/sources/codex.ts
|
|
575
|
+
import { join as join7, relative, sep } from "path";
|
|
576
|
+
import { homedir as homedir7 } from "os";
|
|
577
|
+
function getCodexSessionDirs() {
|
|
578
|
+
const homes = parsePathList(process.env[CODEX_HOME_ENV], [join7(homedir7(), DEFAULT_CODEX_DIR)]);
|
|
579
|
+
return existingDirectories(homes.map((home) => join7(home, "sessions")));
|
|
580
|
+
}
|
|
581
|
+
function normalizeRawUsage(value) {
|
|
582
|
+
const record = asRecord(value);
|
|
583
|
+
if (record == null) return null;
|
|
584
|
+
const inputTokens = asNumber(record.input_tokens);
|
|
585
|
+
const cachedInputTokens = asNumber(record.cached_input_tokens ?? record.cache_read_input_tokens);
|
|
586
|
+
const outputTokens = asNumber(record.output_tokens);
|
|
587
|
+
const reasoningTokens = asNumber(record.reasoning_output_tokens);
|
|
588
|
+
const fallbackTotal = inputTokens + outputTokens + reasoningTokens;
|
|
589
|
+
const totalTokens = asNumber(record.total_tokens) || fallbackTotal;
|
|
590
|
+
return { inputTokens, cachedInputTokens, outputTokens, reasoningTokens, totalTokens };
|
|
591
|
+
}
|
|
592
|
+
function subtractUsage(current, previous) {
|
|
593
|
+
return {
|
|
594
|
+
inputTokens: Math.max(current.inputTokens - (previous?.inputTokens ?? 0), 0),
|
|
595
|
+
cachedInputTokens: Math.max(current.cachedInputTokens - (previous?.cachedInputTokens ?? 0), 0),
|
|
596
|
+
outputTokens: Math.max(current.outputTokens - (previous?.outputTokens ?? 0), 0),
|
|
597
|
+
reasoningTokens: Math.max(current.reasoningTokens - (previous?.reasoningTokens ?? 0), 0),
|
|
598
|
+
totalTokens: Math.max(current.totalTokens - (previous?.totalTokens ?? 0), 0)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function extractModelFromPayload(payload) {
|
|
602
|
+
const payloadRecord = asRecord(payload);
|
|
603
|
+
const info = asRecord(payloadRecord?.info);
|
|
604
|
+
const metadata = asRecord(info?.metadata) ?? asRecord(payloadRecord?.metadata);
|
|
605
|
+
return asString(info?.model) ?? asString(info?.model_name) ?? asString(metadata?.model) ?? asString(payloadRecord?.model) ?? asString(payloadRecord?.model_name);
|
|
606
|
+
}
|
|
607
|
+
function sessionIdForFile(sessionDir, file) {
|
|
608
|
+
return relative(sessionDir, file).split(sep).join("/").replace(/\.jsonl$/i, "");
|
|
609
|
+
}
|
|
610
|
+
async function collectCodexUsage() {
|
|
611
|
+
const source = "codex";
|
|
612
|
+
const sessionDirs = await getCodexSessionDirs();
|
|
613
|
+
const files = await globFiles(sessionDirs, "**/*.jsonl");
|
|
614
|
+
const entries = [];
|
|
615
|
+
const turns = [];
|
|
616
|
+
const seen = /* @__PURE__ */ new Set();
|
|
617
|
+
for (const sessionDir of sessionDirs) {
|
|
618
|
+
const sessionFiles = files.filter((file) => file.startsWith(`${sessionDir}${sep}`));
|
|
619
|
+
for (const file of sessionFiles) {
|
|
620
|
+
const sessionId = sessionIdForFile(sessionDir, file);
|
|
621
|
+
let previousTotal = null;
|
|
622
|
+
let currentModel;
|
|
623
|
+
await readJsonlFile(file, (value) => {
|
|
624
|
+
const record = asRecord(value);
|
|
625
|
+
if (record == null) return;
|
|
626
|
+
const payload = asRecord(record.payload);
|
|
627
|
+
const type = asString(record.type);
|
|
628
|
+
if (type === "turn_context") {
|
|
629
|
+
currentModel = extractModelFromPayload(payload) ?? currentModel;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (type !== "event_msg" || payload?.type !== "token_count") return;
|
|
633
|
+
const timestamp = toIsoTimestamp(record.timestamp);
|
|
634
|
+
if (timestamp == null) return;
|
|
635
|
+
const info = asRecord(payload.info);
|
|
636
|
+
const lastUsage = normalizeRawUsage(info?.last_token_usage);
|
|
637
|
+
const totalUsage = normalizeRawUsage(info?.total_token_usage);
|
|
638
|
+
const rawUsage = lastUsage ?? (totalUsage == null ? null : subtractUsage(totalUsage, previousTotal));
|
|
639
|
+
if (totalUsage != null) previousTotal = totalUsage;
|
|
640
|
+
if (rawUsage == null) return;
|
|
641
|
+
currentModel = extractModelFromPayload(payload) ?? currentModel;
|
|
642
|
+
const model = currentModel ?? "gpt-5";
|
|
643
|
+
const cacheReadTokens = Math.min(rawUsage.cachedInputTokens, rawUsage.inputTokens);
|
|
644
|
+
const inputTokens = Math.max(rawUsage.inputTokens - cacheReadTokens, 0);
|
|
645
|
+
const totalTokens = rawUsage.totalTokens > 0 ? rawUsage.totalTokens : inputTokens + cacheReadTokens + rawUsage.outputTokens + rawUsage.reasoningTokens;
|
|
646
|
+
if (inputTokens === 0 && cacheReadTokens === 0 && rawUsage.outputTokens === 0 && rawUsage.reasoningTokens === 0) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const dedupeKey = [
|
|
650
|
+
source,
|
|
651
|
+
sessionId,
|
|
652
|
+
timestamp,
|
|
653
|
+
model,
|
|
654
|
+
inputTokens,
|
|
655
|
+
cacheReadTokens,
|
|
656
|
+
rawUsage.outputTokens,
|
|
657
|
+
rawUsage.reasoningTokens,
|
|
658
|
+
totalTokens
|
|
659
|
+
].join(":");
|
|
660
|
+
if (seen.has(dedupeKey)) return;
|
|
661
|
+
seen.add(dedupeKey);
|
|
662
|
+
entries.push({
|
|
663
|
+
source,
|
|
664
|
+
timestamp,
|
|
665
|
+
sessionId,
|
|
666
|
+
requestId: dedupeKey,
|
|
667
|
+
model,
|
|
668
|
+
inputTokens,
|
|
669
|
+
outputTokens: rawUsage.outputTokens,
|
|
670
|
+
cacheCreationTokens: 0,
|
|
671
|
+
cacheReadTokens,
|
|
672
|
+
reasoningTokens: rawUsage.reasoningTokens,
|
|
673
|
+
totalTokens,
|
|
674
|
+
costUSD: calculateCost(model, inputTokens, rawUsage.outputTokens, 0, cacheReadTokens, rawUsage.reasoningTokens)
|
|
675
|
+
});
|
|
676
|
+
turns.push({ source, timestamp, key: dedupeKey });
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { source, entries, turns, files: files.length, warnings: [] };
|
|
681
|
+
}
|
|
682
|
+
var codexCollector = {
|
|
683
|
+
source: "codex",
|
|
684
|
+
label: "Codex",
|
|
685
|
+
collect: collectCodexUsage
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/sources/opencode.ts
|
|
689
|
+
import { join as join8 } from "path";
|
|
690
|
+
import { homedir as homedir8 } from "os";
|
|
691
|
+
function getOpenCodeDirs() {
|
|
692
|
+
const dirs = parsePathList(process.env[OPENCODE_DATA_DIR_ENV], [join8(homedir8(), DEFAULT_OPENCODE_DIR)]);
|
|
693
|
+
return existingDirectories(dirs);
|
|
694
|
+
}
|
|
695
|
+
function parseOpenCodeMessage(row) {
|
|
696
|
+
const source = "opencode";
|
|
697
|
+
const record = asRecord(row.data);
|
|
698
|
+
if (record == null) return null;
|
|
699
|
+
const tokens = asRecord(record.tokens);
|
|
700
|
+
const cache = asRecord(tokens?.cache);
|
|
701
|
+
const model = asString(record.modelID) ?? asString(record.model) ?? "unknown";
|
|
702
|
+
const providerID = asString(record.providerID) ?? "unknown";
|
|
703
|
+
if (model === "unknown" || tokens == null) return null;
|
|
704
|
+
const time = asRecord(record.time);
|
|
705
|
+
const timestamp = toIsoTimestamp(time?.created ?? time?.completed);
|
|
706
|
+
if (timestamp == null) return null;
|
|
707
|
+
const inputTokens = asNumber(tokens.input);
|
|
708
|
+
const outputTokens = asNumber(tokens.output);
|
|
709
|
+
const reasoningTokens = asNumber(tokens.reasoning);
|
|
710
|
+
const cacheCreationTokens = asNumber(cache?.write);
|
|
711
|
+
const cacheReadTokens = asNumber(cache?.read);
|
|
712
|
+
if (inputTokens === 0 && outputTokens === 0 && reasoningTokens === 0 && cacheCreationTokens === 0 && cacheReadTokens === 0) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const sessionId = row.sessionId ?? asString(record.sessionID) ?? "unknown";
|
|
716
|
+
const costUSD = asNumber(record.cost) || calculateCost(
|
|
717
|
+
model,
|
|
718
|
+
inputTokens,
|
|
719
|
+
outputTokens,
|
|
720
|
+
cacheCreationTokens,
|
|
721
|
+
cacheReadTokens,
|
|
722
|
+
reasoningTokens
|
|
723
|
+
);
|
|
724
|
+
return {
|
|
725
|
+
source,
|
|
726
|
+
timestamp,
|
|
727
|
+
sessionId,
|
|
728
|
+
requestId: row.id,
|
|
729
|
+
model: providerID === "unknown" ? model : `${providerID}/${model}`,
|
|
730
|
+
inputTokens,
|
|
731
|
+
outputTokens,
|
|
732
|
+
cacheCreationTokens,
|
|
733
|
+
cacheReadTokens,
|
|
734
|
+
reasoningTokens,
|
|
735
|
+
totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + reasoningTokens,
|
|
736
|
+
costUSD
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async function loadOpenCodeJsonRows(openCodeDirs) {
|
|
740
|
+
const messageDirs = openCodeDirs.map((dir) => join8(dir, "storage", "message"));
|
|
741
|
+
const files = await globFiles(await existingDirectories(messageDirs), "**/*.json");
|
|
742
|
+
const rows = [];
|
|
743
|
+
for (const file of files) {
|
|
744
|
+
const data = await readJsonFile(file);
|
|
745
|
+
const record = asRecord(data);
|
|
746
|
+
const id = asString(record?.id);
|
|
747
|
+
if (id == null) continue;
|
|
748
|
+
rows.push({ id, sessionId: asString(record?.sessionID), data });
|
|
749
|
+
}
|
|
750
|
+
return rows;
|
|
751
|
+
}
|
|
752
|
+
async function loadNodeSqlite() {
|
|
753
|
+
try {
|
|
754
|
+
return await import("sqlite");
|
|
755
|
+
} catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async function loadOpenCodeDbRows(openCodeDirs) {
|
|
760
|
+
const dbFiles = [
|
|
761
|
+
...await globFiles(openCodeDirs, "opencode.db"),
|
|
762
|
+
...await globFiles(openCodeDirs, "opencode-*.db")
|
|
763
|
+
];
|
|
764
|
+
if (dbFiles.length === 0) return [];
|
|
765
|
+
const sqlite = await loadNodeSqlite();
|
|
766
|
+
if (sqlite == null) return [];
|
|
767
|
+
const rows = [];
|
|
768
|
+
for (const dbFile of Array.from(new Set(dbFiles))) {
|
|
769
|
+
let db;
|
|
770
|
+
try {
|
|
771
|
+
db = new sqlite.DatabaseSync(dbFile, { readOnly: true });
|
|
772
|
+
const result = db.prepare("SELECT id, session_id, data FROM message").all();
|
|
773
|
+
for (const raw of result) {
|
|
774
|
+
const id = asString(raw.id);
|
|
775
|
+
const dataText = asString(raw.data);
|
|
776
|
+
if (id == null || dataText == null) continue;
|
|
777
|
+
try {
|
|
778
|
+
rows.push({
|
|
779
|
+
id,
|
|
780
|
+
sessionId: asString(raw.session_id),
|
|
781
|
+
data: JSON.parse(dataText)
|
|
782
|
+
});
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
} catch {
|
|
787
|
+
} finally {
|
|
788
|
+
try {
|
|
789
|
+
db?.close();
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
277
792
|
}
|
|
278
793
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
794
|
+
return rows;
|
|
795
|
+
}
|
|
796
|
+
async function collectOpenCodeUsage() {
|
|
797
|
+
const source = "opencode";
|
|
798
|
+
const openCodeDirs = await getOpenCodeDirs();
|
|
799
|
+
const [jsonRows, dbRows] = await Promise.all([
|
|
800
|
+
loadOpenCodeJsonRows(openCodeDirs),
|
|
801
|
+
loadOpenCodeDbRows(openCodeDirs)
|
|
802
|
+
]);
|
|
803
|
+
const rows = [...dbRows, ...jsonRows];
|
|
804
|
+
const entries = [];
|
|
805
|
+
const turns = [];
|
|
806
|
+
const seen = /* @__PURE__ */ new Set();
|
|
807
|
+
for (const row of rows) {
|
|
808
|
+
if (seen.has(row.id)) continue;
|
|
809
|
+
seen.add(row.id);
|
|
810
|
+
const entry = parseOpenCodeMessage(row);
|
|
811
|
+
if (entry == null) continue;
|
|
812
|
+
entries.push(entry);
|
|
813
|
+
turns.push({ source, timestamp: entry.timestamp, key: `${source}:${row.id}` });
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
source,
|
|
817
|
+
entries,
|
|
818
|
+
turns,
|
|
819
|
+
files: jsonRows.length + dbRows.length,
|
|
820
|
+
warnings: []
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
var openCodeCollector = {
|
|
824
|
+
source: "opencode",
|
|
825
|
+
label: "OpenCode",
|
|
826
|
+
collect: collectOpenCodeUsage
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// src/sources/pi.ts
|
|
830
|
+
import { basename as basename2, join as join9 } from "path";
|
|
831
|
+
import { homedir as homedir9 } from "os";
|
|
832
|
+
function getPiSessionDirs() {
|
|
833
|
+
const dirs = parsePathList(process.env[PI_AGENT_DIR_ENV], [join9(homedir9(), DEFAULT_PI_AGENT_SESSIONS_DIR)]);
|
|
834
|
+
return existingDirectories(dirs);
|
|
835
|
+
}
|
|
836
|
+
function extractSessionId(file) {
|
|
837
|
+
const name = basename2(file, ".jsonl");
|
|
838
|
+
const index = name.indexOf("_");
|
|
839
|
+
return index === -1 ? name : name.slice(index + 1);
|
|
840
|
+
}
|
|
841
|
+
function extractProject(file) {
|
|
842
|
+
const parts = file.split(/[\\/]/g);
|
|
843
|
+
const sessionsIndex = parts.findIndex((part) => part === "sessions");
|
|
844
|
+
return sessionsIndex >= 0 ? parts[sessionsIndex + 1] ?? "unknown" : "unknown";
|
|
845
|
+
}
|
|
846
|
+
function normalizePiModel(model) {
|
|
847
|
+
return model == null ? "unknown" : `[pi] ${model}`;
|
|
848
|
+
}
|
|
849
|
+
async function collectPiUsage() {
|
|
850
|
+
const source = "pi";
|
|
851
|
+
const dirs = await getPiSessionDirs();
|
|
852
|
+
const files = await globFiles(dirs, "**/*.jsonl");
|
|
853
|
+
const entries = [];
|
|
854
|
+
const turns = [];
|
|
855
|
+
const seen = /* @__PURE__ */ new Set();
|
|
856
|
+
for (const file of files) {
|
|
857
|
+
const sessionId = extractSessionId(file);
|
|
858
|
+
const project = extractProject(file);
|
|
859
|
+
await readJsonlFile(file, (value) => {
|
|
860
|
+
const record = asRecord(value);
|
|
861
|
+
const message = asRecord(record?.message);
|
|
862
|
+
const usage = asRecord(message?.usage);
|
|
863
|
+
const timestamp = toIsoTimestamp(record?.timestamp);
|
|
864
|
+
if (timestamp == null || usage == null || message?.role !== "assistant") return;
|
|
865
|
+
const inputTokens = asNumber(usage.input);
|
|
866
|
+
const outputTokens = asNumber(usage.output);
|
|
867
|
+
const cacheReadTokens = asNumber(usage.cacheRead);
|
|
868
|
+
const cacheCreationTokens = asNumber(usage.cacheWrite);
|
|
869
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreationTokens === 0) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const cost = asRecord(usage.cost);
|
|
873
|
+
const model = normalizePiModel(asString(message.model));
|
|
874
|
+
const totalTokens = asNumber(usage.totalTokens) || inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
|
|
875
|
+
const key = [
|
|
876
|
+
source,
|
|
877
|
+
project,
|
|
878
|
+
sessionId,
|
|
879
|
+
timestamp,
|
|
880
|
+
model,
|
|
881
|
+
inputTokens,
|
|
882
|
+
outputTokens,
|
|
883
|
+
cacheCreationTokens,
|
|
884
|
+
cacheReadTokens,
|
|
885
|
+
totalTokens
|
|
886
|
+
].join(":");
|
|
887
|
+
if (seen.has(key)) return;
|
|
888
|
+
seen.add(key);
|
|
889
|
+
entries.push({
|
|
890
|
+
source,
|
|
891
|
+
timestamp,
|
|
892
|
+
sessionId,
|
|
893
|
+
requestId: key,
|
|
894
|
+
model,
|
|
895
|
+
inputTokens,
|
|
896
|
+
outputTokens,
|
|
897
|
+
cacheCreationTokens,
|
|
898
|
+
cacheReadTokens,
|
|
899
|
+
totalTokens,
|
|
900
|
+
costUSD: asNumber(cost?.total)
|
|
901
|
+
});
|
|
902
|
+
turns.push({ source, timestamp, key });
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
return { source, entries, turns, files: files.length, warnings: [] };
|
|
906
|
+
}
|
|
907
|
+
var piCollector = {
|
|
908
|
+
source: "pi",
|
|
909
|
+
label: "pi-agent",
|
|
910
|
+
collect: collectPiUsage
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
// src/sources/index.ts
|
|
914
|
+
var COLLECTORS = {
|
|
915
|
+
claude: claudeCollector,
|
|
916
|
+
codex: codexCollector,
|
|
917
|
+
opencode: openCodeCollector,
|
|
918
|
+
amp: ampCollector,
|
|
919
|
+
pi: piCollector
|
|
920
|
+
};
|
|
921
|
+
function isAgentSource(value) {
|
|
922
|
+
return AGENT_SOURCES.includes(value);
|
|
923
|
+
}
|
|
924
|
+
function parseSources(value) {
|
|
925
|
+
if (value == null || value.trim() === "") return [...AGENT_SOURCES];
|
|
926
|
+
const sources = value.split(",").map((source) => source.trim().toLowerCase()).filter((source) => isAgentSource(source));
|
|
927
|
+
return sources.length > 0 ? Array.from(new Set(sources)) : [...AGENT_SOURCES];
|
|
928
|
+
}
|
|
929
|
+
async function collectAllUsageEntries(options) {
|
|
930
|
+
const selectedSources = options?.sources ?? parseSources(process.env.CCCLUB_SOURCES);
|
|
931
|
+
const results = await Promise.all(
|
|
932
|
+
selectedSources.map(async (source) => {
|
|
933
|
+
const collector = COLLECTORS[source];
|
|
934
|
+
try {
|
|
935
|
+
return await collector.collect();
|
|
936
|
+
} catch (error) {
|
|
937
|
+
return {
|
|
938
|
+
source,
|
|
939
|
+
entries: [],
|
|
940
|
+
turns: [],
|
|
941
|
+
files: 0,
|
|
942
|
+
warnings: [`${AGENT_LABELS[source]}: ${error instanceof Error ? error.message : String(error)}`]
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
);
|
|
947
|
+
const entries = results.flatMap((result) => result.entries).sort(
|
|
948
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
949
|
+
);
|
|
950
|
+
const humanTurns = results.flatMap((result) => result.turns).sort(
|
|
951
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
952
|
+
);
|
|
953
|
+
const warnings = results.flatMap((result) => result.warnings);
|
|
954
|
+
return { entries, humanTurns, sources: results, warnings };
|
|
282
955
|
}
|
|
283
956
|
|
|
284
957
|
// src/aggregator.ts
|
|
@@ -288,9 +961,9 @@ function floorToBlock(date) {
|
|
|
288
961
|
floored.setUTCMinutes(min - min % 30, 0, 0);
|
|
289
962
|
return floored;
|
|
290
963
|
}
|
|
291
|
-
function
|
|
964
|
+
function aggregateSourceToBlocks(source, entries, humanTurns) {
|
|
292
965
|
if (entries.length === 0) return [];
|
|
293
|
-
const humanTurnMs = humanTurns.map((t) => new Date(t).getTime());
|
|
966
|
+
const humanTurnMs = humanTurns.map((t) => new Date(t.timestamp).getTime());
|
|
294
967
|
const blocks = [];
|
|
295
968
|
let blockStart = floorToBlock(new Date(entries[0].timestamp));
|
|
296
969
|
let blockEnd = new Date(blockStart.getTime() + BLOCK_DURATION_MS);
|
|
@@ -315,6 +988,7 @@ function aggregateToBlocks(entries, humanTurns = []) {
|
|
|
315
988
|
let outputTokens = 0;
|
|
316
989
|
let cacheCreationTokens = 0;
|
|
317
990
|
let cacheReadTokens = 0;
|
|
991
|
+
let reasoningTokens = 0;
|
|
318
992
|
let totalTokens = 0;
|
|
319
993
|
let costUSD = 0;
|
|
320
994
|
for (const entry of currentBlock) {
|
|
@@ -322,6 +996,7 @@ function aggregateToBlocks(entries, humanTurns = []) {
|
|
|
322
996
|
outputTokens += entry.outputTokens;
|
|
323
997
|
cacheCreationTokens += entry.cacheCreationTokens;
|
|
324
998
|
cacheReadTokens += entry.cacheReadTokens;
|
|
999
|
+
reasoningTokens += entry.reasoningTokens || 0;
|
|
325
1000
|
totalTokens += entry.totalTokens;
|
|
326
1001
|
models.add(entry.model);
|
|
327
1002
|
if (entry.costUSD > 0) {
|
|
@@ -332,17 +1007,20 @@ function aggregateToBlocks(entries, humanTurns = []) {
|
|
|
332
1007
|
entry.inputTokens,
|
|
333
1008
|
entry.outputTokens,
|
|
334
1009
|
entry.cacheCreationTokens,
|
|
335
|
-
entry.cacheReadTokens
|
|
1010
|
+
entry.cacheReadTokens,
|
|
1011
|
+
entry.reasoningTokens || 0
|
|
336
1012
|
);
|
|
337
1013
|
}
|
|
338
1014
|
}
|
|
339
1015
|
blocks.push({
|
|
1016
|
+
source,
|
|
340
1017
|
blockStart: blockStart.toISOString(),
|
|
341
1018
|
blockEnd: blockEnd.toISOString(),
|
|
342
1019
|
inputTokens,
|
|
343
1020
|
outputTokens,
|
|
344
1021
|
cacheCreationTokens,
|
|
345
1022
|
cacheReadTokens,
|
|
1023
|
+
reasoningTokens,
|
|
346
1024
|
totalTokens,
|
|
347
1025
|
costUSD: Math.round(costUSD * 1e4) / 1e4,
|
|
348
1026
|
models: Array.from(models),
|
|
@@ -363,6 +1041,34 @@ function aggregateToBlocks(entries, humanTurns = []) {
|
|
|
363
1041
|
flushBlock();
|
|
364
1042
|
return blocks;
|
|
365
1043
|
}
|
|
1044
|
+
function aggregateToBlocks(entries, humanTurns = []) {
|
|
1045
|
+
if (entries.length === 0) return [];
|
|
1046
|
+
const entriesBySource = /* @__PURE__ */ new Map();
|
|
1047
|
+
for (const entry of entries) {
|
|
1048
|
+
const group = entriesBySource.get(entry.source) ?? [];
|
|
1049
|
+
group.push(entry);
|
|
1050
|
+
entriesBySource.set(entry.source, group);
|
|
1051
|
+
}
|
|
1052
|
+
const turnsBySource = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const turn of humanTurns) {
|
|
1054
|
+
const group = turnsBySource.get(turn.source) ?? [];
|
|
1055
|
+
group.push(turn);
|
|
1056
|
+
turnsBySource.set(turn.source, group);
|
|
1057
|
+
}
|
|
1058
|
+
const blocks = [];
|
|
1059
|
+
for (const [source, sourceEntries] of entriesBySource) {
|
|
1060
|
+
blocks.push(
|
|
1061
|
+
...aggregateSourceToBlocks(
|
|
1062
|
+
source,
|
|
1063
|
+
sourceEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()),
|
|
1064
|
+
turnsBySource.get(source) ?? []
|
|
1065
|
+
)
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
return blocks.sort(
|
|
1069
|
+
(a, b) => new Date(a.blockStart).getTime() - new Date(b.blockStart).getTime() || (a.source ?? "claude").localeCompare(b.source ?? "claude")
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
366
1072
|
|
|
367
1073
|
// src/fetch-error.ts
|
|
368
1074
|
import chalk from "chalk";
|
|
@@ -394,15 +1100,15 @@ function formatFetchError(err) {
|
|
|
394
1100
|
// src/usage-limits.ts
|
|
395
1101
|
import { execSync as execSync2, exec } from "child_process";
|
|
396
1102
|
import { promisify } from "util";
|
|
397
|
-
import { userInfo as userInfo2, homedir as
|
|
398
|
-
import { join as
|
|
1103
|
+
import { userInfo as userInfo2, homedir as homedir10 } from "os";
|
|
1104
|
+
import { join as join10 } from "path";
|
|
399
1105
|
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
400
1106
|
var execAsync = promisify(exec);
|
|
401
1107
|
var debug = (...args) => {
|
|
402
1108
|
if (process.env.CCCLUB_DEBUG) console.error("[usage-debug]", ...args);
|
|
403
1109
|
};
|
|
404
1110
|
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
405
|
-
var CACHE_PATH =
|
|
1111
|
+
var CACHE_PATH = join10(homedir10(), CCCLUB_CONFIG_DIR, "usage-cache.json");
|
|
406
1112
|
function readCache(allowStale = false) {
|
|
407
1113
|
try {
|
|
408
1114
|
const raw = readFileSync2(CACHE_PATH, "utf-8");
|
|
@@ -491,13 +1197,16 @@ async function fetchUsageLimits() {
|
|
|
491
1197
|
}
|
|
492
1198
|
|
|
493
1199
|
// src/commands/sync.ts
|
|
494
|
-
var SYNC_FORMAT_VERSION = "
|
|
1200
|
+
var SYNC_FORMAT_VERSION = "7";
|
|
495
1201
|
function getSyncVersionPath() {
|
|
496
|
-
return
|
|
1202
|
+
return join11(homedir11(), CCCLUB_CONFIG_DIR, "sync-version");
|
|
1203
|
+
}
|
|
1204
|
+
function getLastSyncBySourcePath() {
|
|
1205
|
+
return join11(homedir11(), CCCLUB_CONFIG_DIR, "last-sync-sources.json");
|
|
497
1206
|
}
|
|
498
1207
|
function needsFullSync() {
|
|
499
1208
|
const path = getSyncVersionPath();
|
|
500
|
-
if (!
|
|
1209
|
+
if (!existsSync4(path)) return true;
|
|
501
1210
|
try {
|
|
502
1211
|
const stored = readFileSync3(path, "utf-8").trim();
|
|
503
1212
|
return stored !== SYNC_FORMAT_VERSION;
|
|
@@ -509,7 +1218,7 @@ var THROTTLE_MS = 5 * 60 * 1e3;
|
|
|
509
1218
|
async function syncCommand(options) {
|
|
510
1219
|
const timePath = getLastSyncTimePath();
|
|
511
1220
|
if (options.silent && !options.full) {
|
|
512
|
-
if (
|
|
1221
|
+
if (existsSync4(timePath)) {
|
|
513
1222
|
try {
|
|
514
1223
|
const ts = parseInt(readFileSync3(timePath, "utf-8").trim(), 10);
|
|
515
1224
|
if (Date.now() - ts < THROTTLE_MS) return;
|
|
@@ -542,31 +1251,47 @@ async function doSync(firstSync = false, silent = false) {
|
|
|
542
1251
|
} : console.log;
|
|
543
1252
|
const spinner = silent ? null : ora("Collecting usage data...").start();
|
|
544
1253
|
try {
|
|
545
|
-
const [{ entries, humanTurns }, usageSnapshot] = await Promise.all([
|
|
546
|
-
|
|
1254
|
+
const [{ entries, humanTurns, sources, warnings }, usageSnapshot] = await Promise.all([
|
|
1255
|
+
collectAllUsageEntries(),
|
|
547
1256
|
fetchUsageLimits().catch(() => null)
|
|
548
1257
|
]);
|
|
549
|
-
|
|
1258
|
+
const activeSources = sources.filter((source) => source.entries.length > 0).map((source) => AGENT_LABELS[source.source]);
|
|
1259
|
+
if (spinner) spinner.text = `Found ${entries.length} entries${activeSources.length > 0 ? ` from ${activeSources.join(", ")}` : ""}`;
|
|
550
1260
|
if (entries.length === 0) {
|
|
551
|
-
if (spinner) spinner.warn("No usage data found
|
|
1261
|
+
if (spinner) spinner.warn("No usage data found for supported coding agents");
|
|
1262
|
+
if (!silent && warnings.length > 0) {
|
|
1263
|
+
for (const warning of warnings) log(chalk2.dim(` ${warning}`));
|
|
1264
|
+
}
|
|
552
1265
|
return;
|
|
553
1266
|
}
|
|
554
1267
|
const allBlocks = aggregateToBlocks(entries, humanTurns);
|
|
555
1268
|
const lastSyncPath = getLastSyncPath();
|
|
556
1269
|
let lastSync = null;
|
|
557
|
-
if (
|
|
1270
|
+
if (existsSync4(lastSyncPath)) {
|
|
558
1271
|
lastSync = (await readFile4(lastSyncPath, "utf-8")).trim() || null;
|
|
559
1272
|
}
|
|
1273
|
+
let lastSyncBySource = {};
|
|
1274
|
+
if (existsSync4(getLastSyncBySourcePath())) {
|
|
1275
|
+
try {
|
|
1276
|
+
lastSyncBySource = JSON.parse(await readFile4(getLastSyncBySourcePath(), "utf-8"));
|
|
1277
|
+
} catch {
|
|
1278
|
+
lastSyncBySource = {};
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
560
1281
|
let blocksToSync;
|
|
561
1282
|
if (lastSync && !firstSync) {
|
|
562
|
-
|
|
563
|
-
|
|
1283
|
+
blocksToSync = allBlocks.filter((b) => {
|
|
1284
|
+
const source = b.source ?? "claude";
|
|
1285
|
+
const sourceLastSync = lastSyncBySource[source] ?? lastSync;
|
|
1286
|
+
return new Date(b.blockStart).getTime() >= new Date(sourceLastSync).getTime();
|
|
1287
|
+
});
|
|
564
1288
|
} else {
|
|
565
1289
|
blocksToSync = allBlocks;
|
|
566
1290
|
}
|
|
567
1291
|
if (blocksToSync.length === 0) {
|
|
568
1292
|
if (spinner) spinner.succeed("Already up to date");
|
|
569
|
-
await
|
|
1293
|
+
await writeFile4(getLastSyncBySourcePath(), JSON.stringify(getLatestBlockStartBySource(allBlocks), null, 2));
|
|
1294
|
+
await writeFile4(getSyncVersionPath(), SYNC_FORMAT_VERSION);
|
|
570
1295
|
if (usageSnapshot) {
|
|
571
1296
|
fetch(`${config.apiUrl}/api/usage`, {
|
|
572
1297
|
method: "POST",
|
|
@@ -598,27 +1323,42 @@ async function doSync(firstSync = false, silent = false) {
|
|
|
598
1323
|
}
|
|
599
1324
|
const data = await res.json();
|
|
600
1325
|
const latest = blocksToSync[blocksToSync.length - 1];
|
|
601
|
-
await
|
|
602
|
-
await
|
|
603
|
-
await
|
|
1326
|
+
await writeFile4(lastSyncPath, latest.blockStart);
|
|
1327
|
+
await writeFile4(getLastSyncBySourcePath(), JSON.stringify(getLatestBlockStartBySource(allBlocks), null, 2));
|
|
1328
|
+
await writeFile4(getSyncVersionPath(), SYNC_FORMAT_VERSION);
|
|
1329
|
+
await writeFile4(getLastSyncTimePath(), String(Date.now()));
|
|
604
1330
|
const totalTokens = blocksToSync.reduce((s, b) => s + b.totalTokens, 0);
|
|
605
1331
|
const totalCost = blocksToSync.reduce((s, b) => s + b.costUSD, 0);
|
|
606
1332
|
if (spinner) {
|
|
607
1333
|
spinner.succeed(`Synced ${data.synced} blocks`);
|
|
608
1334
|
log(chalk2.dim(` Tokens: ${totalTokens.toLocaleString()} Cost: $${totalCost.toFixed(4)}`));
|
|
1335
|
+
if (warnings.length > 0) {
|
|
1336
|
+
for (const warning of warnings) log(chalk2.dim(` ${warning}`));
|
|
1337
|
+
}
|
|
609
1338
|
}
|
|
610
1339
|
} catch (err) {
|
|
611
1340
|
if (spinner) spinner.fail(`Sync error: ${formatFetchError(err)}`);
|
|
612
1341
|
if (silent) throw err;
|
|
613
1342
|
}
|
|
614
1343
|
}
|
|
1344
|
+
function getLatestBlockStartBySource(blocks) {
|
|
1345
|
+
const latest = {};
|
|
1346
|
+
for (const block of blocks) {
|
|
1347
|
+
const source = block.source ?? "claude";
|
|
1348
|
+
if (!AGENT_SOURCES.includes(source)) continue;
|
|
1349
|
+
if (latest[source] == null || block.blockStart > latest[source]) {
|
|
1350
|
+
latest[source] = block.blockStart;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return latest;
|
|
1354
|
+
}
|
|
615
1355
|
|
|
616
1356
|
// src/global-install.ts
|
|
617
1357
|
import { exec as exec2 } from "child_process";
|
|
618
1358
|
import chalk3 from "chalk";
|
|
619
1359
|
function run(cmd) {
|
|
620
|
-
return new Promise((
|
|
621
|
-
exec2(cmd, (err, stdout5) =>
|
|
1360
|
+
return new Promise((resolve2) => {
|
|
1361
|
+
exec2(cmd, (err, stdout5) => resolve2(err ? "" : stdout5.trim()));
|
|
622
1362
|
});
|
|
623
1363
|
}
|
|
624
1364
|
async function ensureGlobalInstall() {
|
|
@@ -645,10 +1385,14 @@ async function initCommand() {
|
|
|
645
1385
|
const hookOk = await installHook();
|
|
646
1386
|
if (hookOk) console.log(chalk4.green(" Auto-sync hook installed!"));
|
|
647
1387
|
}
|
|
1388
|
+
if (!isHeartbeatInstalled()) {
|
|
1389
|
+
const heartbeatOk = await installHeartbeat();
|
|
1390
|
+
if (heartbeatOk) console.log(chalk4.green(" Background sync installed!"));
|
|
1391
|
+
}
|
|
648
1392
|
console.log(chalk4.dim('\n Run "ccclub" to see the leaderboard'));
|
|
649
1393
|
return;
|
|
650
1394
|
}
|
|
651
|
-
const rl =
|
|
1395
|
+
const rl = createInterface2({ input: stdin, output: stdout });
|
|
652
1396
|
try {
|
|
653
1397
|
const defaultName = getDefaultDisplayName();
|
|
654
1398
|
const prompt = defaultName ? chalk4.bold(`Your display name (${defaultName}): `) : chalk4.bold("Your display name: ");
|
|
@@ -686,11 +1430,17 @@ async function initCommand() {
|
|
|
686
1430
|
displayName: displayName.trim(),
|
|
687
1431
|
groups: [data.groupCode]
|
|
688
1432
|
});
|
|
689
|
-
const hookOk = await
|
|
1433
|
+
const [hookOk, heartbeatOk] = await Promise.all([
|
|
1434
|
+
installHook(),
|
|
1435
|
+
installHeartbeat()
|
|
1436
|
+
]);
|
|
690
1437
|
spinner.succeed("ccclub initialized!");
|
|
691
1438
|
if (!hookOk) {
|
|
692
1439
|
console.log(chalk4.dim(' Tip: run "ccclub hook" to set up auto-sync'));
|
|
693
1440
|
}
|
|
1441
|
+
if (!heartbeatOk) {
|
|
1442
|
+
console.log(chalk4.dim(' Tip: run "ccclub sync" manually to refresh non-Claude agent usage'));
|
|
1443
|
+
}
|
|
694
1444
|
console.log("");
|
|
695
1445
|
console.log(chalk4.bold(" Invite friends to compete:"));
|
|
696
1446
|
console.log("");
|
|
@@ -712,7 +1462,7 @@ function printQuickStart() {
|
|
|
712
1462
|
}
|
|
713
1463
|
|
|
714
1464
|
// src/commands/join.ts
|
|
715
|
-
import { createInterface as
|
|
1465
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
716
1466
|
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
717
1467
|
import chalk5 from "chalk";
|
|
718
1468
|
import ora3 from "ora";
|
|
@@ -725,7 +1475,7 @@ async function joinCommand(inviteCode) {
|
|
|
725
1475
|
token = config.token;
|
|
726
1476
|
displayName = config.displayName;
|
|
727
1477
|
} else {
|
|
728
|
-
const rl =
|
|
1478
|
+
const rl = createInterface3({ input: stdin2, output: stdout2 });
|
|
729
1479
|
try {
|
|
730
1480
|
const defaultName = getDefaultDisplayName();
|
|
731
1481
|
const prompt = defaultName ? chalk5.bold(`Your display name (${defaultName}): `) : chalk5.bold("Your display name: ");
|
|
@@ -791,16 +1541,16 @@ import Table from "cli-table3";
|
|
|
791
1541
|
import ora4 from "ora";
|
|
792
1542
|
|
|
793
1543
|
// src/update-check.ts
|
|
794
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as
|
|
795
|
-
import { join as
|
|
796
|
-
import { homedir as
|
|
1544
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
1545
|
+
import { join as join12 } from "path";
|
|
1546
|
+
import { homedir as homedir12 } from "os";
|
|
797
1547
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
798
|
-
var CHECK_FILE =
|
|
1548
|
+
var CHECK_FILE = join12(homedir12(), CCCLUB_CONFIG_DIR, "last-update-check");
|
|
799
1549
|
var pendingCheck = null;
|
|
800
1550
|
function startUpdateCheck(currentVersion) {
|
|
801
1551
|
if (process.argv.includes("--silent") || process.argv.includes("-s")) return;
|
|
802
1552
|
try {
|
|
803
|
-
if (
|
|
1553
|
+
if (existsSync5(CHECK_FILE)) {
|
|
804
1554
|
const ts = parseInt(readFileSync4(CHECK_FILE, "utf-8").trim(), 10);
|
|
805
1555
|
if (Date.now() - ts < CHECK_INTERVAL_MS) return;
|
|
806
1556
|
}
|
|
@@ -836,6 +1586,7 @@ var ACTIVE_THRESHOLD_MS = 15 * 60 * 1e3;
|
|
|
836
1586
|
async function rankCommand(options) {
|
|
837
1587
|
const config = await requireConfig();
|
|
838
1588
|
if (!isHookInstalled()) await installHook();
|
|
1589
|
+
if (!isHeartbeatInstalled()) await installHeartbeat();
|
|
839
1590
|
if (needsFullSync()) {
|
|
840
1591
|
await doSync(true, true);
|
|
841
1592
|
}
|
|
@@ -932,7 +1683,7 @@ async function rankCommand(options) {
|
|
|
932
1683
|
if (activityData) renderActivity(activityData, range);
|
|
933
1684
|
if (i < groupResults.length - 1) console.log("");
|
|
934
1685
|
}
|
|
935
|
-
console.log(chalk6.dim("\n Tokens = input + output ") + chalk6.yellow("(cache excluded)") + chalk6.dim(". Use ") + chalk6.white("--cache") + chalk6.dim(" to include cache tokens."));
|
|
1686
|
+
console.log(chalk6.dim("\n Tokens = input + output + reasoning ") + chalk6.yellow("(cache excluded)") + chalk6.dim(". Use ") + chalk6.white("--cache") + chalk6.dim(" to include cache tokens."));
|
|
936
1687
|
const update = await getUpdateResult();
|
|
937
1688
|
if (update) {
|
|
938
1689
|
console.log(chalk6.yellow("\n Update available") + chalk6.dim(`: ${update.current} \u2192 ${update.latest} Run `) + chalk6.cyan("npm i -g ccclub@latest"));
|
|
@@ -978,14 +1729,21 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
|
|
|
978
1729
|
const hiddenCount = data.rankings.length - activeRankings.length;
|
|
979
1730
|
const hasPlan = activeRankings.some((r) => r.plan);
|
|
980
1731
|
const hasUsage = activeRankings.some((r) => r.usageSnapshot);
|
|
1732
|
+
const hasAgents = activeRankings.some(
|
|
1733
|
+
(r) => r.agents && r.agents.length > 0 && !(r.agents.length === 1 && r.agents[0] === "claude")
|
|
1734
|
+
);
|
|
981
1735
|
const head = ["#", "Name", "Cost", "Tokens"];
|
|
982
1736
|
const hasActive = activeRankings.some((r) => r.lastSync && now - new Date(r.lastSync).getTime() < ACTIVE_THRESHOLD_MS);
|
|
983
1737
|
const widths = [5, hasActive ? 30 : 20, 12, 10];
|
|
1738
|
+
if (hasAgents) {
|
|
1739
|
+
head.splice(2, 0, "Agents");
|
|
1740
|
+
widths.splice(2, 0, 16);
|
|
1741
|
+
}
|
|
984
1742
|
if (hasPlan) {
|
|
985
1743
|
head.push("Monthly ROI");
|
|
986
1744
|
widths.push(15);
|
|
987
1745
|
}
|
|
988
|
-
head.push("
|
|
1746
|
+
head.push("Turns", "$/Turn");
|
|
989
1747
|
widths.push(8, 9);
|
|
990
1748
|
if (hasUsage) {
|
|
991
1749
|
head.push("Usage 7d");
|
|
@@ -998,7 +1756,7 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
|
|
|
998
1756
|
});
|
|
999
1757
|
for (const entry of activeRankings) {
|
|
1000
1758
|
const isMe = entry.userId === config.userId;
|
|
1001
|
-
const tokens = showCache ? entry.totalTokens : entry.inputTokens + entry.outputTokens;
|
|
1759
|
+
const tokens = showCache ? entry.totalTokens : entry.inputTokens + entry.outputTokens + (entry.reasoningTokens || 0);
|
|
1002
1760
|
const marker = isMe ? chalk6.green("\u2192") : " ";
|
|
1003
1761
|
const isActive = entry.lastSync && now - new Date(entry.lastSync).getTime() < ACTIVE_THRESHOLD_MS;
|
|
1004
1762
|
const id = (s) => s;
|
|
@@ -1007,10 +1765,12 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
|
|
|
1007
1765
|
const displayName = isActive ? `${entry.displayName} ${chalk6.green("(active)")}` : entry.displayName;
|
|
1008
1766
|
const row = [
|
|
1009
1767
|
`${marker}${c(String(entry.rank))}`,
|
|
1010
|
-
nameC(displayName)
|
|
1011
|
-
c(`$${entry.costUSD.toFixed(2)}`),
|
|
1012
|
-
c(formatTokens(tokens))
|
|
1768
|
+
nameC(displayName)
|
|
1013
1769
|
];
|
|
1770
|
+
if (hasAgents) {
|
|
1771
|
+
row.push(c(formatAgents(entry.agents)));
|
|
1772
|
+
}
|
|
1773
|
+
row.push(c(`$${entry.costUSD.toFixed(2)}`), c(formatTokens(tokens)));
|
|
1014
1774
|
if (hasPlan) {
|
|
1015
1775
|
if (entry.plan && entry.plan !== "api") {
|
|
1016
1776
|
const price = PLAN_PRICES[entry.plan];
|
|
@@ -1052,6 +1812,10 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
|
|
|
1052
1812
|
}
|
|
1053
1813
|
}
|
|
1054
1814
|
}
|
|
1815
|
+
function formatAgents(agents) {
|
|
1816
|
+
if (!agents || agents.length === 0) return "\u2014";
|
|
1817
|
+
return agents.map((agent) => AGENT_LABELS[agent] ?? agent).join(", ");
|
|
1818
|
+
}
|
|
1055
1819
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587";
|
|
1056
1820
|
function renderActivity(data, range) {
|
|
1057
1821
|
const active = data.series.filter((s) => s.blocks.length > 0);
|
|
@@ -1219,38 +1983,49 @@ async function showDataCommand() {
|
|
|
1219
1983
|
console.log(chalk8.bold("\n What ccclub uploads:\n"));
|
|
1220
1984
|
console.log(chalk8.dim(" Only aggregated 30-minute block summaries. No conversation content,"));
|
|
1221
1985
|
console.log(chalk8.dim(" no file paths, no project names, no session details.\n"));
|
|
1222
|
-
const { entries, humanTurns } = await
|
|
1986
|
+
const { entries, humanTurns, sources, warnings } = await collectAllUsageEntries();
|
|
1223
1987
|
const blocks = aggregateToBlocks(entries, humanTurns);
|
|
1224
1988
|
if (blocks.length === 0) {
|
|
1225
|
-
console.log(chalk8.yellow(" No usage data found
|
|
1989
|
+
console.log(chalk8.yellow(" No usage data found for supported coding agents."));
|
|
1990
|
+
for (const warning of warnings) console.log(chalk8.dim(` ${warning}`));
|
|
1226
1991
|
return;
|
|
1227
1992
|
}
|
|
1228
1993
|
console.log(chalk8.dim(` Total entries found: ${entries.length}`));
|
|
1229
1994
|
console.log(chalk8.dim(` Aggregated into: ${blocks.length} blocks
|
|
1230
1995
|
`));
|
|
1996
|
+
console.log(chalk8.bold(" Sources:\n"));
|
|
1997
|
+
for (const source of sources.filter((s) => s.entries.length > 0)) {
|
|
1998
|
+
console.log(chalk8.dim(` ${AGENT_LABELS[source.source]}: ${source.entries.length.toLocaleString()} entries from ${source.files.toLocaleString()} files/records`));
|
|
1999
|
+
}
|
|
2000
|
+
console.log("");
|
|
1231
2001
|
const recent = blocks.slice(-5);
|
|
1232
2002
|
console.log(chalk8.bold(" Last 5 blocks (this is exactly what gets uploaded):\n"));
|
|
1233
2003
|
for (const block of recent) {
|
|
1234
|
-
console.log(chalk8.cyan(` ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
|
|
2004
|
+
console.log(chalk8.cyan(` ${AGENT_LABELS[block.source ?? "claude"]} \xB7 ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
|
|
1235
2005
|
console.log(chalk8.dim(` input: ${block.inputTokens.toLocaleString()} output: ${block.outputTokens.toLocaleString()} cache_create: ${block.cacheCreationTokens.toLocaleString()} cache_read: ${block.cacheReadTokens.toLocaleString()}`));
|
|
1236
|
-
|
|
2006
|
+
if (block.reasoningTokens) {
|
|
2007
|
+
console.log(chalk8.dim(` reasoning: ${block.reasoningTokens.toLocaleString()}`));
|
|
2008
|
+
}
|
|
2009
|
+
console.log(chalk8.dim(` cost: $${block.costUSD.toFixed(4)} calls: ${block.entryCount} turns: ${block.chatCount || 0} models: ${block.models.join(", ")}`));
|
|
1237
2010
|
}
|
|
1238
2011
|
const totalInput = blocks.reduce((s, b) => s + b.inputTokens, 0);
|
|
1239
2012
|
const totalOutput = blocks.reduce((s, b) => s + b.outputTokens, 0);
|
|
2013
|
+
const totalReasoning = blocks.reduce((s, b) => s + (b.reasoningTokens || 0), 0);
|
|
2014
|
+
const totalCache = blocks.reduce((s, b) => s + b.cacheCreationTokens + b.cacheReadTokens, 0);
|
|
1240
2015
|
const totalCost = blocks.reduce((s, b) => s + b.costUSD, 0);
|
|
1241
2016
|
console.log(chalk8.bold(`
|
|
1242
|
-
All-time total: ${(totalInput + totalOutput).toLocaleString()} tokens \xB7 $${totalCost.toFixed(2)}`));
|
|
1243
|
-
console.log(chalk8.dim(` input: ${totalInput.toLocaleString()} output: ${totalOutput.toLocaleString()}`));
|
|
2017
|
+
All-time total: ${(totalInput + totalOutput + totalReasoning).toLocaleString()} non-cache tokens \xB7 $${totalCost.toFixed(2)}`));
|
|
2018
|
+
console.log(chalk8.dim(` input: ${totalInput.toLocaleString()} output: ${totalOutput.toLocaleString()} reasoning: ${totalReasoning.toLocaleString()} cache: ${totalCache.toLocaleString()}`));
|
|
1244
2019
|
}
|
|
1245
2020
|
|
|
1246
2021
|
// src/commands/group.ts
|
|
1247
|
-
import { createInterface as
|
|
2022
|
+
import { createInterface as createInterface4 } from "readline/promises";
|
|
1248
2023
|
import { stdin as stdin3, stdout as stdout3 } from "process";
|
|
1249
2024
|
import chalk9 from "chalk";
|
|
1250
2025
|
import ora6 from "ora";
|
|
1251
2026
|
async function createGroupCommand() {
|
|
1252
2027
|
const config = await requireConfig();
|
|
1253
|
-
const rl =
|
|
2028
|
+
const rl = createInterface4({ input: stdin3, output: stdout3 });
|
|
1254
2029
|
try {
|
|
1255
2030
|
const name = await rl.question(chalk9.bold("Group name: "));
|
|
1256
2031
|
if (!name.trim()) {
|
|
@@ -1294,7 +2069,7 @@ async function createGroupCommand() {
|
|
|
1294
2069
|
}
|
|
1295
2070
|
|
|
1296
2071
|
// src/commands/leave.ts
|
|
1297
|
-
import { createInterface as
|
|
2072
|
+
import { createInterface as createInterface5 } from "readline/promises";
|
|
1298
2073
|
import { stdin as stdin4, stdout as stdout4 } from "process";
|
|
1299
2074
|
import chalk10 from "chalk";
|
|
1300
2075
|
import ora7 from "ora";
|
|
@@ -1318,7 +2093,7 @@ async function leaveCommand(code) {
|
|
|
1318
2093
|
for (let i = 0; i < config.groups.length; i++) {
|
|
1319
2094
|
console.log(` ${i + 1}. ${config.groups[i]}`);
|
|
1320
2095
|
}
|
|
1321
|
-
const rl2 =
|
|
2096
|
+
const rl2 = createInterface5({ input: stdin4, output: stdout4 });
|
|
1322
2097
|
try {
|
|
1323
2098
|
const input = await rl2.question(chalk10.bold("\n Leave which group? (number): "));
|
|
1324
2099
|
const idx = parseInt(input.trim(), 10) - 1;
|
|
@@ -1331,7 +2106,7 @@ async function leaveCommand(code) {
|
|
|
1331
2106
|
rl2.close();
|
|
1332
2107
|
}
|
|
1333
2108
|
}
|
|
1334
|
-
const rl =
|
|
2109
|
+
const rl = createInterface5({ input: stdin4, output: stdout4 });
|
|
1335
2110
|
try {
|
|
1336
2111
|
const answer = await rl.question(chalk10.bold(` Leave group ${targetCode}? [y/N] `));
|
|
1337
2112
|
if (answer.trim().toLowerCase() !== "y") {
|
|
@@ -1385,10 +2160,10 @@ async function hookCommand() {
|
|
|
1385
2160
|
}
|
|
1386
2161
|
|
|
1387
2162
|
// src/index.ts
|
|
1388
|
-
var VERSION = "0.3.
|
|
2163
|
+
var VERSION = "0.3.3";
|
|
1389
2164
|
startUpdateCheck(VERSION);
|
|
1390
2165
|
var program = new Command();
|
|
1391
|
-
program.name("ccclub").description("
|
|
2166
|
+
program.name("ccclub").description("Coding agent usage leaderboard among friends").version(VERSION, "-v, -V, --version");
|
|
1392
2167
|
program.command("rank", { isDefault: true, hidden: true }).description("Show leaderboard").option("-d, --days [days]", "Time window: 1 | 7 | 30 | all (default: today)").addOption(new Option("-p, --period [period]").hideHelp()).option("-g, --group <code>", "Group invite code").option("--global", "Show global public ranking").option("--cache", "Include cache tokens in count").option("--all", "Show all members including those with no activity").action(rankCommand);
|
|
1393
2168
|
program.command("init").description("Create a group and get started (first-time setup)").action(initCommand);
|
|
1394
2169
|
program.command("join").description("Join a group with a 6-letter invite code").argument("[invite-code]", "6-character invite code").action((code) => {
|
package/package.json
CHANGED