@vibescore/tracker 0.0.4 → 0.0.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/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  **QUANTIFY YOUR AI OUTPUT**
6
6
  _Real-time AI Analytics for Codex CLI_
7
7
 
8
+ [**www.vibescore.space**](https://www.vibescore.space)
9
+
8
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
9
11
  [![Node.js Support](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen.svg)](https://nodejs.org/)
10
12
  [![Platform](https://img.shields.io/badge/Platform-macOS-lightgrey.svg)](https://www.apple.com/macos/)
@@ -50,6 +52,24 @@ npx --yes @vibescore/tracker sync
50
52
  npx --yes @vibescore/tracker status
51
53
  ```
52
54
 
55
+ ## 🧰 Troubleshooting
56
+
57
+ ### Streak shows 0 days while totals look correct
58
+
59
+ - Streak is defined as consecutive days ending today. If today's total is 0, streak will be 0.
60
+ - If you expect a non-zero streak, clear cached auth/heatmap data and sign in again:
61
+
62
+ ```js
63
+ localStorage.removeItem('vibescore.dashboard.auth.v1');
64
+ Object.keys(localStorage)
65
+ .filter((k) => k.startsWith('vibescore.heatmap.'))
66
+ .forEach((k) => localStorage.removeItem(k));
67
+ location.reload();
68
+ ```
69
+
70
+ - Complete the landing page sign-in flow again after reload.
71
+ - Note: `insforge-auth-token` is not used by the dashboard; use `vibescore.dashboard.auth.v1`.
72
+
53
73
  ## 🏗️ Architecture
54
74
 
55
75
  ```mermaid
package/README.zh-CN.md CHANGED
@@ -5,6 +5,8 @@
5
5
  **量化你的 AI 产出**
6
6
  _Codex CLI 实时 AI 分析工具_
7
7
 
8
+ [**www.vibescore.space**](https://www.vibescore.space)
9
+
8
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
9
11
  [![Node.js Support](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen.svg)](https://nodejs.org/)
10
12
  [![Platform](https://img.shields.io/badge/Platform-macOS-lightgrey.svg)](https://www.apple.com/macos/)
@@ -50,6 +52,24 @@ npx --yes @vibescore/tracker sync
50
52
  npx --yes @vibescore/tracker status
51
53
  ```
52
54
 
55
+ ## 🧰 常见问题
56
+
57
+ ### Streak 显示 0 天但总量正常
58
+
59
+ - Streak 的口径是“从今天开始连续使用的天数”,如果今天的 total 为 0,streak 就是 0。
60
+ - 如果你确认应该有 streak,请清理本地缓存并重新登录:
61
+
62
+ ```js
63
+ localStorage.removeItem('vibescore.dashboard.auth.v1');
64
+ Object.keys(localStorage)
65
+ .filter((k) => k.startsWith('vibescore.heatmap.'))
66
+ .forEach((k) => localStorage.removeItem(k));
67
+ location.reload();
68
+ ```
69
+
70
+ - 刷新后重新走一遍 landing page 的登录流程。
71
+ - 说明:Dashboard 不使用 `insforge-auth-token`,实际存储在 `vibescore.dashboard.auth.v1`。
72
+
53
73
  ## 🏗️ 系统架构
54
74
 
55
75
  ```mermaid
@@ -92,6 +112,6 @@ npm run smoke
92
112
  ---
93
113
 
94
114
  <div align="center">
95
- <b>System_Ready // 2014 VibeScore OS</b><br/>
115
+ <b>System_Ready // 2024 VibeScore OS</b><br/>
96
116
  <i>"More Tokens. More Vibe."</i>
97
117
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/cli.js CHANGED
@@ -48,7 +48,7 @@ function printHelp() {
48
48
  '',
49
49
  'Notes:',
50
50
  ' - init installs a Codex notify hook and issues a device token (default: browser sign in/up).',
51
- ' - optional: set VIBESCORE_DASHBOARD_URL (or --dashboard-url) to use a hosted /connect page.',
51
+ ' - optional: set VIBESCORE_DASHBOARD_URL (or --dashboard-url) to use a hosted landing page.',
52
52
  ' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and uploads token_count deltas.',
53
53
  ' - --debug prints original backend errors when they are normalized.',
54
54
  ''
@@ -24,6 +24,7 @@ async function cmdStatus(argv = []) {
24
24
  const notifySignalPath = path.join(trackerDir, 'notify.signal');
25
25
  const throttlePath = path.join(trackerDir, 'sync.throttle');
26
26
  const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
27
+ const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
27
28
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
28
29
  const codexConfigPath = path.join(codexHome, 'config.toml');
29
30
 
@@ -31,6 +32,7 @@ async function cmdStatus(argv = []) {
31
32
  const cursors = await readJson(cursorsPath);
32
33
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
33
34
  const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
35
+ const autoRetry = await readJson(autoRetryPath);
34
36
 
35
37
  const queueSize = await safeStatSize(queuePath);
36
38
  const pendingBytes = Math.max(0, queueSize - (queueState.offset || 0));
@@ -51,6 +53,12 @@ async function cmdStatus(argv = []) {
51
53
  const lastUploadError = uploadThrottle.lastError
52
54
  ? `${uploadThrottle.lastErrorAt || 'unknown'} ${uploadThrottle.lastError}`
53
55
  : null;
56
+ const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs || null);
57
+ const autoRetryLine = autoRetryAt
58
+ ? `- Auto retry after: ${autoRetryAt} (${autoRetry?.reason || 'scheduled'}, pending ${Number(
59
+ autoRetry?.pendingBytes || 0
60
+ )} bytes)`
61
+ : null;
54
62
 
55
63
  process.stdout.write(
56
64
  [
@@ -65,6 +73,7 @@ async function cmdStatus(argv = []) {
65
73
  `- Next upload after: ${nextUpload || 'never'}`,
66
74
  `- Backoff until: ${backoffUntil || 'never'}`,
67
75
  lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
76
+ autoRetryLine,
68
77
  `- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : 'unset'}`,
69
78
  ''
70
79
  ]
@@ -1,6 +1,7 @@
1
1
  const os = require('node:os');
2
2
  const path = require('node:path');
3
3
  const fs = require('node:fs/promises');
4
+ const cp = require('node:child_process');
4
5
 
5
6
  const { ensureDir, readJson, writeJson, openLock } = require('../lib/fs');
6
7
  const { listRolloutFiles, parseRolloutIncremental } = require('../lib/rollout');
@@ -39,6 +40,7 @@ async function cmdSync(argv) {
39
40
  const config = await readJson(configPath);
40
41
  const cursors = (await readJson(cursorsPath)) || { version: 1, files: {}, updatedAt: null };
41
42
  const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
43
+ let uploadThrottleState = uploadThrottle;
42
44
 
43
45
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
44
46
  const sessionsDir = path.join(codexHome, 'sessions');
@@ -72,6 +74,7 @@ async function cmdSync(argv) {
72
74
  const baseUrl = config?.baseUrl || process.env.VIBESCORE_INSFORGE_BASE_URL || 'https://5tmappuk.us-east.insforge.app';
73
75
 
74
76
  let uploadResult = null;
77
+ let uploadAttempted = false;
75
78
  if (deviceToken) {
76
79
  const beforeState = (await readJson(queueStatePath)) || { offset: 0 };
77
80
  const queueSize = await safeStatSize(queuePath);
@@ -79,16 +82,27 @@ async function cmdSync(argv) {
79
82
  let maxBatches = opts.auto ? 3 : opts.drain ? 10_000 : 10;
80
83
  let batchSize = UPLOAD_DEFAULTS.batchSize;
81
84
  let allowUpload = pendingBytes > 0;
85
+ let autoDecision = null;
82
86
 
83
87
  if (opts.auto) {
84
- const decision = decideAutoUpload({
88
+ autoDecision = decideAutoUpload({
85
89
  nowMs: Date.now(),
86
90
  pendingBytes,
87
91
  state: uploadThrottle
88
92
  });
89
- allowUpload = allowUpload && decision.allowed;
90
- maxBatches = decision.allowed ? decision.maxBatches : 0;
91
- batchSize = decision.batchSize;
93
+ allowUpload = allowUpload && autoDecision.allowed;
94
+ maxBatches = autoDecision.allowed ? autoDecision.maxBatches : 0;
95
+ batchSize = autoDecision.batchSize;
96
+ if (!autoDecision.allowed && pendingBytes > 0 && autoDecision.blockedUntilMs > 0) {
97
+ const reason = deriveAutoSkipReason({ decision: autoDecision, state: uploadThrottle });
98
+ await scheduleAutoRetry({
99
+ trackerDir,
100
+ retryAtMs: autoDecision.blockedUntilMs,
101
+ reason,
102
+ pendingBytes,
103
+ source: 'auto-throttled'
104
+ });
105
+ }
92
106
  }
93
107
 
94
108
  if (progress?.enabled && pendingBytes > 0 && allowUpload) {
@@ -99,6 +113,7 @@ async function cmdSync(argv) {
99
113
  }
100
114
 
101
115
  if (allowUpload && maxBatches > 0) {
116
+ uploadAttempted = true;
102
117
  try {
103
118
  uploadResult = await drainQueueToCloud({
104
119
  baseUrl,
@@ -118,12 +133,26 @@ async function cmdSync(argv) {
118
133
  }
119
134
  });
120
135
  if (uploadResult.attempted > 0) {
121
- const next = recordUploadSuccess({ nowMs: Date.now(), state: uploadThrottle });
136
+ const next = recordUploadSuccess({ nowMs: Date.now(), state: uploadThrottleState });
137
+ uploadThrottleState = next;
122
138
  await writeJson(uploadThrottlePath, next);
123
139
  }
124
140
  } catch (e) {
125
- const next = recordUploadFailure({ nowMs: Date.now(), state: uploadThrottle, error: e });
141
+ const next = recordUploadFailure({ nowMs: Date.now(), state: uploadThrottleState, error: e });
142
+ uploadThrottleState = next;
126
143
  await writeJson(uploadThrottlePath, next);
144
+ if (opts.auto && pendingBytes > 0) {
145
+ const retryAtMs = Math.max(next.nextAllowedAtMs || 0, next.backoffUntilMs || 0);
146
+ if (retryAtMs > 0) {
147
+ await scheduleAutoRetry({
148
+ trackerDir,
149
+ retryAtMs,
150
+ reason: 'backoff',
151
+ pendingBytes,
152
+ source: 'auto-error'
153
+ });
154
+ }
155
+ }
127
156
  throw e;
128
157
  }
129
158
  } else {
@@ -137,6 +166,21 @@ async function cmdSync(argv) {
137
166
  const queueSize = await safeStatSize(queuePath);
138
167
  const pendingBytes = Math.max(0, queueSize - Number(afterState.offset || 0));
139
168
 
169
+ if (pendingBytes <= 0) {
170
+ await clearAutoRetry(trackerDir);
171
+ } else if (opts.auto && uploadAttempted) {
172
+ const retryAtMs = Number(uploadThrottleState?.nextAllowedAtMs || 0);
173
+ if (retryAtMs > Date.now()) {
174
+ await scheduleAutoRetry({
175
+ trackerDir,
176
+ retryAtMs,
177
+ reason: 'backlog',
178
+ pendingBytes,
179
+ source: 'auto-backlog'
180
+ });
181
+ }
182
+ }
183
+
140
184
  await maybeSendHeartbeat({
141
185
  baseUrl,
142
186
  deviceToken,
@@ -174,12 +218,14 @@ function parseArgs(argv) {
174
218
  const out = {
175
219
  auto: false,
176
220
  fromNotify: false,
221
+ fromRetry: false,
177
222
  drain: false
178
223
  };
179
224
  for (let i = 0; i < argv.length; i++) {
180
225
  const a = argv[i];
181
226
  if (a === '--auto') out.auto = true;
182
227
  else if (a === '--from-notify') out.fromNotify = true;
228
+ else if (a === '--from-retry') out.fromRetry = true;
183
229
  else if (a === '--drain') out.drain = true;
184
230
  else throw new Error(`Unknown option: ${a}`);
185
231
  }
@@ -219,5 +265,103 @@ async function maybeSendHeartbeat({ baseUrl, deviceToken, trackerDir, uploadResu
219
265
  }
220
266
  }
221
267
 
268
+ function deriveAutoSkipReason({ decision, state }) {
269
+ if (!decision || decision.reason !== 'throttled') return decision?.reason || 'unknown';
270
+ const backoffUntilMs = Number(state?.backoffUntilMs || 0);
271
+ const nextAllowedAtMs = Number(state?.nextAllowedAtMs || 0);
272
+ if (backoffUntilMs > 0 && backoffUntilMs >= nextAllowedAtMs) return 'backoff';
273
+ return 'throttled';
274
+ }
275
+
276
+ async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes, source }) {
277
+ const retryMs = coerceRetryMs(retryAtMs);
278
+ if (!retryMs) return { scheduled: false, retryAtMs: 0 };
279
+
280
+ const retryPath = path.join(trackerDir, AUTO_RETRY_FILENAME);
281
+ const nowMs = Date.now();
282
+ const existing = await readJson(retryPath);
283
+ const existingMs = coerceRetryMs(existing?.retryAtMs);
284
+ if (existingMs && existingMs >= retryMs - 1000) {
285
+ return { scheduled: false, retryAtMs: existingMs };
286
+ }
287
+
288
+ const payload = {
289
+ version: 1,
290
+ retryAtMs: retryMs,
291
+ retryAt: new Date(retryMs).toISOString(),
292
+ reason: typeof reason === 'string' && reason.length > 0 ? reason : 'throttled',
293
+ pendingBytes: Math.max(0, Number(pendingBytes || 0)),
294
+ scheduledAt: new Date(nowMs).toISOString(),
295
+ source: typeof source === 'string' ? source : 'auto'
296
+ };
297
+
298
+ await writeJson(retryPath, payload);
299
+
300
+ const delayMs = Math.min(AUTO_RETRY_MAX_DELAY_MS, Math.max(0, retryMs - nowMs));
301
+ if (delayMs <= 0) return { scheduled: false, retryAtMs: retryMs };
302
+ if (process.env.VIBESCORE_AUTO_RETRY_NO_SPAWN === '1') {
303
+ return { scheduled: false, retryAtMs: retryMs };
304
+ }
305
+
306
+ spawnAutoRetryProcess({
307
+ retryPath,
308
+ trackerBinPath: path.join(trackerDir, 'app', 'bin', 'tracker.js'),
309
+ fallbackPkg: '@vibescore/tracker',
310
+ delayMs
311
+ });
312
+ return { scheduled: true, retryAtMs: retryMs };
313
+ }
314
+
315
+ async function clearAutoRetry(trackerDir) {
316
+ const retryPath = path.join(trackerDir, AUTO_RETRY_FILENAME);
317
+ await fs.unlink(retryPath).catch(() => {});
318
+ }
319
+
320
+ function spawnAutoRetryProcess({ retryPath, trackerBinPath, fallbackPkg, delayMs }) {
321
+ const script = buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs });
322
+ try {
323
+ const child = cp.spawn(process.execPath, ['-e', script], {
324
+ detached: true,
325
+ stdio: 'ignore',
326
+ env: process.env
327
+ });
328
+ child.unref();
329
+ } catch (_e) {}
330
+ }
331
+
332
+ function buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs }) {
333
+ return `'use strict';\n` +
334
+ `const fs = require('node:fs');\n` +
335
+ `const cp = require('node:child_process');\n` +
336
+ `const retryPath = ${JSON.stringify(retryPath)};\n` +
337
+ `const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` +
338
+ `const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
339
+ `const delayMs = ${Math.max(0, Math.floor(delayMs || 0))};\n` +
340
+ `setTimeout(() => {\n` +
341
+ ` let retryAtMs = 0;\n` +
342
+ ` try {\n` +
343
+ ` const raw = fs.readFileSync(retryPath, 'utf8');\n` +
344
+ ` retryAtMs = Number(JSON.parse(raw).retryAtMs || 0);\n` +
345
+ ` } catch (_) {}\n` +
346
+ ` if (!retryAtMs || Date.now() + 1000 < retryAtMs) process.exit(0);\n` +
347
+ ` const argv = ['sync', '--auto', '--from-retry'];\n` +
348
+ ` const cmd = fs.existsSync(trackerBinPath)\n` +
349
+ ` ? [process.execPath, trackerBinPath, ...argv]\n` +
350
+ ` : ['npx', '--yes', fallbackPkg, ...argv];\n` +
351
+ ` try {\n` +
352
+ ` const child = cp.spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore', env: process.env });\n` +
353
+ ` child.unref();\n` +
354
+ ` } catch (_) {}\n` +
355
+ `}, delayMs);\n`;
356
+ }
357
+
358
+ function coerceRetryMs(v) {
359
+ const n = Number(v);
360
+ if (!Number.isFinite(n) || n <= 0) return 0;
361
+ return Math.floor(n);
362
+ }
363
+
222
364
  const HEARTBEAT_MIN_INTERVAL_MINUTES = 30;
223
365
  const HEARTBEAT_MIN_INTERVAL_MS = HEARTBEAT_MIN_INTERVAL_MINUTES * 60 * 1000;
366
+ const AUTO_RETRY_FILENAME = 'auto.retry.json';
367
+ const AUTO_RETRY_MAX_DELAY_MS = 2 * 60 * 60 * 1000;
@@ -10,7 +10,7 @@ async function beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs, open }) {
10
10
 
11
11
  const { callbackUrl, waitForCallback } = await startLocalCallbackServer({ callbackPath, timeoutMs });
12
12
 
13
- const authUrl = dashboardUrl ? new URL('/connect', dashboardUrl) : new URL('/auth/sign-up', baseUrl);
13
+ const authUrl = dashboardUrl ? new URL('/', dashboardUrl) : new URL('/auth/sign-up', baseUrl);
14
14
  authUrl.searchParams.set('redirect', callbackUrl);
15
15
  if (dashboardUrl && baseUrl && baseUrl !== DEFAULT_BASE_URL) authUrl.searchParams.set('base_url', baseUrl);
16
16
 
@@ -18,12 +18,14 @@ async function collectTrackerDiagnostics({
18
18
  const notifySignalPath = path.join(trackerDir, 'notify.signal');
19
19
  const throttlePath = path.join(trackerDir, 'sync.throttle');
20
20
  const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
21
+ const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
21
22
  const codexConfigPath = path.join(codexHome, 'config.toml');
22
23
 
23
24
  const config = await readJson(configPath);
24
25
  const cursors = await readJson(cursorsPath);
25
26
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
26
27
  const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
28
+ const autoRetry = await readJson(autoRetryPath);
27
29
 
28
30
  const queueSize = await safeStatSize(queuePath);
29
31
  const offsetBytes = Number(queueState.offset || 0);
@@ -37,6 +39,7 @@ async function collectTrackerDiagnostics({
37
39
  const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
38
40
 
39
41
  const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
42
+ const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
40
43
 
41
44
  return {
42
45
  ok: true,
@@ -84,7 +87,18 @@ async function collectTrackerDiagnostics({
84
87
  message: redactError(String(uploadThrottle.lastError), home)
85
88
  }
86
89
  : null
87
- }
90
+ },
91
+ auto_retry: autoRetryAt
92
+ ? {
93
+ next_retry_at: autoRetryAt,
94
+ reason: typeof autoRetry?.reason === 'string' ? autoRetry.reason : null,
95
+ pending_bytes: Number.isFinite(Number(autoRetry?.pendingBytes))
96
+ ? Math.max(0, Number(autoRetry.pendingBytes))
97
+ : null,
98
+ scheduled_at: typeof autoRetry?.scheduledAt === 'string' ? autoRetry.scheduledAt : null,
99
+ source: typeof autoRetry?.source === 'string' ? autoRetry.source : null
100
+ }
101
+ : null
88
102
  };
89
103
  }
90
104
 
@@ -135,4 +149,3 @@ function parseEpochMsToIso(v) {
135
149
  }
136
150
 
137
151
  module.exports = { collectTrackerDiagnostics };
138
-