@poco-ai/tokenarena 0.1.4 → 0.1.5

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());
@@ -979,7 +1104,7 @@ import { Command, Option } from "commander";
979
1104
  // src/infrastructure/config/manager.ts
980
1105
  import { randomUUID } from "crypto";
981
1106
  import {
982
- existsSync as existsSync7,
1107
+ existsSync as existsSync8,
983
1108
  mkdirSync,
984
1109
  readFileSync as readFileSync6,
985
1110
  unlinkSync,
@@ -1012,7 +1137,7 @@ function getConfigDir() {
1012
1137
  return CONFIG_DIR;
1013
1138
  }
1014
1139
  function loadConfig() {
1015
- if (!existsSync7(CONFIG_FILE)) return null;
1140
+ if (!existsSync8(CONFIG_FILE)) return null;
1016
1141
  try {
1017
1142
  const raw = readFileSync6(CONFIG_FILE, "utf-8");
1018
1143
  const config = JSON.parse(raw);
@@ -1030,7 +1155,7 @@ function saveConfig(config) {
1030
1155
  `, "utf-8");
1031
1156
  }
1032
1157
  function deleteConfig() {
1033
- if (existsSync7(CONFIG_FILE)) {
1158
+ if (existsSync8(CONFIG_FILE)) {
1034
1159
  unlinkSync(CONFIG_FILE);
1035
1160
  }
1036
1161
  }
@@ -1041,7 +1166,7 @@ function getOrCreateDeviceId(config) {
1041
1166
  return next;
1042
1167
  }
1043
1168
  function validateApiKey(key) {
1044
- return key.startsWith("vbu_");
1169
+ return key.startsWith("ta_");
1045
1170
  }
1046
1171
  function getDefaultApiUrl() {
1047
1172
  return process.env.TOKEN_ARENA_API_URL || DEFAULT_API_URL;
@@ -1107,7 +1232,8 @@ function handleConfig(args) {
1107
1232
  if (!config || !(key in config)) {
1108
1233
  process.exit(0);
1109
1234
  }
1110
- console.log(config[key] ?? "");
1235
+ const record = config;
1236
+ console.log(record[key] ?? "");
1111
1237
  break;
1112
1238
  }
1113
1239
  case "set": {
@@ -1133,7 +1259,8 @@ function handleConfig(args) {
1133
1259
  process.exit(1);
1134
1260
  }
1135
1261
  }
1136
- config[key] = value;
1262
+ const record = config;
1263
+ record[key] = value;
1137
1264
  saveConfig(config);
1138
1265
  logger.info(`Set ${key} = ${value}`);
1139
1266
  break;
@@ -1393,7 +1520,7 @@ var ApiClient = class {
1393
1520
  // src/infrastructure/runtime/lock.ts
1394
1521
  import {
1395
1522
  closeSync,
1396
- existsSync as existsSync8,
1523
+ existsSync as existsSync9,
1397
1524
  openSync,
1398
1525
  readFileSync as readFileSync7,
1399
1526
  rmSync,
@@ -1432,7 +1559,7 @@ function isProcessAlive(pid) {
1432
1559
  }
1433
1560
  }
1434
1561
  function readLockMetadata(lockPath) {
1435
- if (!existsSync8(lockPath)) {
1562
+ if (!existsSync9(lockPath)) {
1436
1563
  return null;
1437
1564
  }
1438
1565
  try {
@@ -1506,13 +1633,13 @@ function describeExistingSyncLock() {
1506
1633
  }
1507
1634
 
1508
1635
  // src/infrastructure/runtime/state.ts
1509
- 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";
1510
1637
  function getDefaultState() {
1511
1638
  return { status: "idle" };
1512
1639
  }
1513
1640
  function loadSyncState() {
1514
1641
  const path = getSyncStatePath();
1515
- if (!existsSync9(path)) {
1642
+ if (!existsSync10(path)) {
1516
1643
  return getDefaultState();
1517
1644
  }
1518
1645
  try {
@@ -1738,7 +1865,7 @@ async function runSync(config, opts = {}) {
1738
1865
  logger.info(` ${p.source}: ${parts.join(", ")}`);
1739
1866
  }
1740
1867
  }
1741
- const apiUrl = config.apiUrl || "http://localhost:3000";
1868
+ const apiUrl = config.apiUrl || "https://token.poco-ai.com";
1742
1869
  const apiClient = new ApiClient(apiUrl, config.apiKey);
1743
1870
  let settings;
1744
1871
  try {
@@ -1927,11 +2054,15 @@ async function runDaemon(opts = {}) {
1927
2054
  }
1928
2055
 
1929
2056
  // src/commands/init.ts
1930
- import { execFile } from "child_process";
1931
- import { existsSync as existsSync10 } from "fs";
1932
- 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";
1933
2060
  import { homedir as homedir8, platform } from "os";
2061
+ import { dirname as dirname2, join as join11, posix, win32 } from "path";
1934
2062
  import { createInterface } from "readline";
2063
+ function joinForPlatform(currentPlatform, ...parts) {
2064
+ return currentPlatform === "win32" ? win32.join(...parts) : posix.join(...parts);
2065
+ }
1935
2066
  function prompt(question) {
1936
2067
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1937
2068
  return new Promise((resolve2) => {
@@ -1941,15 +2072,142 @@ function prompt(question) {
1941
2072
  });
1942
2073
  });
1943
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
+ }
1944
2097
  function openBrowser(url) {
1945
- const cmds = {
1946
- darwin: "open",
1947
- linux: "xdg-open",
1948
- win32: "start"
1949
- };
1950
- const cmd = cmds[platform()] || cmds.linux;
1951
- execFile(cmd, [url], () => {
1952
- });
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
+ }
1953
2211
  }
1954
2212
  async function runInit(opts = {}) {
1955
2213
  logger.info("\n tokenarena - Token Usage Tracker\n");
@@ -1969,7 +2227,7 @@ async function runInit(opts = {}) {
1969
2227
  while (true) {
1970
2228
  apiKey = await prompt("Paste your API key: ");
1971
2229
  if (validateApiKey(apiKey)) break;
1972
- logger.info('Invalid key \u2014 must start with "vbu_". Try again.');
2230
+ logger.info('Invalid key - must start with "ta_". Try again.');
1973
2231
  }
1974
2232
  logger.info(`
1975
2233
  Verifying key ${apiKey.slice(0, 8)}...`);
@@ -2001,7 +2259,7 @@ Verifying key ${apiKey.slice(0, 8)}...`);
2001
2259
  logger.info(`Device registered: ${deviceId.slice(0, 8)}...`);
2002
2260
  const tools = getDetectedTools();
2003
2261
  if (tools.length > 0) {
2004
- logger.info(`Detected tools: ${tools.map((t) => t.name).join(", ")}`);
2262
+ logger.info(`Detected tools: ${tools.map((tool) => tool.name).join(", ")}`);
2005
2263
  } else {
2006
2264
  logger.info("No AI coding tools detected. Install one and re-run init.");
2007
2265
  }
@@ -2012,85 +2270,55 @@ Setup complete! View your dashboard at: ${apiUrl}/usage`);
2012
2270
  await setupShellAlias();
2013
2271
  }
2014
2272
  async function setupShellAlias() {
2015
- const shell = process.env.SHELL;
2016
- if (!shell) {
2273
+ const setup = resolveShellAliasSetup();
2274
+ if (!setup) {
2017
2275
  return;
2018
2276
  }
2019
- const shellName = shell.split("/").pop() ?? "";
2020
- const aliasName = "ta";
2021
- let configFile;
2022
- let aliasLine;
2023
- let sourceHint;
2024
- switch (shellName) {
2025
- case "zsh":
2026
- configFile = `${homedir8()}/.zshrc`;
2027
- aliasLine = `alias ${aliasName}="tokenarena"`;
2028
- sourceHint = "source ~/.zshrc";
2029
- break;
2030
- case "bash":
2031
- if (platform() === "darwin" && existsSync10(`${homedir8()}/.bash_profile`)) {
2032
- configFile = `${homedir8()}/.bash_profile`;
2033
- } else {
2034
- configFile = `${homedir8()}/.bashrc`;
2035
- }
2036
- aliasLine = `alias ${aliasName}="tokenarena"`;
2037
- sourceHint = `source ${configFile}`;
2038
- break;
2039
- case "fish":
2040
- configFile = `${homedir8()}/.config/fish/config.fish`;
2041
- aliasLine = `alias ${aliasName} "tokenarena"`;
2042
- sourceHint = "source ~/.config/fish/config.fish";
2043
- break;
2044
- default:
2045
- return;
2046
- }
2047
2277
  const answer = await prompt(
2048
2278
  `
2049
- Set up shell alias '${aliasName}' for 'tokenarena'? (Y/n) `
2279
+ Set up ${setup.shellLabel} alias 'ta' for 'tokenarena'? (Y/n) `
2050
2280
  );
2051
2281
  if (answer.toLowerCase() === "n") {
2052
2282
  return;
2053
2283
  }
2054
2284
  try {
2285
+ await mkdir(dirname2(setup.configFile), { recursive: true });
2055
2286
  let existingContent = "";
2056
- if (existsSync10(configFile)) {
2057
- existingContent = await readFile(configFile, "utf-8");
2287
+ if (existsSync11(setup.configFile)) {
2288
+ existingContent = await readFile(setup.configFile, "utf-8");
2058
2289
  }
2059
- const aliasPatterns = [
2060
- `alias ${aliasName}=`,
2061
- `alias ${aliasName} "`,
2062
- `alias ${aliasName}=`
2063
- ];
2064
- const aliasExists = aliasPatterns.some(
2065
- (pattern) => existingContent.includes(pattern)
2290
+ const normalizedContent = existingContent.toLowerCase();
2291
+ const aliasExists = setup.aliasPatterns.some(
2292
+ (pattern) => normalizedContent.includes(pattern.toLowerCase())
2066
2293
  );
2067
2294
  if (aliasExists) {
2068
2295
  logger.info(
2069
2296
  `
2070
- Alias '${aliasName}' already exists in ${configFile}. Skipping.`
2297
+ Alias 'ta' already exists in ${setup.configFile}. Skipping.`
2071
2298
  );
2072
2299
  return;
2073
2300
  }
2074
2301
  const aliasWithComment = `
2075
2302
  # TokenArena alias
2076
- ${aliasLine}
2303
+ ${setup.aliasLine}
2077
2304
  `;
2078
- await appendFile(configFile, aliasWithComment);
2305
+ await appendFile(setup.configFile, aliasWithComment, "utf-8");
2079
2306
  logger.info(`
2080
- Added alias to ${configFile}`);
2081
- logger.info(` Run '${sourceHint}' or restart your terminal to use it.`);
2082
- 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");
2083
2312
  } catch (err) {
2084
2313
  logger.info(
2085
2314
  `
2086
- Could not write to ${configFile}: ${err.message}`
2315
+ Could not write to ${setup.configFile}: ${err.message}`
2087
2316
  );
2088
- logger.info(` Add this line manually: ${aliasLine}`);
2317
+ logger.info(` Add this line manually: ${setup.aliasLine}`);
2089
2318
  }
2090
2319
  }
2091
2320
 
2092
2321
  // src/commands/status.ts
2093
- import { existsSync as existsSync11 } from "fs";
2094
2322
  function formatMaybe(value) {
2095
2323
  return value || "(never)";
2096
2324
  }
@@ -2099,8 +2327,7 @@ async function runStatus() {
2099
2327
  logger.info("\ntokenarena status\n");
2100
2328
  if (!config?.apiKey) {
2101
2329
  logger.info(" Config: not configured");
2102
- logger.info(` Run \`tokenarena init\` to set up.
2103
- `);
2330
+ logger.info(" Run `tokenarena init` to set up.\n");
2104
2331
  } else {
2105
2332
  logger.info(` Config: ${getConfigPath()}`);
2106
2333
  logger.info(` API key: ${config.apiKey.slice(0, 8)}...`);
@@ -2123,7 +2350,7 @@ async function runStatus() {
2123
2350
  }
2124
2351
  logger.info(" All supported tools:");
2125
2352
  for (const tool of getAllTools()) {
2126
- const installed = existsSync11(tool.dataDir) ? "installed" : "not found";
2353
+ const installed = isToolInstalled(tool.id) ? "installed" : "not found";
2127
2354
  logger.info(` ${tool.name}: ${installed}`);
2128
2355
  }
2129
2356
  const syncState = loadSyncState();
@@ -2270,7 +2497,7 @@ async function runUninstall() {
2270
2497
 
2271
2498
  // src/infrastructure/runtime/cli-version.ts
2272
2499
  import { readFileSync as readFileSync10 } from "fs";
2273
- import { dirname, join as join11 } from "path";
2500
+ import { dirname as dirname3, join as join12 } from "path";
2274
2501
  import { fileURLToPath } from "url";
2275
2502
  var FALLBACK_VERSION = "0.0.0";
2276
2503
  var cachedVersion;
@@ -2278,8 +2505,8 @@ function getCliVersion(metaUrl = import.meta.url) {
2278
2505
  if (cachedVersion) {
2279
2506
  return cachedVersion;
2280
2507
  }
2281
- const packageJsonPath = join11(
2282
- dirname(fileURLToPath(metaUrl)),
2508
+ const packageJsonPath = join12(
2509
+ dirname3(fileURLToPath(metaUrl)),
2283
2510
  "..",
2284
2511
  "package.json"
2285
2512
  );