@token-dashboard/codex-usage-uploader 0.1.0 → 0.1.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/README.md CHANGED
@@ -18,14 +18,6 @@ npx @token-dashboard/codex-usage-uploader init
18
18
 
19
19
  安装过程中会自动读取 Codex 登录态(`~/.codex/auth.json`)获取身份信息。如果读取不到邮箱,会在终端交互式提示你手动填写。
20
20
 
21
- 如需跳过交互提示,可以直接指定邮箱:
22
-
23
- ```bash
24
- npx @token-dashboard/codex-usage-uploader init \
25
- --email you@company.com \
26
- --yes
27
- ```
28
-
29
21
  安装完成后,`codex-usage-uploader` 命令会被写入 `~/bin/`,后续可直接使用。
30
22
 
31
23
  ## 命令一览
@@ -82,16 +74,16 @@ codex-usage-uploader uninstall
82
74
 
83
75
  ## 常用选项
84
76
 
85
- | 选项 | 说明 |
86
- | -------------------------- | ------------------------------- |
87
- | `--backend-url <url>` | Dashboard 后端地址(默认 `http://101.126.66.51:8086`) |
88
- | `--email <email>` | 绑定邮箱 |
89
- | `--employee-name <name>` | 绑定姓名 |
90
- | `--employee-id <id>` | 绑定工号 |
91
- | `--interval <seconds>` | 扫描间隔秒数(默认 30) |
92
- | `--yes` | 跳过所有交互确认 |
93
- | `--lines <n>` | `logs` 命令输出行数(默认 100) |
94
- | `-h, --help` | 显示帮助 |
77
+ | 选项 | 说明 |
78
+ | ------------------------ | ------------------------------------------------------ |
79
+ | `--backend-url <url>` | Dashboard 后端地址(默认 `http://101.126.66.51:8086`) |
80
+ | `--email <email>` | 绑定邮箱 |
81
+ | `--employee-name <name>` | 绑定姓名 |
82
+ | `--employee-id <id>` | 绑定工号 |
83
+ | `--interval <seconds>` | 扫描间隔秒数(默认 30) |
84
+ | `--yes` | 跳过所有交互确认 |
85
+ | `--lines <n>` | `logs` 命令输出行数(默认 100) |
86
+ | `-h, --help` | 显示帮助 |
95
87
 
96
88
  ## 工作原理
97
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@token-dashboard/codex-usage-uploader",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Codex 用量上报 CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
+ "dev": "node bin/codex-usage-uploader.js init --backend-url http://localhost:8086",
15
16
  "test": "node --no-warnings --test tests/*.test.mjs"
16
17
  },
