@poco-ai/tokenarena 0.1.2 → 0.1.5-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +587 -218
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/parsers/claude-code.ts
|
|
4
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4
5
|
import { homedir } from "os";
|
|
5
6
|
import { join as join2, sep as sep2 } from "path";
|
|
6
7
|
|
|
@@ -220,15 +221,23 @@ var TOOLS = [];
|
|
|
220
221
|
var parsers = /* @__PURE__ */ new Map();
|
|
221
222
|
function registerParser(parser) {
|
|
222
223
|
parsers.set(parser.tool.id, parser);
|
|
223
|
-
if (!TOOLS.find((
|
|
224
|
+
if (!TOOLS.find((tool) => tool.id === parser.tool.id)) {
|
|
224
225
|
TOOLS.push(parser.tool);
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
function getAllParsers() {
|
|
228
229
|
return Array.from(parsers.values());
|
|
229
230
|
}
|
|
231
|
+
function isToolInstalled(toolId) {
|
|
232
|
+
const parser = parsers.get(toolId);
|
|
233
|
+
if (parser?.isInstalled) {
|
|
234
|
+
return parser.isInstalled();
|
|
235
|
+
}
|
|
236
|
+
const tool = TOOLS.find((candidate) => candidate.id === toolId);
|
|
237
|
+
return tool ? existsSync2(tool.dataDir) : false;
|
|
238
|
+
}
|
|
230
239
|
function detectInstalledTools() {
|
|
231
|
-
return TOOLS.filter((
|
|
240
|
+
return TOOLS.filter((tool) => isToolInstalled(tool.id));
|
|
232
241
|
}
|
|
233
242
|
function getAllTools() {
|
|
234
243
|
return TOOLS;
|
|
@@ -240,14 +249,17 @@ var TOOL = {
|
|
|
240
249
|
name: "Claude Code",
|
|
241
250
|
dataDir: join2(homedir(), ".claude", "projects")
|
|
242
251
|
};
|
|
243
|
-
var
|
|
252
|
+
var TRANSCRIPT_DIRS = [
|
|
253
|
+
join2(homedir(), ".claude", "transcripts"),
|
|
254
|
+
join2(homedir(), ".claude", "sessions")
|
|
255
|
+
];
|
|
244
256
|
function extractProject(filePath) {
|
|
245
257
|
const projectsPrefix = TOOL.dataDir + sep2;
|
|
246
258
|
if (!filePath.startsWith(projectsPrefix)) return "unknown";
|
|
247
259
|
const relative = filePath.slice(projectsPrefix.length);
|
|
248
|
-
const
|
|
249
|
-
if (!
|
|
250
|
-
const parts =
|
|
260
|
+
const firstSegment = relative.split(sep2)[0];
|
|
261
|
+
if (!firstSegment) return "unknown";
|
|
262
|
+
const parts = firstSegment.split("-").filter(Boolean);
|
|
251
263
|
return parts.length > 0 ? parts[parts.length - 1] : "unknown";
|
|
252
264
|
}
|
|
253
265
|
var ClaudeCodeParser = class {
|
|
@@ -270,23 +282,24 @@ var ClaudeCodeParser = class {
|
|
|
270
282
|
const obj = JSON.parse(line);
|
|
271
283
|
const timestamp = obj.timestamp;
|
|
272
284
|
if (!timestamp) continue;
|
|
273
|
-
const
|
|
274
|
-
if (Number.isNaN(
|
|
285
|
+
const parsedTimestamp = new Date(timestamp);
|
|
286
|
+
if (Number.isNaN(parsedTimestamp.getTime())) continue;
|
|
275
287
|
if (obj.type === "user" || obj.type === "assistant") {
|
|
276
288
|
sessionEvents.push({
|
|
277
289
|
sessionId,
|
|
278
290
|
source: "claude-code",
|
|
279
291
|
project,
|
|
280
|
-
timestamp:
|
|
292
|
+
timestamp: parsedTimestamp,
|
|
281
293
|
role: obj.type === "user" ? "user" : "assistant"
|
|
282
294
|
});
|
|
283
295
|
}
|
|
284
296
|
if (obj.type !== "assistant") continue;
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
287
|
-
const usage =
|
|
288
|
-
if (usage.input_tokens == null && usage.output_tokens == null)
|
|
297
|
+
const message = obj.message;
|
|
298
|
+
if (!message?.usage) continue;
|
|
299
|
+
const usage = message.usage;
|
|
300
|
+
if (usage.input_tokens == null && usage.output_tokens == null) {
|
|
289
301
|
continue;
|
|
302
|
+
}
|
|
290
303
|
const uuid = obj.uuid;
|
|
291
304
|
if (uuid) {
|
|
292
305
|
if (seenUuids.has(uuid)) continue;
|
|
@@ -295,9 +308,9 @@ var ClaudeCodeParser = class {
|
|
|
295
308
|
entries.push({
|
|
296
309
|
sessionId,
|
|
297
310
|
source: "claude-code",
|
|
298
|
-
model:
|
|
311
|
+
model: message.model || "unknown",
|
|
299
312
|
project,
|
|
300
|
-
timestamp:
|
|
313
|
+
timestamp: parsedTimestamp,
|
|
301
314
|
inputTokens: usage.input_tokens || 0,
|
|
302
315
|
outputTokens: usage.output_tokens || 0,
|
|
303
316
|
reasoningTokens: 0,
|
|
@@ -307,30 +320,32 @@ var ClaudeCodeParser = class {
|
|
|
307
320
|
}
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
323
|
+
for (const transcriptsDir of TRANSCRIPT_DIRS) {
|
|
324
|
+
const transcriptFiles = findJsonlFiles(transcriptsDir);
|
|
325
|
+
for (const filePath of transcriptFiles) {
|
|
326
|
+
const sessionId = extractSessionId(filePath);
|
|
327
|
+
if (seenSessionIds.has(sessionId)) continue;
|
|
328
|
+
const content = readFileSafe(filePath);
|
|
329
|
+
if (!content) continue;
|
|
330
|
+
for (const line of content.split("\n")) {
|
|
331
|
+
if (!line.trim()) continue;
|
|
332
|
+
try {
|
|
333
|
+
const obj = JSON.parse(line);
|
|
334
|
+
const timestamp = obj.timestamp;
|
|
335
|
+
if (!timestamp) continue;
|
|
336
|
+
const parsedTimestamp = new Date(timestamp);
|
|
337
|
+
if (Number.isNaN(parsedTimestamp.getTime())) continue;
|
|
338
|
+
if (obj.type === "user" || obj.type === "assistant") {
|
|
339
|
+
sessionEvents.push({
|
|
340
|
+
sessionId,
|
|
341
|
+
source: "claude-code",
|
|
342
|
+
project: "unknown",
|
|
343
|
+
timestamp: parsedTimestamp,
|
|
344
|
+
role: obj.type === "user" ? "user" : "assistant"
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
332
348
|
}
|
|
333
|
-
} catch {
|
|
334
349
|
}
|
|
335
350
|
}
|
|
336
351
|
}
|
|
@@ -339,6 +354,9 @@ var ClaudeCodeParser = class {
|
|
|
339
354
|
sessions: extractSessions(sessionEvents, entries)
|
|
340
355
|
};
|
|
341
356
|
}
|
|
357
|
+
isInstalled() {
|
|
358
|
+
return existsSync3(TOOL.dataDir) || TRANSCRIPT_DIRS.some((dir) => existsSync3(dir));
|
|
359
|
+
}
|
|
342
360
|
};
|
|
343
361
|
registerParser(new ClaudeCodeParser());
|
|
344
362
|
|
|
@@ -350,6 +368,27 @@ var TOOL2 = {
|
|
|
350
368
|
name: "Codex CLI",
|
|
351
369
|
dataDir: join3(homedir2(), ".codex", "sessions")
|
|
352
370
|
};
|
|
371
|
+
function getPathLeaf(value) {
|
|
372
|
+
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
373
|
+
const leaf = normalized.split("/").filter(Boolean).pop();
|
|
374
|
+
return leaf || "unknown";
|
|
375
|
+
}
|
|
376
|
+
function resolveCodexProject(payload) {
|
|
377
|
+
if (!payload) {
|
|
378
|
+
return "unknown";
|
|
379
|
+
}
|
|
380
|
+
const repositoryUrl = payload.git?.repository_url;
|
|
381
|
+
if (repositoryUrl) {
|
|
382
|
+
const match = repositoryUrl.match(/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
383
|
+
if (match) {
|
|
384
|
+
return match[1];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (payload.cwd) {
|
|
388
|
+
return getPathLeaf(payload.cwd);
|
|
389
|
+
}
|
|
390
|
+
return "unknown";
|
|
391
|
+
}
|
|
353
392
|
var CodexParser = class {
|
|
354
393
|
tool = TOOL2;
|
|
355
394
|
async parse() {
|
|
@@ -368,17 +407,8 @@ var CodexParser = class {
|
|
|
368
407
|
if (!line.trim()) continue;
|
|
369
408
|
try {
|
|
370
409
|
const obj = JSON.parse(line);
|
|
371
|
-
if (obj.type === "session_meta"
|
|
372
|
-
|
|
373
|
-
if (meta.cwd) {
|
|
374
|
-
sessionProject = meta.cwd.split("/").pop() || "unknown";
|
|
375
|
-
}
|
|
376
|
-
if (meta.git?.repository_url) {
|
|
377
|
-
const match = meta.git.repository_url.match(
|
|
378
|
-
/([^/]+\/[^/]+?)(?:\.git)?$/
|
|
379
|
-
);
|
|
380
|
-
if (match) sessionProject = match[1];
|
|
381
|
-
}
|
|
410
|
+
if (obj.type === "session_meta") {
|
|
411
|
+
sessionProject = resolveCodexProject(obj.payload);
|
|
382
412
|
break;
|
|
383
413
|
}
|
|
384
414
|
} catch {
|
|
@@ -392,13 +422,13 @@ var CodexParser = class {
|
|
|
392
422
|
try {
|
|
393
423
|
const obj = JSON.parse(line);
|
|
394
424
|
if (obj.type === "turn_context" && obj.timestamp) {
|
|
395
|
-
const
|
|
396
|
-
if (!Number.isNaN(
|
|
425
|
+
const eventTimestamp = new Date(obj.timestamp);
|
|
426
|
+
if (!Number.isNaN(eventTimestamp.getTime())) {
|
|
397
427
|
sessionEvents.push({
|
|
398
428
|
sessionId: filePath,
|
|
399
429
|
source: "codex",
|
|
400
430
|
project: sessionProject,
|
|
401
|
-
timestamp:
|
|
431
|
+
timestamp: eventTimestamp,
|
|
402
432
|
role: "user"
|
|
403
433
|
});
|
|
404
434
|
}
|
|
@@ -409,8 +439,7 @@ var CodexParser = class {
|
|
|
409
439
|
}
|
|
410
440
|
if (obj.type !== "event_msg") continue;
|
|
411
441
|
const payload = obj.payload;
|
|
412
|
-
if (!payload) continue;
|
|
413
|
-
if (payload.type !== "token_count") continue;
|
|
442
|
+
if (!payload || payload.type !== "token_count") continue;
|
|
414
443
|
const info = payload.info;
|
|
415
444
|
if (!info) continue;
|
|
416
445
|
const timestamp = obj.timestamp ? new Date(obj.timestamp) : null;
|
|
@@ -467,7 +496,7 @@ var CodexParser = class {
|
|
|
467
496
|
registerParser(new CodexParser());
|
|
468
497
|
|
|
469
498
|
// src/parsers/gemini-cli.ts
|
|
470
|
-
import { existsSync as
|
|
499
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
471
500
|
import { homedir as homedir3 } from "os";
|
|
472
501
|
import { join as join4 } from "path";
|
|
473
502
|
var TOOL3 = {
|
|
@@ -477,12 +506,12 @@ var TOOL3 = {
|
|
|
477
506
|
};
|
|
478
507
|
function findSessionFiles(baseDir) {
|
|
479
508
|
const results = [];
|
|
480
|
-
if (!
|
|
509
|
+
if (!existsSync4(baseDir)) return results;
|
|
481
510
|
try {
|
|
482
511
|
for (const entry of readdirSync2(baseDir, { withFileTypes: true })) {
|
|
483
512
|
if (!entry.isDirectory()) continue;
|
|
484
513
|
const chatsDir = join4(baseDir, entry.name, "chats");
|
|
485
|
-
if (!
|
|
514
|
+
if (!existsSync4(chatsDir)) continue;
|
|
486
515
|
try {
|
|
487
516
|
for (const f of readdirSync2(chatsDir)) {
|
|
488
517
|
if (f.startsWith("session-") && f.endsWith(".json")) {
|
|
@@ -571,39 +600,51 @@ var GeminiCliParser = class {
|
|
|
571
600
|
registerParser(new GeminiCliParser());
|
|
572
601
|
|
|
573
602
|
// src/parsers/copilot-cli.ts
|
|
574
|
-
import { existsSync as
|
|
603
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
575
604
|
import { homedir as homedir4 } from "os";
|
|
576
|
-
import { basename as basename2, join as join5 } from "path";
|
|
605
|
+
import { basename as basename2, dirname, join as join5 } from "path";
|
|
606
|
+
var ROOT_DIR = join5(homedir4(), ".copilot");
|
|
577
607
|
var TOOL4 = {
|
|
578
608
|
id: "copilot-cli",
|
|
579
609
|
name: "GitHub Copilot CLI",
|
|
580
|
-
dataDir:
|
|
610
|
+
dataDir: ROOT_DIR
|
|
581
611
|
};
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
612
|
+
function collectEventFiles(dir, results, visited) {
|
|
613
|
+
if (!existsSync5(dir) || visited.has(dir)) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
visited.add(dir);
|
|
585
617
|
try {
|
|
586
|
-
for (const entry of readdirSync3(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
618
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
619
|
+
const fullPath = join5(dir, entry.name);
|
|
620
|
+
if (entry.isDirectory()) {
|
|
621
|
+
collectEventFiles(fullPath, results, visited);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (entry.isFile() && entry.name === "events.jsonl") {
|
|
625
|
+
results.push({
|
|
626
|
+
filePath: fullPath,
|
|
627
|
+
sessionId: basename2(dirname(fullPath))
|
|
628
|
+
});
|
|
591
629
|
}
|
|
592
630
|
}
|
|
593
631
|
} catch {
|
|
594
|
-
return results;
|
|
595
632
|
}
|
|
633
|
+
}
|
|
634
|
+
function findEventFiles(baseDir) {
|
|
635
|
+
const results = [];
|
|
636
|
+
collectEventFiles(baseDir, results, /* @__PURE__ */ new Set());
|
|
596
637
|
return results;
|
|
597
638
|
}
|
|
598
639
|
function getProjectFromContext(context) {
|
|
599
640
|
const projectPath = context?.gitRoot || context?.cwd;
|
|
600
641
|
if (!projectPath) return "unknown";
|
|
601
|
-
return basename2(projectPath) || "unknown";
|
|
642
|
+
return basename2(projectPath.replace(/[\\/]+$/, "")) || "unknown";
|
|
602
643
|
}
|
|
603
644
|
var CopilotCliParser = class {
|
|
604
645
|
tool = TOOL4;
|
|
605
646
|
async parse() {
|
|
606
|
-
const eventFiles = findEventFiles(
|
|
647
|
+
const eventFiles = findEventFiles(ROOT_DIR);
|
|
607
648
|
if (eventFiles.length === 0) {
|
|
608
649
|
return { buckets: [], sessions: [] };
|
|
609
650
|
}
|
|
@@ -644,8 +685,9 @@ var CopilotCliParser = class {
|
|
|
644
685
|
role: "assistant"
|
|
645
686
|
});
|
|
646
687
|
}
|
|
647
|
-
if (obj.type !== "session.shutdown" || !hasTimestamp || !timestamp)
|
|
688
|
+
if (obj.type !== "session.shutdown" || !hasTimestamp || !timestamp) {
|
|
648
689
|
continue;
|
|
690
|
+
}
|
|
649
691
|
const modelMetrics = obj.data?.modelMetrics || {};
|
|
650
692
|
for (const [model, metrics] of Object.entries(modelMetrics)) {
|
|
651
693
|
const usage = metrics?.usage;
|
|
@@ -677,27 +719,127 @@ var CopilotCliParser = class {
|
|
|
677
719
|
sessions: extractSessions(sessionEvents, entries)
|
|
678
720
|
};
|
|
679
721
|
}
|
|
722
|
+
isInstalled() {
|
|
723
|
+
return existsSync5(ROOT_DIR);
|
|
724
|
+
}
|
|
680
725
|
};
|
|
681
726
|
registerParser(new CopilotCliParser());
|
|
682
727
|
|
|
683
728
|
// src/parsers/opencode.ts
|
|
684
729
|
import { execFileSync } from "child_process";
|
|
685
|
-
import { existsSync as
|
|
730
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
686
731
|
import { homedir as homedir5 } from "os";
|
|
687
732
|
import { basename as basename3, join as join6 } from "path";
|
|
733
|
+
var DEFAULT_DATA_DIR = join6(homedir5(), ".local", "share", "opencode");
|
|
688
734
|
var TOOL5 = {
|
|
689
735
|
id: "opencode",
|
|
690
736
|
name: "OpenCode",
|
|
691
|
-
dataDir:
|
|
737
|
+
dataDir: DEFAULT_DATA_DIR
|
|
692
738
|
};
|
|
693
|
-
|
|
694
|
-
|
|
739
|
+
function getOpenCodeDataDirs(env = process.env) {
|
|
740
|
+
const dirs = [
|
|
741
|
+
env.TOKEN_ARENA_OPENCODE_DIR,
|
|
742
|
+
env.XDG_DATA_HOME ? join6(env.XDG_DATA_HOME, "opencode") : void 0,
|
|
743
|
+
DEFAULT_DATA_DIR,
|
|
744
|
+
env.LOCALAPPDATA ? join6(env.LOCALAPPDATA, "opencode") : void 0,
|
|
745
|
+
env.APPDATA ? join6(env.APPDATA, "opencode") : void 0
|
|
746
|
+
].filter((value) => Boolean(value));
|
|
747
|
+
return Array.from(new Set(dirs));
|
|
748
|
+
}
|
|
749
|
+
async function withSuppressedSqliteWarning(fn) {
|
|
750
|
+
const originalEmitWarning = process.emitWarning;
|
|
751
|
+
process.emitWarning = ((warning, ...args) => {
|
|
752
|
+
const warningName = typeof warning === "string" ? typeof args[0] === "string" ? args[0] : "" : warning.name;
|
|
753
|
+
const warningMessage = typeof warning === "string" ? warning : warning.message;
|
|
754
|
+
if (warningName === "ExperimentalWarning" && warningMessage.includes("SQLite")) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
originalEmitWarning.call(process, warning, ...args);
|
|
758
|
+
});
|
|
759
|
+
try {
|
|
760
|
+
return await fn();
|
|
761
|
+
} finally {
|
|
762
|
+
process.emitWarning = originalEmitWarning;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function readSqliteRowsWithBuiltin(dbPath, query) {
|
|
766
|
+
try {
|
|
767
|
+
return await withSuppressedSqliteWarning(async () => {
|
|
768
|
+
const sqliteModuleId = "node:sqlite";
|
|
769
|
+
const sqlite = await import(sqliteModuleId);
|
|
770
|
+
const db = new sqlite.DatabaseSync(dbPath);
|
|
771
|
+
try {
|
|
772
|
+
return db.prepare(query).all();
|
|
773
|
+
} finally {
|
|
774
|
+
db.close();
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
} catch (err) {
|
|
778
|
+
const error = err;
|
|
779
|
+
const message = err.message;
|
|
780
|
+
if (error.code === "ERR_UNKNOWN_BUILTIN_MODULE" || message.includes("node:sqlite")) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
throw err;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function readSqliteRowsWithCli(dbPath, query) {
|
|
787
|
+
const candidates = [
|
|
788
|
+
process.env.TOKEN_ARENA_SQLITE3,
|
|
789
|
+
"sqlite3",
|
|
790
|
+
"sqlite3.exe"
|
|
791
|
+
].filter((value) => Boolean(value));
|
|
792
|
+
let lastError = null;
|
|
793
|
+
for (const command of candidates) {
|
|
794
|
+
try {
|
|
795
|
+
const output = execFileSync(command, ["-json", dbPath, query], {
|
|
796
|
+
encoding: "utf-8",
|
|
797
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
798
|
+
timeout: 3e4,
|
|
799
|
+
windowsHide: true
|
|
800
|
+
}).trim();
|
|
801
|
+
if (!output || output === "[]") {
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
return JSON.parse(output);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
lastError = err;
|
|
807
|
+
const nodeError = err;
|
|
808
|
+
if (nodeError.status === 127 || nodeError.message?.includes("ENOENT")) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
throw err;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
throw new Error(
|
|
815
|
+
`sqlite3 CLI not found. Install sqlite3 or set TOKEN_ARENA_SQLITE3 to its full path. Last error: ${lastError?.message || "not found"}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
695
818
|
var OpenCodeParser = class {
|
|
696
819
|
tool = TOOL5;
|
|
697
820
|
async parse() {
|
|
698
|
-
|
|
821
|
+
const buckets = [];
|
|
822
|
+
const sessions = [];
|
|
823
|
+
for (const rootDir of getOpenCodeDataDirs()) {
|
|
824
|
+
const result = await this.parseRoot(rootDir);
|
|
825
|
+
if (result.buckets.length > 0) {
|
|
826
|
+
buckets.push(...result.buckets);
|
|
827
|
+
}
|
|
828
|
+
if (result.sessions.length > 0) {
|
|
829
|
+
sessions.push(...result.sessions);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return { buckets, sessions };
|
|
833
|
+
}
|
|
834
|
+
isInstalled() {
|
|
835
|
+
return getOpenCodeDataDirs().some((dir) => existsSync6(dir));
|
|
836
|
+
}
|
|
837
|
+
async parseRoot(rootDir) {
|
|
838
|
+
const dbPath = join6(rootDir, "opencode.db");
|
|
839
|
+
const messagesDir = join6(rootDir, "storage", "message");
|
|
840
|
+
if (existsSync6(dbPath)) {
|
|
699
841
|
try {
|
|
700
|
-
return this.parseFromSqlite();
|
|
842
|
+
return await this.parseFromSqlite(dbPath);
|
|
701
843
|
} catch (err) {
|
|
702
844
|
process.stderr.write(
|
|
703
845
|
`warn: opencode sqlite parse failed (${err.message}), trying legacy json...
|
|
@@ -705,9 +847,9 @@ var OpenCodeParser = class {
|
|
|
705
847
|
);
|
|
706
848
|
}
|
|
707
849
|
}
|
|
708
|
-
return this.parseFromJson();
|
|
850
|
+
return this.parseFromJson(messagesDir);
|
|
709
851
|
}
|
|
710
|
-
parseFromSqlite() {
|
|
852
|
+
async parseFromSqlite(dbPath) {
|
|
711
853
|
const query = `SELECT
|
|
712
854
|
session_id as sessionID,
|
|
713
855
|
json_extract(data, '$.role') as role,
|
|
@@ -716,29 +858,10 @@ var OpenCodeParser = class {
|
|
|
716
858
|
json_extract(data, '$.tokens') as tokens,
|
|
717
859
|
json_extract(data, '$.path.root') as rootPath
|
|
718
860
|
FROM message`;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
maxBuffer: 100 * 1024 * 1024,
|
|
724
|
-
timeout: 3e4
|
|
725
|
-
});
|
|
726
|
-
} catch (err) {
|
|
727
|
-
const nodeErr = err;
|
|
728
|
-
if (nodeErr.status === 127 || nodeErr.message?.includes("ENOENT")) {
|
|
729
|
-
throw new Error(
|
|
730
|
-
"sqlite3 CLI not found. Install sqlite3 to sync opencode data."
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
throw err;
|
|
734
|
-
}
|
|
735
|
-
output = output.trim();
|
|
736
|
-
if (!output || output === "[]") return { buckets: [], sessions: [] };
|
|
737
|
-
let rows;
|
|
738
|
-
try {
|
|
739
|
-
rows = JSON.parse(output);
|
|
740
|
-
} catch {
|
|
741
|
-
throw new Error("Failed to parse sqlite3 JSON output");
|
|
861
|
+
const builtinRows = await readSqliteRowsWithBuiltin(dbPath, query);
|
|
862
|
+
const rows = builtinRows ?? readSqliteRowsWithCli(dbPath, query);
|
|
863
|
+
if (rows.length === 0) {
|
|
864
|
+
return { buckets: [], sessions: [] };
|
|
742
865
|
}
|
|
743
866
|
const entries = [];
|
|
744
867
|
const sessionEvents = [];
|
|
@@ -780,27 +903,29 @@ var OpenCodeParser = class {
|
|
|
780
903
|
sessions: extractSessions(sessionEvents, entries)
|
|
781
904
|
};
|
|
782
905
|
}
|
|
783
|
-
parseFromJson() {
|
|
784
|
-
if (!
|
|
906
|
+
parseFromJson(messagesDir) {
|
|
907
|
+
if (!existsSync6(messagesDir)) return { buckets: [], sessions: [] };
|
|
785
908
|
const entries = [];
|
|
786
909
|
const sessionEvents = [];
|
|
787
910
|
let sessionDirs;
|
|
788
911
|
try {
|
|
789
|
-
sessionDirs = readdirSync4(
|
|
790
|
-
(
|
|
912
|
+
sessionDirs = readdirSync4(messagesDir, { withFileTypes: true }).filter(
|
|
913
|
+
(dirent) => dirent.isDirectory() && dirent.name.startsWith("ses_")
|
|
791
914
|
);
|
|
792
915
|
} catch {
|
|
793
916
|
return { buckets: [], sessions: [] };
|
|
794
917
|
}
|
|
795
918
|
for (const sessionDir of sessionDirs) {
|
|
796
|
-
const sessionPath = join6(
|
|
797
|
-
let
|
|
919
|
+
const sessionPath = join6(messagesDir, sessionDir.name);
|
|
920
|
+
let messageFiles;
|
|
798
921
|
try {
|
|
799
|
-
|
|
922
|
+
messageFiles = readdirSync4(sessionPath).filter(
|
|
923
|
+
(file) => file.endsWith(".json")
|
|
924
|
+
);
|
|
800
925
|
} catch {
|
|
801
926
|
continue;
|
|
802
927
|
}
|
|
803
|
-
for (const file of
|
|
928
|
+
for (const file of messageFiles) {
|
|
804
929
|
const filePath = join6(sessionPath, file);
|
|
805
930
|
let data;
|
|
806
931
|
try {
|
|
@@ -845,7 +970,7 @@ var OpenCodeParser = class {
|
|
|
845
970
|
registerParser(new OpenCodeParser());
|
|
846
971
|
|
|
847
972
|
// src/parsers/openclaw.ts
|
|
848
|
-
import { existsSync as
|
|
973
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync5 } from "fs";
|
|
849
974
|
import { homedir as homedir6 } from "os";
|
|
850
975
|
import { join as join7 } from "path";
|
|
851
976
|
var POSSIBLE_ROOTS = [
|
|
@@ -874,7 +999,7 @@ var OpenClawParser = class {
|
|
|
874
999
|
const sessionEvents = [];
|
|
875
1000
|
for (const root of POSSIBLE_ROOTS) {
|
|
876
1001
|
const agentsDir = join7(root, "agents");
|
|
877
|
-
if (!
|
|
1002
|
+
if (!existsSync7(agentsDir)) continue;
|
|
878
1003
|
let agentDirs;
|
|
879
1004
|
try {
|
|
880
1005
|
agentDirs = readdirSync5(agentsDir, { withFileTypes: true }).filter(
|
|
@@ -886,7 +1011,7 @@ var OpenClawParser = class {
|
|
|
886
1011
|
for (const agentDir of agentDirs) {
|
|
887
1012
|
const project = agentDir.name;
|
|
888
1013
|
const sessionsDir = join7(agentsDir, agentDir.name, "sessions");
|
|
889
|
-
if (!
|
|
1014
|
+
if (!existsSync7(sessionsDir)) continue;
|
|
890
1015
|
let files;
|
|
891
1016
|
try {
|
|
892
1017
|
files = readdirSync5(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -968,7 +1093,7 @@ var OpenClawParser = class {
|
|
|
968
1093
|
}
|
|
969
1094
|
/** Check if any of the possible roots exist */
|
|
970
1095
|
isInstalled() {
|
|
971
|
-
return POSSIBLE_ROOTS.some((root) =>
|
|
1096
|
+
return POSSIBLE_ROOTS.some((root) => existsSync7(join7(root, "agents")));
|
|
972
1097
|
}
|
|
973
1098
|
};
|
|
974
1099
|
registerParser(new OpenClawParser());
|
|
@@ -978,13 +1103,33 @@ import { Command, Option } from "commander";
|
|
|
978
1103
|
|
|
979
1104
|
// src/infrastructure/config/manager.ts
|
|
980
1105
|
import { randomUUID } from "crypto";
|
|
981
|
-
import {
|
|
1106
|
+
import {
|
|
1107
|
+
existsSync as existsSync8,
|
|
1108
|
+
mkdirSync,
|
|
1109
|
+
readFileSync as readFileSync6,
|
|
1110
|
+
unlinkSync,
|
|
1111
|
+
writeFileSync
|
|
1112
|
+
} from "fs";
|
|
1113
|
+
import { join as join9 } from "path";
|
|
1114
|
+
|
|
1115
|
+
// src/infrastructure/xdg.ts
|
|
982
1116
|
import { homedir as homedir7 } from "os";
|
|
983
1117
|
import { join as join8 } from "path";
|
|
984
|
-
|
|
1118
|
+
function getConfigHome() {
|
|
1119
|
+
return process.env.XDG_CONFIG_HOME || join8(homedir7(), ".config");
|
|
1120
|
+
}
|
|
1121
|
+
function getStateHome() {
|
|
1122
|
+
return process.env.XDG_STATE_HOME || join8(homedir7(), ".local", "state");
|
|
1123
|
+
}
|
|
1124
|
+
function getRuntimeDir() {
|
|
1125
|
+
return process.env.XDG_RUNTIME_DIR || getStateHome();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/infrastructure/config/manager.ts
|
|
1129
|
+
var CONFIG_DIR = join9(getConfigHome(), "tokenarena");
|
|
985
1130
|
var isDev = process.env.TOKEN_ARENA_DEV === "1";
|
|
986
|
-
var CONFIG_FILE =
|
|
987
|
-
var DEFAULT_API_URL = "
|
|
1131
|
+
var CONFIG_FILE = join9(CONFIG_DIR, isDev ? "config.dev.json" : "config.json");
|
|
1132
|
+
var DEFAULT_API_URL = "https://token.poco-ai.com";
|
|
988
1133
|
function getConfigPath() {
|
|
989
1134
|
return CONFIG_FILE;
|
|
990
1135
|
}
|
|
@@ -992,7 +1137,7 @@ function getConfigDir() {
|
|
|
992
1137
|
return CONFIG_DIR;
|
|
993
1138
|
}
|
|
994
1139
|
function loadConfig() {
|
|
995
|
-
if (!
|
|
1140
|
+
if (!existsSync8(CONFIG_FILE)) return null;
|
|
996
1141
|
try {
|
|
997
1142
|
const raw = readFileSync6(CONFIG_FILE, "utf-8");
|
|
998
1143
|
const config = JSON.parse(raw);
|
|
@@ -1009,6 +1154,11 @@ function saveConfig(config) {
|
|
|
1009
1154
|
writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
|
|
1010
1155
|
`, "utf-8");
|
|
1011
1156
|
}
|
|
1157
|
+
function deleteConfig() {
|
|
1158
|
+
if (existsSync8(CONFIG_FILE)) {
|
|
1159
|
+
unlinkSync(CONFIG_FILE);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1012
1162
|
function getOrCreateDeviceId(config) {
|
|
1013
1163
|
if (config.deviceId) return config.deviceId;
|
|
1014
1164
|
const next = randomUUID();
|
|
@@ -1016,7 +1166,7 @@ function getOrCreateDeviceId(config) {
|
|
|
1016
1166
|
return next;
|
|
1017
1167
|
}
|
|
1018
1168
|
function validateApiKey(key) {
|
|
1019
|
-
return key.startsWith("
|
|
1169
|
+
return key.startsWith("ta_");
|
|
1020
1170
|
}
|
|
1021
1171
|
function getDefaultApiUrl() {
|
|
1022
1172
|
return process.env.TOKEN_ARENA_API_URL || DEFAULT_API_URL;
|
|
@@ -1082,7 +1232,8 @@ function handleConfig(args) {
|
|
|
1082
1232
|
if (!config || !(key in config)) {
|
|
1083
1233
|
process.exit(0);
|
|
1084
1234
|
}
|
|
1085
|
-
|
|
1235
|
+
const record = config;
|
|
1236
|
+
console.log(record[key] ?? "");
|
|
1086
1237
|
break;
|
|
1087
1238
|
}
|
|
1088
1239
|
case "set": {
|
|
@@ -1099,7 +1250,7 @@ function handleConfig(args) {
|
|
|
1099
1250
|
}
|
|
1100
1251
|
const config = loadConfig() || {
|
|
1101
1252
|
apiKey: "",
|
|
1102
|
-
apiUrl: "
|
|
1253
|
+
apiUrl: "https://token.poco-ai.com"
|
|
1103
1254
|
};
|
|
1104
1255
|
if (key === "syncInterval") {
|
|
1105
1256
|
value = parseInt(value, 10);
|
|
@@ -1108,7 +1259,8 @@ function handleConfig(args) {
|
|
|
1108
1259
|
process.exit(1);
|
|
1109
1260
|
}
|
|
1110
1261
|
}
|
|
1111
|
-
|
|
1262
|
+
const record = config;
|
|
1263
|
+
record[key] = value;
|
|
1112
1264
|
saveConfig(config);
|
|
1113
1265
|
logger.info(`Set ${key} = ${value}`);
|
|
1114
1266
|
break;
|
|
@@ -1368,7 +1520,7 @@ var ApiClient = class {
|
|
|
1368
1520
|
// src/infrastructure/runtime/lock.ts
|
|
1369
1521
|
import {
|
|
1370
1522
|
closeSync,
|
|
1371
|
-
existsSync as
|
|
1523
|
+
existsSync as existsSync9,
|
|
1372
1524
|
openSync,
|
|
1373
1525
|
readFileSync as readFileSync7,
|
|
1374
1526
|
rmSync,
|
|
@@ -1377,18 +1529,23 @@ import {
|
|
|
1377
1529
|
|
|
1378
1530
|
// src/infrastructure/runtime/paths.ts
|
|
1379
1531
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1380
|
-
import { join as
|
|
1381
|
-
|
|
1382
|
-
|
|
1532
|
+
import { join as join10 } from "path";
|
|
1533
|
+
var APP_NAME = "tokenarena";
|
|
1534
|
+
function getRuntimeDirPath() {
|
|
1535
|
+
return join10(getRuntimeDir(), APP_NAME);
|
|
1536
|
+
}
|
|
1537
|
+
function getStateDir() {
|
|
1538
|
+
return join10(getStateHome(), APP_NAME);
|
|
1383
1539
|
}
|
|
1384
1540
|
function getSyncLockPath() {
|
|
1385
|
-
return
|
|
1541
|
+
return join10(getRuntimeDirPath(), "sync.lock");
|
|
1386
1542
|
}
|
|
1387
1543
|
function getSyncStatePath() {
|
|
1388
|
-
return
|
|
1544
|
+
return join10(getStateDir(), "status.json");
|
|
1389
1545
|
}
|
|
1390
|
-
function
|
|
1391
|
-
mkdirSync2(
|
|
1546
|
+
function ensureAppDirs() {
|
|
1547
|
+
mkdirSync2(getRuntimeDirPath(), { recursive: true });
|
|
1548
|
+
mkdirSync2(getStateDir(), { recursive: true });
|
|
1392
1549
|
}
|
|
1393
1550
|
|
|
1394
1551
|
// src/infrastructure/runtime/lock.ts
|
|
@@ -1402,7 +1559,7 @@ function isProcessAlive(pid) {
|
|
|
1402
1559
|
}
|
|
1403
1560
|
}
|
|
1404
1561
|
function readLockMetadata(lockPath) {
|
|
1405
|
-
if (!
|
|
1562
|
+
if (!existsSync9(lockPath)) {
|
|
1406
1563
|
return null;
|
|
1407
1564
|
}
|
|
1408
1565
|
try {
|
|
@@ -1422,7 +1579,7 @@ function removeStaleLock(lockPath) {
|
|
|
1422
1579
|
}
|
|
1423
1580
|
}
|
|
1424
1581
|
function tryAcquireSyncLock(source) {
|
|
1425
|
-
|
|
1582
|
+
ensureAppDirs();
|
|
1426
1583
|
const lockPath = getSyncLockPath();
|
|
1427
1584
|
try {
|
|
1428
1585
|
const fd = openSync(lockPath, "wx");
|
|
@@ -1476,13 +1633,13 @@ function describeExistingSyncLock() {
|
|
|
1476
1633
|
}
|
|
1477
1634
|
|
|
1478
1635
|
// src/infrastructure/runtime/state.ts
|
|
1479
|
-
import { existsSync as
|
|
1636
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
1480
1637
|
function getDefaultState() {
|
|
1481
1638
|
return { status: "idle" };
|
|
1482
1639
|
}
|
|
1483
1640
|
function loadSyncState() {
|
|
1484
1641
|
const path = getSyncStatePath();
|
|
1485
|
-
if (!
|
|
1642
|
+
if (!existsSync10(path)) {
|
|
1486
1643
|
return getDefaultState();
|
|
1487
1644
|
}
|
|
1488
1645
|
try {
|
|
@@ -1495,7 +1652,7 @@ function loadSyncState() {
|
|
|
1495
1652
|
}
|
|
1496
1653
|
}
|
|
1497
1654
|
function saveSyncState(next) {
|
|
1498
|
-
|
|
1655
|
+
ensureAppDirs();
|
|
1499
1656
|
writeFileSync3(
|
|
1500
1657
|
getSyncStatePath(),
|
|
1501
1658
|
`${JSON.stringify(next, null, 2)}
|
|
@@ -1708,7 +1865,7 @@ async function runSync(config, opts = {}) {
|
|
|
1708
1865
|
logger.info(` ${p.source}: ${parts.join(", ")}`);
|
|
1709
1866
|
}
|
|
1710
1867
|
}
|
|
1711
|
-
const apiUrl = config.apiUrl || "
|
|
1868
|
+
const apiUrl = config.apiUrl || "https://token.poco-ai.com";
|
|
1712
1869
|
const apiClient = new ApiClient(apiUrl, config.apiKey);
|
|
1713
1870
|
let settings;
|
|
1714
1871
|
try {
|
|
@@ -1897,11 +2054,15 @@ async function runDaemon(opts = {}) {
|
|
|
1897
2054
|
}
|
|
1898
2055
|
|
|
1899
2056
|
// src/commands/init.ts
|
|
1900
|
-
import {
|
|
1901
|
-
import { existsSync as
|
|
1902
|
-
import { appendFile, readFile } from "fs/promises";
|
|
2057
|
+
import { execFileSync as execFileSync2, spawn } from "child_process";
|
|
2058
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2059
|
+
import { appendFile, mkdir, readFile } from "fs/promises";
|
|
1903
2060
|
import { homedir as homedir8, platform } from "os";
|
|
2061
|
+
import { dirname as dirname2, join as join11, posix, win32 } from "path";
|
|
1904
2062
|
import { createInterface } from "readline";
|
|
2063
|
+
function joinForPlatform(currentPlatform, ...parts) {
|
|
2064
|
+
return currentPlatform === "win32" ? win32.join(...parts) : posix.join(...parts);
|
|
2065
|
+
}
|
|
1905
2066
|
function prompt(question) {
|
|
1906
2067
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1907
2068
|
return new Promise((resolve2) => {
|
|
@@ -1911,15 +2072,142 @@ function prompt(question) {
|
|
|
1911
2072
|
});
|
|
1912
2073
|
});
|
|
1913
2074
|
}
|
|
2075
|
+
function basenameLikeShell(input) {
|
|
2076
|
+
return input.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input;
|
|
2077
|
+
}
|
|
2078
|
+
function getBrowserLaunchCommand(url, currentPlatform = platform()) {
|
|
2079
|
+
switch (currentPlatform) {
|
|
2080
|
+
case "darwin":
|
|
2081
|
+
return {
|
|
2082
|
+
command: "open",
|
|
2083
|
+
args: [url]
|
|
2084
|
+
};
|
|
2085
|
+
case "win32":
|
|
2086
|
+
return {
|
|
2087
|
+
command: "cmd.exe",
|
|
2088
|
+
args: ["/d", "/s", "/c", "start", "", url]
|
|
2089
|
+
};
|
|
2090
|
+
default:
|
|
2091
|
+
return {
|
|
2092
|
+
command: "xdg-open",
|
|
2093
|
+
args: [url]
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
1914
2097
|
function openBrowser(url) {
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2098
|
+
const { command, args } = getBrowserLaunchCommand(url);
|
|
2099
|
+
try {
|
|
2100
|
+
const child = spawn(command, args, {
|
|
2101
|
+
detached: true,
|
|
2102
|
+
stdio: "ignore",
|
|
2103
|
+
windowsHide: true
|
|
2104
|
+
});
|
|
2105
|
+
child.on("error", () => {
|
|
2106
|
+
});
|
|
2107
|
+
child.unref();
|
|
2108
|
+
} catch {
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
function resolvePowerShellProfilePath() {
|
|
2112
|
+
const systemRoot = process.env.SYSTEMROOT || "C:\\Windows";
|
|
2113
|
+
const candidates = [
|
|
2114
|
+
"pwsh.exe",
|
|
2115
|
+
join11(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
2116
|
+
];
|
|
2117
|
+
for (const command of candidates) {
|
|
2118
|
+
try {
|
|
2119
|
+
const output = execFileSync2(
|
|
2120
|
+
command,
|
|
2121
|
+
[
|
|
2122
|
+
"-NoLogo",
|
|
2123
|
+
"-NoProfile",
|
|
2124
|
+
"-Command",
|
|
2125
|
+
"$PROFILE.CurrentUserCurrentHost"
|
|
2126
|
+
],
|
|
2127
|
+
{
|
|
2128
|
+
encoding: "utf-8",
|
|
2129
|
+
timeout: 3e3,
|
|
2130
|
+
windowsHide: true
|
|
2131
|
+
}
|
|
2132
|
+
).trim();
|
|
2133
|
+
if (output) {
|
|
2134
|
+
return output;
|
|
2135
|
+
}
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
function resolveShellAliasSetup(options = {}) {
|
|
2142
|
+
const currentPlatform = options.currentPlatform ?? platform();
|
|
2143
|
+
const env = options.env ?? process.env;
|
|
2144
|
+
const homeDir = options.homeDir ?? homedir8();
|
|
2145
|
+
const pathExists = options.exists ?? existsSync11;
|
|
2146
|
+
const shellFromEnv = env.SHELL ? basenameLikeShell(env.SHELL).toLowerCase() : "";
|
|
2147
|
+
const shellName = shellFromEnv || (currentPlatform === "win32" ? "powershell" : "");
|
|
2148
|
+
const aliasName = "ta";
|
|
2149
|
+
switch (shellName) {
|
|
2150
|
+
case "zsh":
|
|
2151
|
+
return {
|
|
2152
|
+
aliasLine: `alias ${aliasName}="tokenarena"`,
|
|
2153
|
+
aliasPatterns: [`alias ${aliasName}=`, `function ${aliasName}`],
|
|
2154
|
+
configFile: joinForPlatform(currentPlatform, homeDir, ".zshrc"),
|
|
2155
|
+
shellLabel: "zsh",
|
|
2156
|
+
sourceHint: "source ~/.zshrc"
|
|
2157
|
+
};
|
|
2158
|
+
case "bash": {
|
|
2159
|
+
const bashProfile = joinForPlatform(
|
|
2160
|
+
currentPlatform,
|
|
2161
|
+
homeDir,
|
|
2162
|
+
".bash_profile"
|
|
2163
|
+
);
|
|
2164
|
+
const useBashProfile = currentPlatform === "darwin" && pathExists(bashProfile);
|
|
2165
|
+
return {
|
|
2166
|
+
aliasLine: `alias ${aliasName}="tokenarena"`,
|
|
2167
|
+
aliasPatterns: [`alias ${aliasName}=`, `function ${aliasName}`],
|
|
2168
|
+
configFile: useBashProfile ? bashProfile : joinForPlatform(currentPlatform, homeDir, ".bashrc"),
|
|
2169
|
+
shellLabel: "bash",
|
|
2170
|
+
sourceHint: useBashProfile ? "source ~/.bash_profile" : "source ~/.bashrc"
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
case "fish":
|
|
2174
|
+
return {
|
|
2175
|
+
aliasLine: `alias ${aliasName} "tokenarena"`,
|
|
2176
|
+
aliasPatterns: [`alias ${aliasName}`, `function ${aliasName}`],
|
|
2177
|
+
configFile: joinForPlatform(
|
|
2178
|
+
currentPlatform,
|
|
2179
|
+
homeDir,
|
|
2180
|
+
".config",
|
|
2181
|
+
"fish",
|
|
2182
|
+
"config.fish"
|
|
2183
|
+
),
|
|
2184
|
+
shellLabel: "fish",
|
|
2185
|
+
sourceHint: "source ~/.config/fish/config.fish"
|
|
2186
|
+
};
|
|
2187
|
+
case "powershell": {
|
|
2188
|
+
const configFile = options.resolvePowerShellProfilePath?.() || resolvePowerShellProfilePath() || joinForPlatform(
|
|
2189
|
+
currentPlatform,
|
|
2190
|
+
homeDir,
|
|
2191
|
+
"Documents",
|
|
2192
|
+
"PowerShell",
|
|
2193
|
+
"Microsoft.PowerShell_profile.ps1"
|
|
2194
|
+
);
|
|
2195
|
+
return {
|
|
2196
|
+
aliasLine: `Set-Alias -Name ${aliasName} -Value tokenarena`,
|
|
2197
|
+
aliasPatterns: [
|
|
2198
|
+
`set-alias -name ${aliasName}`,
|
|
2199
|
+
`set-alias ${aliasName}`,
|
|
2200
|
+
`new-alias ${aliasName}`,
|
|
2201
|
+
`function ${aliasName}`
|
|
2202
|
+
],
|
|
2203
|
+
configFile,
|
|
2204
|
+
shellLabel: "PowerShell",
|
|
2205
|
+
sourceHint: ". $PROFILE"
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
default:
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
1923
2211
|
}
|
|
1924
2212
|
async function runInit(opts = {}) {
|
|
1925
2213
|
logger.info("\n tokenarena - Token Usage Tracker\n");
|
|
@@ -1939,7 +2227,7 @@ async function runInit(opts = {}) {
|
|
|
1939
2227
|
while (true) {
|
|
1940
2228
|
apiKey = await prompt("Paste your API key: ");
|
|
1941
2229
|
if (validateApiKey(apiKey)) break;
|
|
1942
|
-
logger.info('Invalid key
|
|
2230
|
+
logger.info('Invalid key - must start with "ta_". Try again.');
|
|
1943
2231
|
}
|
|
1944
2232
|
logger.info(`
|
|
1945
2233
|
Verifying key ${apiKey.slice(0, 8)}...`);
|
|
@@ -1971,7 +2259,7 @@ Verifying key ${apiKey.slice(0, 8)}...`);
|
|
|
1971
2259
|
logger.info(`Device registered: ${deviceId.slice(0, 8)}...`);
|
|
1972
2260
|
const tools = getDetectedTools();
|
|
1973
2261
|
if (tools.length > 0) {
|
|
1974
|
-
logger.info(`Detected tools: ${tools.map((
|
|
2262
|
+
logger.info(`Detected tools: ${tools.map((tool) => tool.name).join(", ")}`);
|
|
1975
2263
|
} else {
|
|
1976
2264
|
logger.info("No AI coding tools detected. Install one and re-run init.");
|
|
1977
2265
|
}
|
|
@@ -1982,85 +2270,55 @@ Setup complete! View your dashboard at: ${apiUrl}/usage`);
|
|
|
1982
2270
|
await setupShellAlias();
|
|
1983
2271
|
}
|
|
1984
2272
|
async function setupShellAlias() {
|
|
1985
|
-
const
|
|
1986
|
-
if (!
|
|
2273
|
+
const setup = resolveShellAliasSetup();
|
|
2274
|
+
if (!setup) {
|
|
1987
2275
|
return;
|
|
1988
2276
|
}
|
|
1989
|
-
const shellName = shell.split("/").pop() ?? "";
|
|
1990
|
-
const aliasName = "ta";
|
|
1991
|
-
let configFile;
|
|
1992
|
-
let aliasLine;
|
|
1993
|
-
let sourceHint;
|
|
1994
|
-
switch (shellName) {
|
|
1995
|
-
case "zsh":
|
|
1996
|
-
configFile = `${homedir8()}/.zshrc`;
|
|
1997
|
-
aliasLine = `alias ${aliasName}="tokenarena"`;
|
|
1998
|
-
sourceHint = "source ~/.zshrc";
|
|
1999
|
-
break;
|
|
2000
|
-
case "bash":
|
|
2001
|
-
if (platform() === "darwin" && existsSync10(`${homedir8()}/.bash_profile`)) {
|
|
2002
|
-
configFile = `${homedir8()}/.bash_profile`;
|
|
2003
|
-
} else {
|
|
2004
|
-
configFile = `${homedir8()}/.bashrc`;
|
|
2005
|
-
}
|
|
2006
|
-
aliasLine = `alias ${aliasName}="tokenarena"`;
|
|
2007
|
-
sourceHint = `source ${configFile}`;
|
|
2008
|
-
break;
|
|
2009
|
-
case "fish":
|
|
2010
|
-
configFile = `${homedir8()}/.config/fish/config.fish`;
|
|
2011
|
-
aliasLine = `alias ${aliasName} "tokenarena"`;
|
|
2012
|
-
sourceHint = "source ~/.config/fish/config.fish";
|
|
2013
|
-
break;
|
|
2014
|
-
default:
|
|
2015
|
-
return;
|
|
2016
|
-
}
|
|
2017
2277
|
const answer = await prompt(
|
|
2018
2278
|
`
|
|
2019
|
-
Set up
|
|
2279
|
+
Set up ${setup.shellLabel} alias 'ta' for 'tokenarena'? (Y/n) `
|
|
2020
2280
|
);
|
|
2021
2281
|
if (answer.toLowerCase() === "n") {
|
|
2022
2282
|
return;
|
|
2023
2283
|
}
|
|
2024
2284
|
try {
|
|
2285
|
+
await mkdir(dirname2(setup.configFile), { recursive: true });
|
|
2025
2286
|
let existingContent = "";
|
|
2026
|
-
if (
|
|
2027
|
-
existingContent = await readFile(configFile, "utf-8");
|
|
2287
|
+
if (existsSync11(setup.configFile)) {
|
|
2288
|
+
existingContent = await readFile(setup.configFile, "utf-8");
|
|
2028
2289
|
}
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
`alias ${aliasName}=`
|
|
2033
|
-
];
|
|
2034
|
-
const aliasExists = aliasPatterns.some(
|
|
2035
|
-
(pattern) => existingContent.includes(pattern)
|
|
2290
|
+
const normalizedContent = existingContent.toLowerCase();
|
|
2291
|
+
const aliasExists = setup.aliasPatterns.some(
|
|
2292
|
+
(pattern) => normalizedContent.includes(pattern.toLowerCase())
|
|
2036
2293
|
);
|
|
2037
2294
|
if (aliasExists) {
|
|
2038
2295
|
logger.info(
|
|
2039
2296
|
`
|
|
2040
|
-
Alias '
|
|
2297
|
+
Alias 'ta' already exists in ${setup.configFile}. Skipping.`
|
|
2041
2298
|
);
|
|
2042
2299
|
return;
|
|
2043
2300
|
}
|
|
2044
2301
|
const aliasWithComment = `
|
|
2045
2302
|
# TokenArena alias
|
|
2046
|
-
${aliasLine}
|
|
2303
|
+
${setup.aliasLine}
|
|
2047
2304
|
`;
|
|
2048
|
-
await appendFile(configFile, aliasWithComment);
|
|
2305
|
+
await appendFile(setup.configFile, aliasWithComment, "utf-8");
|
|
2049
2306
|
logger.info(`
|
|
2050
|
-
Added alias to ${configFile}`);
|
|
2051
|
-
logger.info(
|
|
2052
|
-
|
|
2307
|
+
Added alias to ${setup.configFile}`);
|
|
2308
|
+
logger.info(
|
|
2309
|
+
` Run '${setup.sourceHint}' or restart your terminal to use it.`
|
|
2310
|
+
);
|
|
2311
|
+
logger.info(" Then you can use: ta sync");
|
|
2053
2312
|
} catch (err) {
|
|
2054
2313
|
logger.info(
|
|
2055
2314
|
`
|
|
2056
|
-
Could not write to ${configFile}: ${err.message}`
|
|
2315
|
+
Could not write to ${setup.configFile}: ${err.message}`
|
|
2057
2316
|
);
|
|
2058
|
-
logger.info(` Add this line manually: ${aliasLine}`);
|
|
2317
|
+
logger.info(` Add this line manually: ${setup.aliasLine}`);
|
|
2059
2318
|
}
|
|
2060
2319
|
}
|
|
2061
2320
|
|
|
2062
2321
|
// src/commands/status.ts
|
|
2063
|
-
import { existsSync as existsSync11 } from "fs";
|
|
2064
2322
|
function formatMaybe(value) {
|
|
2065
2323
|
return value || "(never)";
|
|
2066
2324
|
}
|
|
@@ -2069,12 +2327,11 @@ async function runStatus() {
|
|
|
2069
2327
|
logger.info("\ntokenarena status\n");
|
|
2070
2328
|
if (!config?.apiKey) {
|
|
2071
2329
|
logger.info(" Config: not configured");
|
|
2072
|
-
logger.info(
|
|
2073
|
-
`);
|
|
2330
|
+
logger.info(" Run `tokenarena init` to set up.\n");
|
|
2074
2331
|
} else {
|
|
2075
2332
|
logger.info(` Config: ${getConfigPath()}`);
|
|
2076
2333
|
logger.info(` API key: ${config.apiKey.slice(0, 8)}...`);
|
|
2077
|
-
logger.info(` API URL: ${config.apiUrl || "
|
|
2334
|
+
logger.info(` API URL: ${config.apiUrl || "https://token.poco-ai.com"}`);
|
|
2078
2335
|
if (config.syncInterval) {
|
|
2079
2336
|
logger.info(
|
|
2080
2337
|
` Sync interval: ${Math.round(config.syncInterval / 6e4)}m`
|
|
@@ -2093,7 +2350,7 @@ async function runStatus() {
|
|
|
2093
2350
|
}
|
|
2094
2351
|
logger.info(" All supported tools:");
|
|
2095
2352
|
for (const tool of getAllTools()) {
|
|
2096
|
-
const installed =
|
|
2353
|
+
const installed = isToolInstalled(tool.id) ? "installed" : "not found";
|
|
2097
2354
|
logger.info(` ${tool.name}: ${installed}`);
|
|
2098
2355
|
}
|
|
2099
2356
|
const syncState = loadSyncState();
|
|
@@ -2128,9 +2385,119 @@ async function runSyncCommand(opts = {}) {
|
|
|
2128
2385
|
});
|
|
2129
2386
|
}
|
|
2130
2387
|
|
|
2388
|
+
// src/commands/uninstall.ts
|
|
2389
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2390
|
+
import { homedir as homedir9, platform as platform2 } from "os";
|
|
2391
|
+
import { createInterface as createInterface2 } from "readline";
|
|
2392
|
+
function prompt2(question) {
|
|
2393
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2394
|
+
return new Promise((resolve2) => {
|
|
2395
|
+
rl.question(question, (answer) => {
|
|
2396
|
+
rl.close();
|
|
2397
|
+
resolve2(answer.trim());
|
|
2398
|
+
});
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
function removeShellAlias() {
|
|
2402
|
+
const shell = process.env.SHELL;
|
|
2403
|
+
if (!shell) return;
|
|
2404
|
+
const shellName = shell.split("/").pop() ?? "";
|
|
2405
|
+
const aliasName = "ta";
|
|
2406
|
+
let configFile;
|
|
2407
|
+
switch (shellName) {
|
|
2408
|
+
case "zsh":
|
|
2409
|
+
configFile = `${homedir9()}/.zshrc`;
|
|
2410
|
+
break;
|
|
2411
|
+
case "bash":
|
|
2412
|
+
if (platform2() === "darwin" && existsSync12(`${homedir9()}/.bash_profile`)) {
|
|
2413
|
+
configFile = `${homedir9()}/.bash_profile`;
|
|
2414
|
+
} else {
|
|
2415
|
+
configFile = `${homedir9()}/.bashrc`;
|
|
2416
|
+
}
|
|
2417
|
+
break;
|
|
2418
|
+
case "fish":
|
|
2419
|
+
configFile = `${homedir9()}/.config/fish/config.fish`;
|
|
2420
|
+
break;
|
|
2421
|
+
default:
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (!existsSync12(configFile)) return;
|
|
2425
|
+
try {
|
|
2426
|
+
let content = readFileSync9(configFile, "utf-8");
|
|
2427
|
+
const aliasPatterns = [
|
|
2428
|
+
// zsh / bash format: alias ta="tokenarena"
|
|
2429
|
+
new RegExp(
|
|
2430
|
+
`\\n?#\\s*TokenArena alias\\s*\\n\\s*alias\\s+${aliasName}\\s*=\\s*"tokenarena"\\s*\\n?`,
|
|
2431
|
+
"g"
|
|
2432
|
+
),
|
|
2433
|
+
// fish format: alias ta "tokenarena"
|
|
2434
|
+
new RegExp(
|
|
2435
|
+
`\\n?#\\s*TokenArena alias\\s*\\n\\s*alias\\s+${aliasName}\\s+"tokenarena"\\s*\\n?`,
|
|
2436
|
+
"g"
|
|
2437
|
+
),
|
|
2438
|
+
// Loose match for any ta alias line
|
|
2439
|
+
new RegExp(`\\n?alias\\s+${aliasName}\\s*=\\s*"tokenarena"\\s*\\n?`, "g"),
|
|
2440
|
+
new RegExp(`\\n?alias\\s+${aliasName}\\s+"tokenarena"\\s*\\n?`, "g")
|
|
2441
|
+
];
|
|
2442
|
+
for (const pattern of aliasPatterns) {
|
|
2443
|
+
const next = content.replace(pattern, "\n");
|
|
2444
|
+
if (next !== content) {
|
|
2445
|
+
content = next;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
writeFileSync4(configFile, content, "utf-8");
|
|
2449
|
+
logger.info(`Removed shell alias from ${configFile}`);
|
|
2450
|
+
} catch (err) {
|
|
2451
|
+
logger.warn(
|
|
2452
|
+
`Could not update ${configFile}: ${err.message}. Please remove the alias manually.`
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
async function runUninstall() {
|
|
2457
|
+
const configPath = getConfigPath();
|
|
2458
|
+
const configDir = getConfigDir();
|
|
2459
|
+
if (!existsSync12(configPath)) {
|
|
2460
|
+
logger.info("No configuration found. Nothing to uninstall.");
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const config = loadConfig();
|
|
2464
|
+
if (config?.apiKey) {
|
|
2465
|
+
logger.info(`API key: ${config.apiKey.slice(0, 8)}...`);
|
|
2466
|
+
}
|
|
2467
|
+
logger.info(`Config directory: ${configDir}`);
|
|
2468
|
+
const answer = await prompt2(
|
|
2469
|
+
"\nAre you sure you want to uninstall? This will delete all local data. (y/N) "
|
|
2470
|
+
);
|
|
2471
|
+
if (answer.toLowerCase() !== "y") {
|
|
2472
|
+
logger.info("Cancelled.");
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
deleteConfig();
|
|
2476
|
+
logger.info("Deleted configuration file.");
|
|
2477
|
+
if (existsSync12(configDir)) {
|
|
2478
|
+
try {
|
|
2479
|
+
rmSync2(configDir, { recursive: false, force: true });
|
|
2480
|
+
logger.info("Deleted config directory.");
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
const stateDir = getStateDir();
|
|
2485
|
+
if (existsSync12(stateDir)) {
|
|
2486
|
+
rmSync2(stateDir, { recursive: true, force: true });
|
|
2487
|
+
logger.info("Deleted state data.");
|
|
2488
|
+
}
|
|
2489
|
+
const runtimeDir = getRuntimeDirPath();
|
|
2490
|
+
if (existsSync12(runtimeDir)) {
|
|
2491
|
+
rmSync2(runtimeDir, { recursive: true, force: true });
|
|
2492
|
+
logger.info("Deleted runtime data.");
|
|
2493
|
+
}
|
|
2494
|
+
removeShellAlias();
|
|
2495
|
+
logger.info("\nTokenArena has been uninstalled successfully.");
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2131
2498
|
// src/infrastructure/runtime/cli-version.ts
|
|
2132
|
-
import { readFileSync as
|
|
2133
|
-
import { dirname, join as
|
|
2499
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2500
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
2134
2501
|
import { fileURLToPath } from "url";
|
|
2135
2502
|
var FALLBACK_VERSION = "0.0.0";
|
|
2136
2503
|
var cachedVersion;
|
|
@@ -2138,13 +2505,13 @@ function getCliVersion(metaUrl = import.meta.url) {
|
|
|
2138
2505
|
if (cachedVersion) {
|
|
2139
2506
|
return cachedVersion;
|
|
2140
2507
|
}
|
|
2141
|
-
const packageJsonPath =
|
|
2142
|
-
|
|
2508
|
+
const packageJsonPath = join12(
|
|
2509
|
+
dirname3(fileURLToPath(metaUrl)),
|
|
2143
2510
|
"..",
|
|
2144
2511
|
"package.json"
|
|
2145
2512
|
);
|
|
2146
2513
|
try {
|
|
2147
|
-
const packageJson = JSON.parse(
|
|
2514
|
+
const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
2148
2515
|
cachedVersion = typeof packageJson.version === "string" ? packageJson.version : FALLBACK_VERSION;
|
|
2149
2516
|
} catch {
|
|
2150
2517
|
cachedVersion = FALLBACK_VERSION;
|
|
@@ -2156,14 +2523,13 @@ function getCliVersion(metaUrl = import.meta.url) {
|
|
|
2156
2523
|
var CLI_VERSION = getCliVersion();
|
|
2157
2524
|
function createCli() {
|
|
2158
2525
|
const program = new Command();
|
|
2159
|
-
program.name("tokenarena").description("Track token burn across AI coding tools").version(CLI_VERSION).showHelpAfterError().showSuggestionAfterError();
|
|
2160
|
-
program.action(
|
|
2161
|
-
const
|
|
2162
|
-
if (
|
|
2163
|
-
|
|
2164
|
-
} else {
|
|
2165
|
-
await runSync(config, { source: "default" });
|
|
2526
|
+
program.name("tokenarena").description("Track token burn across AI coding tools").version(CLI_VERSION).showHelpAfterError().showSuggestionAfterError().helpCommand("help [command]", "Display help for command");
|
|
2527
|
+
program.action(() => {
|
|
2528
|
+
const userArgs = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
2529
|
+
if (userArgs.length > 0) {
|
|
2530
|
+
program.error(`unknown command '${userArgs[0]}'`);
|
|
2166
2531
|
}
|
|
2532
|
+
program.help();
|
|
2167
2533
|
});
|
|
2168
2534
|
program.command("init").description("Initialize configuration with API key").option("--api-url <url>", "Custom API server URL").action(async (opts) => {
|
|
2169
2535
|
await runInit(opts);
|
|
@@ -2181,11 +2547,14 @@ function createCli() {
|
|
|
2181
2547
|
const args = cmd.args.slice(1);
|
|
2182
2548
|
handleConfig(args);
|
|
2183
2549
|
});
|
|
2550
|
+
program.command("uninstall").description("Remove all local configuration and data").action(async () => {
|
|
2551
|
+
await runUninstall();
|
|
2552
|
+
});
|
|
2184
2553
|
return program;
|
|
2185
2554
|
}
|
|
2186
2555
|
|
|
2187
2556
|
// src/infrastructure/runtime/main-module.ts
|
|
2188
|
-
import { existsSync as
|
|
2557
|
+
import { existsSync as existsSync13, realpathSync } from "fs";
|
|
2189
2558
|
import { resolve } from "path";
|
|
2190
2559
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2191
2560
|
function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
|
|
@@ -2196,7 +2565,7 @@ function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
|
|
|
2196
2565
|
try {
|
|
2197
2566
|
return realpathSync(argvEntry) === realpathSync(currentModulePath);
|
|
2198
2567
|
} catch {
|
|
2199
|
-
if (!
|
|
2568
|
+
if (!existsSync13(argvEntry)) {
|
|
2200
2569
|
return false;
|
|
2201
2570
|
}
|
|
2202
2571
|
return resolve(argvEntry) === resolve(currentModulePath);
|