@yishiguji/tokenarena 0.16.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -367,8 +367,25 @@ function selectSnapshot(current, candidate) {
367
367
  }
368
368
  };
369
369
  }
370
+ function getScanDirectories() {
371
+ const directories = [];
372
+ for (const root of getClaudeRoots()) {
373
+ directories.push(join2(root, "projects"));
374
+ }
375
+ for (const root of getClaudeRoots()) {
376
+ for (const directory of ["transcripts", "sessions"]) {
377
+ directories.push(join2(root, directory));
378
+ }
379
+ }
380
+ return directories;
381
+ }
370
382
  var ClaudeCodeParser = class {
371
383
  tool = TOOL;
384
+ listSourceFiles() {
385
+ return getScanDirectories().flatMap(
386
+ (directory) => findJsonlFiles(directory)
387
+ );
388
+ }
372
389
  async parse() {
373
390
  const snapshots = /* @__PURE__ */ new Map();
374
391
  const sessionEvents = /* @__PURE__ */ new Map();
@@ -626,16 +643,22 @@ function readRollout(path) {
626
643
  let rootMeta = null;
627
644
  let model = "unknown";
628
645
  let latestTimestampOrder = 0n;
646
+ let minLineTimestampOrder = null;
629
647
  let line = 0;
648
+ let lineCount = 0;
630
649
  for (const text of content.split("\n")) {
631
650
  line++;
632
651
  if (!text.trim()) continue;
652
+ lineCount++;
633
653
  try {
634
654
  const event = JSON.parse(text);
635
655
  const timestamp = readTimestamp(event.timestamp);
636
656
  const order = timestampOrder(event.timestamp, timestamp);
637
- if (order !== null && order > latestTimestampOrder)
638
- latestTimestampOrder = order;
657
+ if (order !== null) {
658
+ if (order > latestTimestampOrder) latestTimestampOrder = order;
659
+ if (minLineTimestampOrder === null || order < minLineTimestampOrder)
660
+ minLineTimestampOrder = order;
661
+ }
639
662
  if (event.type === "session_meta" && !rootMeta) rootMeta = event;
640
663
  if (event.type === "turn_context") {
641
664
  model = readId(event.payload?.model) || readId(event.payload?.info?.model) || model;
@@ -678,11 +701,14 @@ function readRollout(path) {
678
701
  parentConflict: Boolean(
679
702
  forkedFrom && spawnedFrom && forkedFrom !== spawnedFrom
680
703
  ),
704
+ forkOrdinalExclusive: typeof meta?.forked_from_ordinal_exclusive === "number" && Number.isSafeInteger(meta.forked_from_ordinal_exclusive) && meta.forked_from_ordinal_exclusive > 0 ? meta.forked_from_ordinal_exclusive : null,
705
+ lineCount,
681
706
  rootTimestampOrder: timestampOrder(
682
707
  rootMeta?.timestamp,
683
708
  readTimestamp(rootMeta?.timestamp)
684
709
  ),
685
710
  latestTimestampOrder,
711
+ minLineTimestampOrder,
686
712
  tokens,
687
713
  prompts
688
714
  };
@@ -728,12 +754,16 @@ var CodexParser = class {
728
754
  isInstalled() {
729
755
  return this.dataDirs.some((directory) => existsSync4(directory));
730
756
  }
757
+ /** The same list `parse()` reads, so the two can never drift apart. */
758
+ listSourceFiles() {
759
+ return [...new Set(this.dataDirs.flatMap(findJsonlFiles))].sort();
760
+ }
731
761
  async parse() {
732
762
  const entries = [];
733
763
  const sessionEvents = [];
734
764
  const threads = /* @__PURE__ */ new Map();
735
765
  const threadIndex = /* @__PURE__ */ new Map();
736
- const files = [...new Set(this.dataDirs.flatMap(findJsonlFiles))].sort();
766
+ const files = this.listSourceFiles();
737
767
  for (const path of files) {
738
768
  const file = readRollout(path);
739
769
  if (!file) continue;
@@ -743,6 +773,7 @@ var CodexParser = class {
743
773
  files: [],
744
774
  timeline: [],
745
775
  latestTimestampOrder: 0n,
776
+ minLineTimestampOrder: null,
746
777
  invalidTokenTimestamp: false,
747
778
  status: "new"
748
779
  };
@@ -751,6 +782,8 @@ var CodexParser = class {
751
782
  thread.files.push(file);
752
783
  if (file.latestTimestampOrder > thread.latestTimestampOrder)
753
784
  thread.latestTimestampOrder = file.latestTimestampOrder;
785
+ if (file.minLineTimestampOrder !== null && (thread.minLineTimestampOrder === null || file.minLineTimestampOrder < thread.minLineTimestampOrder))
786
+ thread.minLineTimestampOrder = file.minLineTimestampOrder;
754
787
  thread.invalidTokenTimestamp ||= file.tokens.some(
755
788
  (token) => !token.timestamp
756
789
  );
@@ -776,7 +809,16 @@ var CodexParser = class {
776
809
  if (file.parentConflict || file.parentId) {
777
810
  const parent = file.parentId ? threadIndex.get(file.parentId) : void 0;
778
811
  const cutoff = file.rootTimestampOrder;
779
- if (file.parentConflict || !parent || parent === thread || cutoff === null || !processThread(parent) || parent.invalidTokenTimestamp || parent.latestTimestampOrder < cutoff) {
812
+ const coverageOk = parent ? file.forkOrdinalExclusive !== null ? (
813
+ // 多文件父线程按最新文件(通常就是产生该 fork 的 rollout)
814
+ // 的行数核验;其余文件属于同一线程的历史分段。
815
+ parent.files.some(
816
+ (rollout) => rollout.lineCount >= (file.forkOrdinalExclusive ?? 0)
817
+ )
818
+ ) : cutoff !== null && // 父最大时间戳落后不超过 1 秒,或父内容完全早于截止点
819
+ // 且父最早行时间戳也早于截止点(父在截止前已停止写入)。
820
+ (parent.latestTimestampOrder >= cutoff - 1000000n || parent.minLineTimestampOrder !== null && parent.minLineTimestampOrder < cutoff) : false;
821
+ if (file.parentConflict || !parent || parent === thread || cutoff === null || !processThread(parent) || parent.invalidTokenTimestamp || !coverageOk) {
780
822
  logger.warn(
781
823
  "Codex fork replay could not be verified; skipping " + basename2(file.path)
782
824
  );
@@ -6640,7 +6682,7 @@ async function handleConfig(args) {
6640
6682
  }
6641
6683
 
6642
6684
  // src/services/sync-service.ts
6643
- import { hostname as hostname6 } from "os";
6685
+ import { hostname as hostname7 } from "os";
6644
6686
 
6645
6687
  // src/domain/project-identity.ts
6646
6688
  import { createHmac } from "crypto";
@@ -7272,38 +7314,179 @@ function saveUploadManifest(manifest) {
7272
7314
  );
7273
7315
  }
7274
7316
 
7317
+ // src/services/parse-cache.ts
7318
+ import { createHash as createHash7 } from "crypto";
7319
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
7320
+ import { hostname as hostname6 } from "os";
7321
+ import { join as join35 } from "path";
7322
+
7323
+ // src/infrastructure/runtime/cli-version.ts
7324
+ import { dirname as dirname7, join as join34 } from "path";
7325
+ import { fileURLToPath } from "url";
7326
+ var FALLBACK_VERSION = "0.0.0";
7327
+ var versionCache = /* @__PURE__ */ new Map();
7328
+ function readPackageVersion(startDir) {
7329
+ let dir = startDir;
7330
+ while (true) {
7331
+ const contents = readFileSafe(join34(dir, "package.json"));
7332
+ if (contents !== null) {
7333
+ try {
7334
+ const { version } = JSON.parse(contents);
7335
+ return typeof version === "string" && version.length > 0 ? version : void 0;
7336
+ } catch {
7337
+ return void 0;
7338
+ }
7339
+ }
7340
+ const parent = dirname7(dir);
7341
+ if (parent === dir) {
7342
+ return void 0;
7343
+ }
7344
+ dir = parent;
7345
+ }
7346
+ }
7347
+ function getCliVersion(metaUrl = import.meta.url) {
7348
+ const startDir = dirname7(fileURLToPath(metaUrl));
7349
+ const cached = versionCache.get(startDir);
7350
+ if (cached !== void 0) {
7351
+ return cached;
7352
+ }
7353
+ const version = readPackageVersion(startDir) ?? FALLBACK_VERSION;
7354
+ versionCache.set(startDir, version);
7355
+ return version;
7356
+ }
7357
+
7358
+ // src/services/parse-cache.ts
7359
+ var CACHE_FORMAT_VERSION = 1;
7360
+ var SETTLE_MS = 5e3;
7361
+ function getParseCacheDir() {
7362
+ return join35(getStateDir(), "parse-cache");
7363
+ }
7364
+ function getParseCachePath(toolId) {
7365
+ return join35(getParseCacheDir(), `${toolId}.json`);
7366
+ }
7367
+ function computeScanFingerprint(files, now = Date.now()) {
7368
+ const hash = createHash7("sha256");
7369
+ hash.update(`v${CACHE_FORMAT_VERSION}\0`);
7370
+ hash.update(`${getCliVersion()}\0`);
7371
+ hash.update(`${hostname6()}\0`);
7372
+ for (const file of [...files].sort()) {
7373
+ let stats;
7374
+ try {
7375
+ stats = statSync6(file);
7376
+ } catch {
7377
+ return null;
7378
+ }
7379
+ if (now - stats.mtimeMs < SETTLE_MS) {
7380
+ return null;
7381
+ }
7382
+ hash.update(`${file}\0${stats.mtimeMs}\0${stats.size}\0`);
7383
+ }
7384
+ return hash.digest("hex");
7385
+ }
7386
+ function loadCachedParseResult(toolId, fingerprint2) {
7387
+ if (!fingerprint2) {
7388
+ return null;
7389
+ }
7390
+ try {
7391
+ const envelope = JSON.parse(
7392
+ readFileSync14(getParseCachePath(toolId), "utf-8")
7393
+ );
7394
+ if (envelope.formatVersion !== CACHE_FORMAT_VERSION || envelope.fingerprint !== fingerprint2 || !Array.isArray(envelope.result?.buckets) || !Array.isArray(envelope.result?.sessions)) {
7395
+ return null;
7396
+ }
7397
+ return {
7398
+ buckets: envelope.result.buckets,
7399
+ sessions: envelope.result.sessions
7400
+ };
7401
+ } catch {
7402
+ return null;
7403
+ }
7404
+ }
7405
+ function saveCachedParseResult(toolId, fingerprint2, result) {
7406
+ if (!fingerprint2 || result.incomplete) {
7407
+ return;
7408
+ }
7409
+ const envelope = {
7410
+ formatVersion: CACHE_FORMAT_VERSION,
7411
+ fingerprint: fingerprint2,
7412
+ result: { buckets: result.buckets, sessions: result.sessions }
7413
+ };
7414
+ try {
7415
+ mkdirSync3(getParseCacheDir(), { recursive: true });
7416
+ writeFileSync5(getParseCachePath(toolId), JSON.stringify(envelope), "utf-8");
7417
+ } catch {
7418
+ }
7419
+ }
7420
+
7275
7421
  // src/services/parser-service.ts
7422
+ var PARSER_CONCURRENCY = 8;
7423
+ async function parseWithCache(parser) {
7424
+ const files = parser.listSourceFiles?.();
7425
+ const fingerprint2 = files ? computeScanFingerprint(files) : null;
7426
+ const cached = loadCachedParseResult(parser.tool.id, fingerprint2);
7427
+ if (cached) {
7428
+ return { status: "ok", result: cached, cached: true };
7429
+ }
7430
+ try {
7431
+ const result = await parser.parse();
7432
+ saveCachedParseResult(parser.tool.id, fingerprint2, result);
7433
+ return { status: "ok", result, cached: false };
7434
+ } catch (err) {
7435
+ return {
7436
+ status: "failed",
7437
+ message: err instanceof Error ? err.message : String(err)
7438
+ };
7439
+ }
7440
+ }
7441
+ async function mapWithConcurrency(items, limit, run2) {
7442
+ const results = new Array(items.length);
7443
+ let cursor = 0;
7444
+ async function worker() {
7445
+ while (cursor < items.length) {
7446
+ const index = cursor++;
7447
+ results[index] = await run2(items[index]);
7448
+ }
7449
+ }
7450
+ await Promise.all(
7451
+ Array.from({ length: Math.min(limit, items.length) }, () => worker())
7452
+ );
7453
+ return results;
7454
+ }
7276
7455
  async function runAllParsers() {
7277
7456
  const allBuckets = [];
7278
7457
  const allSessions = [];
7279
7458
  const parserResults = [];
7280
7459
  const failedSources = /* @__PURE__ */ new Set();
7281
- for (const parser of getAllParsers()) {
7282
- try {
7283
- const result = await parser.parse();
7284
- const buckets = result.buckets;
7285
- const sessions = result.sessions;
7286
- if (result.incomplete) {
7287
- failedSources.add(parser.tool.id);
7288
- logger.warn(
7289
- `${parser.tool.id} parser reported an incomplete scan; preserving its remote history.`
7290
- );
7291
- continue;
7292
- }
7293
- if (buckets.length > 0) allBuckets.push(...buckets);
7294
- if (sessions.length > 0) allSessions.push(...sessions);
7295
- if (buckets.length > 0 || sessions.length > 0) {
7296
- parserResults.push({
7297
- source: parser.tool.id,
7298
- buckets: buckets.length,
7299
- sessions: sessions.length
7300
- });
7301
- }
7302
- } catch (err) {
7460
+ const parsers2 = getAllParsers();
7461
+ const settled = await mapWithConcurrency(
7462
+ parsers2,
7463
+ PARSER_CONCURRENCY,
7464
+ parseWithCache
7465
+ );
7466
+ for (const [index, parser] of parsers2.entries()) {
7467
+ const outcome = settled[index];
7468
+ if (outcome.status === "failed") {
7469
+ failedSources.add(parser.tool.id);
7470
+ logger.warn(`${parser.tool.id} parser failed: ${outcome.message}`);
7471
+ continue;
7472
+ }
7473
+ const { buckets, sessions, incomplete } = outcome.result;
7474
+ if (incomplete) {
7303
7475
  failedSources.add(parser.tool.id);
7304
7476
  logger.warn(
7305
- `${parser.tool.id} parser failed: ${err instanceof Error ? err.message : String(err)}`
7477
+ `${parser.tool.id} parser reported an incomplete scan; preserving its remote history.`
7306
7478
  );
7479
+ continue;
7480
+ }
7481
+ if (buckets.length > 0) allBuckets.push(...buckets);
7482
+ if (sessions.length > 0) allSessions.push(...sessions);
7483
+ if (buckets.length > 0 || sessions.length > 0) {
7484
+ parserResults.push({
7485
+ source: parser.tool.id,
7486
+ buckets: buckets.length,
7487
+ sessions: sessions.length,
7488
+ ...outcome.cached ? { cached: true } : {}
7489
+ });
7307
7490
  }
7308
7491
  }
7309
7492
  return {
@@ -7378,7 +7561,7 @@ function persistUploadManifest(manifest, quiet) {
7378
7561
  function toDeviceMetadata(config) {
7379
7562
  return {
7380
7563
  deviceId: getOrCreateDeviceId(config),
7381
- hostname: hostname6().replace(/\.local$/, "")
7564
+ hostname: hostname7().replace(/\.local$/, "")
7382
7565
  };
7383
7566
  }
7384
7567
  function toUploadBuckets(buckets, settings, device) {
@@ -7577,7 +7760,8 @@ async function runSync(config, opts = {}) {
7577
7760
  const parts = [];
7578
7761
  if (p.buckets > 0) parts.push(`${p.buckets} buckets`);
7579
7762
  if (p.sessions > 0) parts.push(`${p.sessions} sessions`);
7580
- logger.info(` ${p.source}: ${parts.join(", ")}`);
7763
+ const suffix = p.cached ? " (unchanged)" : "";
7764
+ logger.info(` ${p.source}: ${parts.join(", ")}${suffix}`);
7581
7765
  }
7582
7766
  }
7583
7767
  const apiUrl = config.apiUrl || "https://token.guji.uno";
@@ -7829,14 +8013,14 @@ import { execFileSync as execFileSync7, spawn } from "child_process";
7829
8013
  import { existsSync as existsSync36 } from "fs";
7830
8014
  import { appendFile, mkdir, readFile } from "fs/promises";
7831
8015
  import { homedir as homedir33, platform as platform5 } from "os";
7832
- import { dirname as dirname7, join as join34, posix as posix3, win32 } from "path";
8016
+ import { dirname as dirname8, join as join36, posix as posix3, win32 } from "path";
7833
8017
 
7834
8018
  // src/infrastructure/service/index.ts
7835
8019
  import { platform as platform4 } from "os";
7836
8020
 
7837
8021
  // src/infrastructure/service/linux-systemd.ts
7838
8022
  import { execFileSync as execFileSync5 } from "child_process";
7839
- import { existsSync as existsSync34, mkdirSync as mkdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
8023
+ import { existsSync as existsSync34, mkdirSync as mkdirSync4, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7840
8024
  import { homedir as homedir31, platform as platform2 } from "os";
7841
8025
  import { posix } from "path";
7842
8026
 
@@ -7998,8 +8182,8 @@ function createLinuxSystemdServiceBackend() {
7998
8182
  execPath: command.execPath,
7999
8183
  args: command.args
8000
8184
  });
8001
- mkdirSync3(serviceDir, { recursive: true });
8002
- writeFileSync5(serviceFile, content, "utf-8");
8185
+ mkdirSync4(serviceDir, { recursive: true });
8186
+ writeFileSync6(serviceFile, content, "utf-8");
8003
8187
  execSystemctl(["daemon-reload"]);
8004
8188
  execSystemctl(["enable", SYSTEMD_SERVICE_NAME]);
8005
8189
  execSystemctl(["start", SYSTEMD_SERVICE_NAME]);
@@ -8158,7 +8342,7 @@ function createLinuxSystemdServiceBackend() {
8158
8342
 
8159
8343
  // src/infrastructure/service/macos-launchd.ts
8160
8344
  import { execFileSync as execFileSync6 } from "child_process";
8161
- import { existsSync as existsSync35, mkdirSync as mkdirSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
8345
+ import { existsSync as existsSync35, mkdirSync as mkdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
8162
8346
  import { homedir as homedir32, platform as platform3 } from "os";
8163
8347
  import { posix as posix2 } from "path";
8164
8348
  var MACOS_LAUNCHD_LABEL = "com.guji.tokenarena";
@@ -8300,8 +8484,8 @@ function writeLaunchAgentPlist() {
8300
8484
  standardErrorPath: stderrPath
8301
8485
  });
8302
8486
  ensureAppDirs();
8303
- mkdirSync4(getMacosLaunchAgentDir(), { recursive: true });
8304
- writeFileSync6(plistFile, plist, "utf-8");
8487
+ mkdirSync5(getMacosLaunchAgentDir(), { recursive: true });
8488
+ writeFileSync7(plistFile, plist, "utf-8");
8305
8489
  return plistFile;
8306
8490
  }
8307
8491
  function bootstrapLaunchAgent() {
@@ -8572,7 +8756,7 @@ function resolvePowerShellProfilePath() {
8572
8756
  const systemRoot = process.env.SYSTEMROOT || "C:\\Windows";
8573
8757
  const candidates = [
8574
8758
  "pwsh.exe",
8575
- join34(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
8759
+ join36(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
8576
8760
  ];
8577
8761
  for (const command of candidates) {
8578
8762
  try {
@@ -8797,7 +8981,7 @@ async function setupShellAlias() {
8797
8981
  return;
8798
8982
  }
8799
8983
  try {
8800
- await mkdir(dirname7(setup.configFile), { recursive: true });
8984
+ await mkdir(dirname8(setup.configFile), { recursive: true });
8801
8985
  let existingContent = "";
8802
8986
  if (existsSync36(setup.configFile)) {
8803
8987
  existingContent = await readFile(setup.configFile, "utf-8");
@@ -9062,41 +9246,6 @@ function buildLocalUsageDashboardData(input2) {
9062
9246
  };
9063
9247
  }
9064
9248
 
9065
- // src/infrastructure/runtime/cli-version.ts
9066
- import { dirname as dirname8, join as join35 } from "path";
9067
- import { fileURLToPath } from "url";
9068
- var FALLBACK_VERSION = "0.0.0";
9069
- var versionCache = /* @__PURE__ */ new Map();
9070
- function readPackageVersion(startDir) {
9071
- let dir = startDir;
9072
- while (true) {
9073
- const contents = readFileSafe(join35(dir, "package.json"));
9074
- if (contents !== null) {
9075
- try {
9076
- const { version } = JSON.parse(contents);
9077
- return typeof version === "string" && version.length > 0 ? version : void 0;
9078
- } catch {
9079
- return void 0;
9080
- }
9081
- }
9082
- const parent = dirname8(dir);
9083
- if (parent === dir) {
9084
- return void 0;
9085
- }
9086
- dir = parent;
9087
- }
9088
- }
9089
- function getCliVersion(metaUrl = import.meta.url) {
9090
- const startDir = dirname8(fileURLToPath(metaUrl));
9091
- const cached = versionCache.get(startDir);
9092
- if (cached !== void 0) {
9093
- return cached;
9094
- }
9095
- const version = readPackageVersion(startDir) ?? FALLBACK_VERSION;
9096
- versionCache.set(startDir, version);
9097
- return version;
9098
- }
9099
-
9100
9249
  // src/infrastructure/ui/local-usage-dashboard.ts
9101
9250
  var MAX_RANK_ROWS = 6;
9102
9251
  var MAX_SESSION_ROWS = 6;
@@ -9495,7 +9644,7 @@ async function runSyncCommand(opts = {}) {
9495
9644
  }
9496
9645
 
9497
9646
  // src/commands/uninstall.ts
9498
- import { existsSync as existsSync37, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
9647
+ import { existsSync as existsSync37, readFileSync as readFileSync15, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9499
9648
  import { homedir as homedir34, platform as platform6 } from "os";
9500
9649
  function removeShellAlias() {
9501
9650
  const shell = process.env.SHELL;
@@ -9522,7 +9671,7 @@ function removeShellAlias() {
9522
9671
  }
9523
9672
  if (!existsSync37(configFile)) return;
9524
9673
  try {
9525
- let content = readFileSync14(configFile, "utf-8");
9674
+ let content = readFileSync15(configFile, "utf-8");
9526
9675
  const aliasPatterns = [
9527
9676
  // zsh / bash format: alias ta="tokenarena"
9528
9677
  new RegExp(
@@ -9544,7 +9693,7 @@ function removeShellAlias() {
9544
9693
  content = next;
9545
9694
  }
9546
9695
  }
9547
- writeFileSync7(configFile, content, "utf-8");
9696
+ writeFileSync8(configFile, content, "utf-8");
9548
9697
  logger.info(`Removed shell alias from ${configFile}`);
9549
9698
  } catch (err) {
9550
9699
  logger.warn(