@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 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((t) => t.id === parser.tool.id)) {
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((t) => existsSync2(t.dataDir));
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 TRANSCRIPTS_DIR = join2(homedir(), ".claude", "transcripts");
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 firstSeg = relative.split(sep2)[0];
249
- if (!firstSeg) return "unknown";
250
- const parts = firstSeg.split("-").filter(Boolean);
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 ts = new Date(timestamp);
274
- if (Number.isNaN(ts.getTime())) continue;
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: ts,
292
+ timestamp: parsedTimestamp,
281
293
  role: obj.type === "user" ? "user" : "assistant"
282
294
  });
283
295
  }
284
296
  if (obj.type !== "assistant") continue;
285
- const msg = obj.message;
286
- if (!msg || !msg.usage) continue;
287
- const usage = msg.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: msg.model || "unknown",
311
+ model: message.model || "unknown",
299
312
  project,
300
- timestamp: ts,
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 transcriptFiles = findJsonlFiles(TRANSCRIPTS_DIR);
311
- for (const filePath of transcriptFiles) {
312
- const sessionId = extractSessionId(filePath);
313
- if (seenSessionIds.has(sessionId)) continue;
314
- const content = readFileSafe(filePath);
315
- if (!content) continue;
316
- for (const line of content.split("\n")) {
317
- if (!line.trim()) continue;
318
- try {
319
- const obj = JSON.parse(line);
320
- const timestamp = obj.timestamp;
321
- if (!timestamp) continue;
322
- const ts = new Date(timestamp);
323
- if (Number.isNaN(ts.getTime())) continue;
324
- if (obj.type === "user" || obj.type === "assistant") {
325
- sessionEvents.push({
326
- sessionId,
327
- source: "claude-code",
328
- project: "unknown",
329
- timestamp: ts,
330
- role: obj.type === "user" ? "user" : "assistant"
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" && obj.payload) {
372
- const meta = obj.payload;
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 evTs = new Date(obj.timestamp);
396
- if (!Number.isNaN(evTs.getTime())) {
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: evTs,
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 existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
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 (!existsSync3(baseDir)) return results;
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 (!existsSync3(chatsDir)) continue;
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 existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
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: join5(homedir4(), ".copilot", "session-state")
610
+ dataDir: ROOT_DIR
581
611
  };
582
- function findEventFiles(baseDir) {
583
- const results = [];
584
- if (!existsSync4(baseDir)) return results;
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(baseDir, { withFileTypes: true })) {
587
- if (!entry.isDirectory()) continue;
588
- const eventsFile = join5(baseDir, entry.name, "events.jsonl");
589
- if (existsSync4(eventsFile)) {
590
- results.push({ filePath: eventsFile, sessionId: entry.name });
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(TOOL4.dataDir);
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 existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
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: join6(homedir5(), ".local", "share", "opencode")
737
+ dataDir: DEFAULT_DATA_DIR
692
738
  };
693
- var DB_PATH = join6(TOOL5.dataDir, "opencode.db");
694
- var MESSAGES_DIR = join6(TOOL5.dataDir, "storage", "message");
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
- if (existsSync5(DB_PATH)) {
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
- let output;
720
- try {
721
- output = execFileSync("sqlite3", ["-json", DB_PATH, query], {
722
- encoding: "utf-8",
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 (!existsSync5(MESSAGES_DIR)) return { buckets: [], sessions: [] };
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(MESSAGES_DIR, { withFileTypes: true }).filter(
790
- (d) => d.isDirectory() && d.name.startsWith("ses_")
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(MESSAGES_DIR, sessionDir.name);
797
- let msgFiles;
919
+ const sessionPath = join6(messagesDir, sessionDir.name);
920
+ let messageFiles;
798
921
  try {
799
- msgFiles = readdirSync4(sessionPath).filter((f) => f.endsWith(".json"));
922
+ messageFiles = readdirSync4(sessionPath).filter(
923
+ (file) => file.endsWith(".json")
924
+ );
800
925
  } catch {
801
926
  continue;
802
927
  }
803
- for (const file of msgFiles) {
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 existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync5 } from "fs";
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 (!existsSync6(agentsDir)) continue;
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 (!existsSync6(sessionsDir)) continue;
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) => existsSync6(join7(root, "agents")));
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 { existsSync as existsSync7, mkdirSync, readFileSync as readFileSync6, writeFileSync } from "fs";
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
- var CONFIG_DIR = join8(homedir7(), ".tokenarena");
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 = join8(CONFIG_DIR, isDev ? "config.dev.json" : "config.json");
987
- var DEFAULT_API_URL = "http://localhost:3000";
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 (!existsSync7(CONFIG_FILE)) return null;
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("vbu_");
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
- console.log(config[key] ?? "");
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: "http://localhost:3000"
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
- config[key] = value;
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 existsSync8,
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 join9 } from "path";
1381
- function getRuntimeDir() {
1382
- return join9(getConfigDir(), "runtime");
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 join9(getRuntimeDir(), "sync.lock");
1541
+ return join10(getRuntimeDirPath(), "sync.lock");
1386
1542
  }
1387
1543
  function getSyncStatePath() {
1388
- return join9(getRuntimeDir(), "status.json");
1544
+ return join10(getStateDir(), "status.json");
1389
1545
  }
1390
- function ensureAppRuntimeDirs() {
1391
- mkdirSync2(getRuntimeDir(), { recursive: true });
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 (!existsSync8(lockPath)) {
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
- ensureAppRuntimeDirs();
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 existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
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 (!existsSync9(path)) {
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
- ensureAppRuntimeDirs();
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 || "http://localhost:3000";
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 { execFile } from "child_process";
1901
- import { existsSync as existsSync10 } from "fs";
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 cmds = {
1916
- darwin: "open",
1917
- linux: "xdg-open",
1918
- win32: "start"
1919
- };
1920
- const cmd = cmds[platform()] || cmds.linux;
1921
- execFile(cmd, [url], () => {
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 \u2014 must start with "vbu_". Try again.');
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((t) => t.name).join(", ")}`);
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 shell = process.env.SHELL;
1986
- if (!shell) {
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 shell alias '${aliasName}' for 'tokenarena'? (Y/n) `
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 (existsSync10(configFile)) {
2027
- existingContent = await readFile(configFile, "utf-8");
2287
+ if (existsSync11(setup.configFile)) {
2288
+ existingContent = await readFile(setup.configFile, "utf-8");
2028
2289
  }
2029
- const aliasPatterns = [
2030
- `alias ${aliasName}=`,
2031
- `alias ${aliasName} "`,
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 '${aliasName}' already exists in ${configFile}. Skipping.`
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(` Run '${sourceHint}' or restart your terminal to use it.`);
2052
- logger.info(` Then you can use: ${aliasName} sync`);
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(` Run \`tokenarena init\` to set up.
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 || "http://localhost:3000"}`);
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 = existsSync11(tool.dataDir) ? "installed" : "not found";
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 readFileSync9 } from "fs";
2133
- import { dirname, join as join10 } from "path";
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 = join10(
2142
- dirname(fileURLToPath(metaUrl)),
2508
+ const packageJsonPath = join12(
2509
+ dirname3(fileURLToPath(metaUrl)),
2143
2510
  "..",
2144
2511
  "package.json"
2145
2512
  );
2146
2513
  try {
2147
- const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
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(async () => {
2161
- const config = loadConfig();
2162
- if (!config?.apiKey) {
2163
- await runInit();
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 existsSync12, realpathSync } from "fs";
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 (!existsSync12(argvEntry)) {
2568
+ if (!existsSync13(argvEntry)) {
2200
2569
  return false;
2201
2570
  }
2202
2571
  return resolve(argvEntry) === resolve(currentModulePath);