17
18
  "engines": {
package/src/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { CodexUsageUploader } from './collector.js';
3
4
  import { identityIsBound, promptConfirm } from './auth.js';
4
- import { CLI_NAME, DEFAULT_CONFIG_FILE, PRODUCT_NAME } from './constants.js';
5
+ import { CLI_NAME, DEFAULT_BACKEND_URL, DEFAULT_CONFIG_FILE, PRODUCT_NAME } from './constants.js';
5
6
  import { findPackageRoot, installCurrentPackage } from './install.js';
6
7
  import { LaunchdServiceManager } from './launchd.js';
7
8
  import { formatStatusOutput, mergeRuntimeConfig } from './runtime-config.js';
@@ -256,44 +257,21 @@ function formatDuration(durationMs) {
256
257
  function printCatchUpProgress(event) {
257
258
  switch (event.phase) {
258
259
  case 'start':
259
- console.log(
260
- `[catch-up] start files=${event.totalFiles} queued_batches=${
261
- event.bufferingBatchCount +
262
- event.pendingBatchCount +
263
- event.retryingBatchCount
264
- } queued_events=${event.queuedEvents}`,
265
- );
260
+ console.log(`[scan] start files=${event.totalFiles}`);
266
261
  return;
267
262
  case 'file':
268
263
  console.log(
269
- `[catch-up] file ${event.filesProcessed}/${event.totalFiles} current=${event.file} events=${event.eventsParsed} queued_batches=${
270
- event.bufferingBatchCount +
271
- event.pendingBatchCount +
272
- event.retryingBatchCount
273
- } queued_events=${event.queuedEvents}`,
274
- );
275
- return;
276
- case 'upload':
277
- console.log(
278
- `[catch-up] uploaded_batches=${event.batchesUploaded} last_batch=${event.batchKey} remaining_batches=${
279
- event.bufferingBatchCount +
280
- event.pendingBatchCount +
281
- event.retryingBatchCount
282
- } remaining_events=${event.queuedEvents}`,
264
+ `[scan] file ${event.filesProcessed}/${event.totalFiles} current=${event.file} events=${event.eventsParsed}`,
283
265
  );
284
266
  return;
285
267
  case 'done':
286
268
  console.log(
287
- `[catch-up] complete files=${event.filesProcessed}/${event.totalFiles} events=${event.eventsParsed} uploaded_batches=${event.batchesUploaded} remaining_batches=${event.remainingQueuedBatches} remaining_events=${event.remainingQueuedEvents} duration=${formatDuration(event.durationMs)}`,
269
+ `[scan] complete files=${event.filesProcessed}/${event.totalFiles} events=${event.eventsParsed} pending_batches=${event.pendingBatches} duration=${formatDuration(event.durationMs)}`,
288
270
  );
289
271
  return;
290
272
  case 'error':
291
273
  console.error(
292
- `[catch-up] failed stage=${event.stage} message=${event.message} remaining_batches=${
293
- event.bufferingBatchCount +
294
- event.pendingBatchCount +
295
- event.retryingBatchCount
296
- } remaining_events=${event.queuedEvents}`,
274
+ `[scan] failed stage=${event.stage} message=${event.message}`,
297
275
  );
298
276
  return;
299
277
  }
@@ -307,8 +285,26 @@ function wrapCatchUpFailure(error) {
307
285
  );
308
286
  }
309
287
 
288
+ function printPathHint(runtime) {
289
+ const linkDir = path.dirname(runtime.homeBinLink);
290
+ const dirs = (process.env.PATH || '').split(path.delimiter);
291
+ if (dirs.includes(linkDir)) return;
292
+ console.log('');
293
+ console.log(
294
+ `Note: ${linkDir} is not in your PATH. To use \`${CLI_NAME}\` directly, run:`,
295
+ );
296
+ console.log(` export PATH="${linkDir}:$PATH"`);
297
+ console.log(
298
+ 'You can add this line to ~/.zshrc or ~/.bashrc to make it permanent.',
299
+ );
300
+ }
301
+
310
302
  async function runInit(options) {
311
- let runtime = mergeRuntimeConfig(options.configFile, runtimeOverrides(options));
303
+ const overrides = runtimeOverrides(options);
304
+ if (!overrides.backendUrl) {
305
+ overrides.backendUrl = DEFAULT_BACKEND_URL;
306
+ }
307
+ let runtime = mergeRuntimeConfig(options.configFile, overrides);
312
308
  if (!runtime.backendUrl) {
313
309
  throw new Error(
314
310
  '--backend-url is required for init unless already configured in config.json',
@@ -332,10 +328,11 @@ async function runInit(options) {
332
328
  console.log(`Install root: ${runtime.installRoot}`);
333
329
  console.log(`Config file: ${runtime.configFile}`);
334
330
  printIdentity(identity);
335
- console.log('Starting foreground historical catch-up.');
331
+ console.log('Scanning local Codex sessions...');
336
332
 
333
+ let scanResult;
337
334
  try {
338
- await uploader.runForegroundCatchUp({
335
+ scanResult = await uploader.runForegroundCatchUp({
339
336
  onProgress: printCatchUpProgress,
340
337
  });
341
338
  } catch (error) {
@@ -344,7 +341,13 @@ async function runInit(options) {
344
341
 
345
342
  manager.start();
346
343
  console.log(`${PRODUCT_NAME} initialized and started.`);
344
+ if (scanResult.pendingBatches > 0) {
345
+ console.log(
346
+ `Background service is uploading ${scanResult.pendingBatches} batch(es) with ${scanResult.pendingEvents} event(s).`,
347
+ );
348
+ }
347
349
  console.log(`Use \`${CLI_NAME} status\` to check the local service.`);
350
+ printPathHint(runtime);
348
351
  } finally {
349
352
  uploader.close();
350
353
  }
package/src/collector.js CHANGED
@@ -465,7 +465,7 @@ export class CodexUsageUploader {
465
465
  const response = await fetch(`${this.backendUrl}${apiPath}`, {
466
466
  method: 'POST',
467
467
  headers: { 'Content-Type': 'application/json' },
468
- body: stableStringify(payload),
468
+ body: JSON.stringify(payload),
469
469
  });
470
470
  if (!response.ok) {
471
471
  const text = await response.text();
@@ -489,7 +489,7 @@ export class CodexUsageUploader {
489
489
  this.stateDb.setCheckpoint('last_register_at', String(nowTs()));
490
490
  }
491
491
 
492
- async flushPendingBatches({ failFast = false, onBatchUploaded } = {}) {
492
+ async flushPendingBatches({ failFast = false, concurrency = 5, onBatchUploaded } = {}) {
493
493
  if (!this.backendUrl) {
494
494
  return { uploadedBatches: 0, failedBatches: 0, lastError: null };
495
495
  }
@@ -509,42 +509,70 @@ export class CodexUsageUploader {
509
509
  let uploadedBatches = 0;
510
510
  let failedBatches = 0;
511
511
  let lastError = null;
512
+ const collectorBody = this.collectorRequestBody();
512
513
 
513
- for (const row of this.stateDb.iterDuePendingBatches()) {
514
+ const rows = this.stateDb.iterDuePendingBatches();
515
+
516
+ const uploadOne = async (row) => {
514
517
  const payload = this.sanitizeUploadPayload(JSON.parse(row.payload_json));
515
518
  const requestBody = {
516
519
  idempotencyKey: row.batch_key,
517
- collector: this.collectorRequestBody(),
520
+ collector: collectorBody,
518
521
  payloadSizeBytes: Number(row.payload_bytes),
519
522
  sessions: payload.sessions ?? [],
520
523
  turns: payload.turns ?? [],
521
524
  events: payload.events ?? [],
522
525
  };
526
+ await this.postJson('/codex-usage/upload', requestBody);
527
+ };
528
+
529
+ const pending = new Set();
530
+ let rowIndex = 0;
531
+
532
+ const enqueue = () => {
533
+ while (pending.size < concurrency && rowIndex < rows.length) {
534
+ const row = rows[rowIndex++];
535
+ const task = uploadOne(row)
536
+ .then(() => {
537
+ this.stateDb.markBatchUploaded(row.id);
538
+ uploadedBatches += 1;
539
+ onBatchUploaded?.({ row, uploadedBatches });
540
+ })
541
+ .catch((error) => {
542
+ this.stateDb.markBatchFailed(
543
+ row.id,
544
+ Number(row.attempt_count) + 1,
545
+ error instanceof Error ? error.message : String(error),
546
+ );
547
+ failedBatches += 1;
548
+ lastError = error;
549
+ if (failFast) {
550
+ throw error;
551
+ }
552
+ })
553
+ .finally(() => {
554
+ pending.delete(task);
555
+ });
556
+ pending.add(task);
557
+ }
558
+ };
559
+
560
+ enqueue();
561
+ while (pending.size > 0) {
523
562
  try {
524
- await this.postJson('/codex-usage/upload', requestBody);
525
- this.stateDb.markBatchUploaded(row.id);
526
- uploadedBatches += 1;
527
- onBatchUploaded?.({
528
- row,
529
- uploadedBatches,
530
- queueStats: this.getQueueStats(),
531
- });
532
- } catch (error) {
533
- this.stateDb.markBatchFailed(
534
- row.id,
535
- Number(row.attempt_count) + 1,
536
- error instanceof Error ? error.message : String(error),
537
- );
538
- failedBatches += 1;
539
- lastError = error;
540
- if (failFast) {
541
- throw new Error(
542
- `Upload batch ${row.batch_key} failed: ${
543
- error instanceof Error ? error.message : String(error)
544
- }`,
545
- );
546
- }
563
+ await Promise.race(pending);
564
+ } catch {
565
+ if (failFast) break;
547
566
  }
567
+ enqueue();
568
+ }
569
+
570
+ if (failFast && lastError) {
571
+ throw new Error(
572
+ `Upload batch failed: ${
573
+ lastError instanceof Error ? lastError.message : String(lastError)
574
+ }`,
575
+ );
548
576
  }
549
577
 
550
578
  return { uploadedBatches, failedBatches, lastError };
@@ -578,15 +606,12 @@ export class CodexUsageUploader {
578
606
  filesProcessed: 0,
579
607
  eventsParsed: 0,
580
608
  batchesQueued: 0,
581
- batchesUploaded: 0,
582
- ...this.getQueueStats(),
583
609
  });
584
610
 
585
611
  let scanResult;
586
612
  try {
587
613
  scanResult = await this.scanSnapshotEntries(backfillSnapshot, {
588
614
  onFileProcessed: ({ entry, filesProcessed, totals }) => {
589
- const queueStats = this.getQueueStats();
590
615
  onProgress?.({
591
616
  phase: 'file',
592
617
  file: entry.progressPath ?? entry.relpath,
@@ -594,8 +619,6 @@ export class CodexUsageUploader {
594
619
  filesProcessed,
595
620
  eventsParsed: totals.events,
596
621
  batchesQueued: totals.batchesQueued,
597
- batchesUploaded: 0,
598
- ...queueStats,
599
622
  });
600
623
  },
601
624
  });
@@ -604,44 +627,13 @@ export class CodexUsageUploader {
604
627
  phase: 'error',
605
628
  stage: 'scan',
606
629
  message: error instanceof Error ? error.message : String(error),
607
- ...this.getQueueStats(),
608
630
  });
609
631
  throw error;
610
632
  }
611
633
 
612
634
  scanResult.batchesQueued += this.stateDb.sealStaleBatches(true);
613
- this.stateDb.markAllPendingDue();
614
635
 
615
- let flushResult;
616
- try {
617
- flushResult = await this.flushPendingBatches({
618
- failFast: true,
619
- onBatchUploaded: ({ row, uploadedBatches, queueStats }) => {
620
- onProgress?.({
621
- phase: 'upload',
622
- batchKey: row.batch_key,
623
- totalFiles,
624
- filesProcessed: totalFiles,
625
- eventsParsed: scanResult.events,
626
- batchesQueued: scanResult.batchesQueued,
627
- batchesUploaded: uploadedBatches,
628
- ...queueStats,
629
- });
630
- },
631
- });
632
- } catch (error) {
633
- onProgress?.({
634
- phase: 'error',
635
- stage: 'upload',
636
- message: error instanceof Error ? error.message : String(error),
637
- totalFiles,
638
- filesProcessed: totalFiles,
639
- eventsParsed: scanResult.events,
640
- batchesQueued: scanResult.batchesQueued,
641
- ...this.getQueueStats(),
642
- });
643
- throw error;
644
- }
636
+ await this.ensureRemoteRegistration();
645
637
 
646
638
  const queueStats = this.getQueueStats();
647
639
  const result = {
@@ -651,14 +643,10 @@ export class CodexUsageUploader {
651
643
  sessionsParsed: scanResult.sessions,
652
644
  turnsParsed: scanResult.turns,
653
645
  batchesQueued: scanResult.batchesQueued,
654
- batchesUploaded: flushResult.uploadedBatches,
655
- remainingQueuedBatches:
656
- queueStats.bufferingBatchCount +
657
- queueStats.pendingBatchCount +
658
- queueStats.retryingBatchCount,
659
- remainingQueuedEvents: queueStats.queuedEvents,
646
+ pendingBatches:
647
+ queueStats.pendingBatchCount + queueStats.retryingBatchCount,
648
+ pendingEvents: queueStats.queuedEvents,
660
649
  durationMs: Date.now() - startedAt,
661
- ...queueStats,
662
650
  };
663
651
 
664
652
  onProgress?.({