@vibescore/tracker 0.0.2 → 0.0.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/bin/tracker.js CHANGED
@@ -9,10 +9,19 @@ if (debug) process.env.VIBESCORE_DEBUG = '1';
9
9
  run(argv).catch((err) => {
10
10
  console.error(err?.stack || String(err));
11
11
  if (debug) {
12
+ if (typeof err?.status === 'number') {
13
+ console.error(`Status: ${err.status}`);
14
+ }
15
+ if (typeof err?.code === 'string' && err.code.trim()) {
16
+ console.error(`Code: ${err.code.trim()}`);
17
+ }
12
18
  const original = err?.originalMessage;
13
19
  if (original && original !== err?.message) {
14
20
  console.error(`Original error: ${original}`);
15
21
  }
22
+ if (typeof err?.nextActions === 'string' && err.nextActions.trim()) {
23
+ console.error(`Next actions: ${err.nextActions.trim()}`);
24
+ }
16
25
  }
17
26
  process.exitCode = 1;
18
27
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -6,6 +6,7 @@ const { ensureDir, readJson, writeJson, openLock } = require('../lib/fs');
6
6
  const { listRolloutFiles, parseRolloutIncremental } = require('../lib/rollout');
7
7
  const { drainQueueToCloud } = require('../lib/uploader');
8
8
  const { createProgress, renderBar, formatNumber, formatBytes } = require('../lib/progress');
9
+ const { syncHeartbeat } = require('../lib/vibescore-api');
9
10
  const {
10
11
  DEFAULTS: UPLOAD_DEFAULTS,
11
12
  normalizeState: normalizeUploadState,
@@ -130,11 +131,19 @@ async function cmdSync(argv) {
130
131
  progress?.stop();
131
132
  }
132
133
 
133
- if (!opts.auto) {
134
- const afterState = (await readJson(queueStatePath)) || { offset: 0 };
135
- const queueSize = await safeStatSize(queuePath);
136
- const pendingBytes = Math.max(0, queueSize - Number(afterState.offset || 0));
134
+ const afterState = (await readJson(queueStatePath)) || { offset: 0 };
135
+ const queueSize = await safeStatSize(queuePath);
136
+ const pendingBytes = Math.max(0, queueSize - Number(afterState.offset || 0));
137
+
138
+ await maybeSendHeartbeat({
139
+ baseUrl,
140
+ deviceToken,
141
+ trackerDir,
142
+ uploadResult,
143
+ pendingBytes
144
+ });
137
145
 
146
+ if (!opts.auto) {
138
147
  process.stdout.write(
139
148
  [
140
149
  'Sync finished:',
@@ -185,3 +194,28 @@ async function safeStatSize(p) {
185
194
  return 0;
186
195
  }
187
196
  }
197
+
198
+ async function maybeSendHeartbeat({ baseUrl, deviceToken, trackerDir, uploadResult, pendingBytes }) {
199
+ if (!deviceToken || !uploadResult) return;
200
+ if (pendingBytes > 0) return;
201
+ if (Number(uploadResult.inserted || 0) !== 0) return;
202
+
203
+ const heartbeatPath = path.join(trackerDir, 'sync.heartbeat.json');
204
+ const heartbeatState = await readJson(heartbeatPath);
205
+ const lastPingAt = Date.parse(heartbeatState?.lastPingAt || '');
206
+ const nowMs = Date.now();
207
+ if (Number.isFinite(lastPingAt) && nowMs - lastPingAt < HEARTBEAT_MIN_INTERVAL_MS) return;
208
+
209
+ try {
210
+ await syncHeartbeat({ baseUrl, deviceToken });
211
+ await writeJson(heartbeatPath, {
212
+ lastPingAt: new Date(nowMs).toISOString(),
213
+ minIntervalMinutes: HEARTBEAT_MIN_INTERVAL_MINUTES
214
+ });
215
+ } catch (_e) {
216
+ // best-effort heartbeat; ignore failures
217
+ }
218
+ }
219
+
220
+ const HEARTBEAT_MIN_INTERVAL_MINUTES = 30;
221
+ const HEARTBEAT_MIN_INTERVAL_MS = HEARTBEAT_MIN_INTERVAL_MINUTES * 60 * 1000;
@@ -52,10 +52,28 @@ async function ingestEvents({ baseUrl, deviceToken, events }) {
52
52
  };
53
53
  }
54
54
 
55
+ async function syncHeartbeat({ baseUrl, deviceToken }) {
56
+ const data = await invokeFunction({
57
+ baseUrl,
58
+ accessToken: deviceToken,
59
+ slug: 'vibescore-sync-ping',
60
+ method: 'POST',
61
+ body: {},
62
+ errorPrefix: 'Sync heartbeat failed'
63
+ });
64
+
65
+ return {
66
+ updated: Boolean(data?.updated),
67
+ last_sync_at: typeof data?.last_sync_at === 'string' ? data.last_sync_at : null,
68
+ min_interval_minutes: Number(data?.min_interval_minutes || 0)
69
+ };
70
+ }
71
+
55
72
  module.exports = {
56
73
  signInWithPassword,
57
74
  issueDeviceToken,
58
- ingestEvents
75
+ ingestEvents,
76
+ syncHeartbeat
59
77
  };
60
78
 
61
79
  async function invokeFunction({ baseUrl, accessToken, slug, method, body, errorPrefix }) {
@@ -82,17 +100,30 @@ async function invokeFunctionWithRetry({ baseUrl, accessToken, slug, method, bod
82
100
  }
83
101
 
84
102
  function normalizeSdkError(error, errorPrefix) {
85
- const raw = error?.message || String(error || 'Unknown error');
103
+ const raw = extractSdkErrorMessage(error);
86
104
  const msg = normalizeBackendErrorMessage(raw);
87
105
  const err = new Error(errorPrefix ? `${errorPrefix}: ${msg}` : msg);
88
106
  const status = error?.statusCode ?? error?.status;
107
+ const code = typeof error?.error === 'string' ? error.error.trim() : '';
89
108
  if (typeof status === 'number') err.status = status;
109
+ if (code) err.code = code;
90
110
  err.retryable = isRetryableStatus(status) || isRetryableMessage(raw);
91
111
  if (msg !== raw) err.originalMessage = raw;
92
112
  if (error?.nextActions) err.nextActions = error.nextActions;
93
113
  return err;
94
114
  }
95
115
 
116
+ function extractSdkErrorMessage(error) {
117
+ if (!error) return 'Unknown error';
118
+ const message = typeof error.message === 'string' ? error.message.trim() : '';
119
+ const code = typeof error.error === 'string' ? error.error.trim() : '';
120
+ if (message && message !== 'InsForgeError') return message;
121
+ if (code && code !== 'REQUEST_FAILED') return code;
122
+ if (message) return message;
123
+ if (code) return code;
124
+ return String(error);
125
+ }
126
+
96
127
  function normalizeBackendErrorMessage(message) {
97
128
  if (!isBackendRuntimeDownMessage(message)) return String(message || 'Unknown error');
98
129
  return 'Backend runtime unavailable (InsForge). Please retry later.';