@vibe-cafe/vibe-usage 0.9.7 → 0.9.9

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
@@ -63,7 +63,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
63
63
  | Amp | `~/.local/share/amp/threads/` |
64
64
  | Droid | `~/.factory/sessions/` |
65
65
  | Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
66
- | Kiro | Kiro CLI native event streams `~/.kiro/sessions/cli/*.jsonl` (estimated tokens from message text: input = prompt + tool results, output = reply + tool calls, reasoning = thinking, cacheRead = re-sent context; thinking-block signatures excluded). Falls back to `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` + optional `~/.kiro_sessions/*.json` archives, then IDE `q-client.log` credit deltas as `kiro-credits`; legacy IDE `dev_data/devdata.sqlite` token telemetry is opt-in with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
66
+ | Kiro | Kiro CLI native event streams `~/.kiro/sessions/cli/*.jsonl` (estimated tokens from message text: input = prompt + tool results, output = reply + tool calls, reasoning = thinking, cacheRead = re-sent context; thinking-block signatures excluded). Falls back to `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` + optional `~/.kiro_sessions/*.json` archives, then IDE `q-client.log` whole-credit deltas as `kiro-credits` (floored cumulative diff — the server stores token counts as bigint); legacy IDE `dev_data/devdata.sqlite` token telemetry is opt-in with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
67
67
  | Cline | `<host>/User/globalStorage/saoudrizwan.claude-dev/{state/taskHistory.json,tasks/<id>/ui_messages.json}` (walks all VSCode-fork hosts: Code, Cursor, Windsurf, VSCodium, Trae, ...) |
68
68
  | Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
69
69
  | Antigravity | `~/.gemini/antigravity/conversations/*.pb` to discover cascades, then reads token usage + sessions from the running language server via Connect RPC (process discovered with `ps`/`lsof` on macOS/Linux, PowerShell CIM with a `wmic` fallback on Windows) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -95,20 +95,34 @@ const FETCH_TIMEOUT_MS = 10_000;
95
95
  async function fetchUsageCsv(token) {
96
96
  const url = `${(process.env.CURSOR_WEB_BASE_URL?.trim() || 'https://cursor.com').replace(/\/+$/, '')}/api/dashboard/export-usage-events-csv?strategy=tokens`;
97
97
  const sub = decodeJwtSub(token);
98
- const cookieValues = sub ? [token, `${sub}::${token}`] : [token];
98
+ // The dashboard API authenticates via the WorkosCursorSessionToken cookie in
99
+ // `{sub}%3A%3A{jwt}` form (what the browser sends). Bearer and bare-token
100
+ // cookies now return 401, so they're kept only as last-resort fallbacks.
101
+ const userId = sub?.includes('|') ? sub.split('|').pop() : null;
102
+ const cookieValues = [
103
+ ...(sub ? [`${sub}%3A%3A${token}`] : []),
104
+ ...(userId ? [`${userId}%3A%3A${token}`] : []),
105
+ token,
106
+ ];
99
107
 
100
- const attempts = [{ Authorization: `Bearer ${token}` }];
101
- for (const cv of cookieValues) {
102
- attempts.push({ Cookie: `${SESSION_COOKIE}=${cv}` });
103
- attempts.push({ Authorization: `Bearer ${token}`, Cookie: `${SESSION_COOKIE}=${cv}` });
104
- }
108
+ // Browser-mimicking headers, matching what the dashboard sends (and what
109
+ // cursor-stats / cursor-price-tracking send) — Node's default UA is a
110
+ // common target for intermittent WAF blocks on cursor.com.
111
+ const baseHeaders = {
112
+ Accept: 'text/csv,*/*;q=0.8',
113
+ Origin: 'https://cursor.com',
114
+ Referer: 'https://cursor.com/dashboard?tab=usage',
115
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
116
+ };
117
+ const attempts = cookieValues.map(cv => ({ Cookie: `${SESSION_COOKIE}=${cv}` }));
118
+ attempts.push({ Authorization: `Bearer ${token}` });
105
119
 
106
120
  const failures = [];
107
121
  for (const headers of attempts) {
108
122
  let resp;
109
123
  try {
110
124
  resp = await fetch(url, {
111
- headers: { Accept: 'text/csv,*/*;q=0.8', ...headers },
125
+ headers: { ...baseHeaders, ...headers },
112
126
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
113
127
  });
114
128
  } catch (e) {
@@ -121,9 +135,18 @@ async function fetchUsageCsv(token) {
121
135
  }
122
136
  if (resp.ok) return await resp.text();
123
137
  failures.push(`${resp.status} ${resp.statusText}`);
138
+ // Only auth rejections are worth retrying with different credentials.
139
+ // 429/5xx are transient server-side states — soft-skip like network errors
140
+ // instead of surfacing them as auth failures every daemon cycle.
141
+ if (resp.status !== 401 && resp.status !== 403) {
142
+ const err = new Error(`Cursor usage export skipped (HTTP ${resp.status} ${resp.statusText})`);
143
+ err.skip = true;
144
+ throw err;
145
+ }
124
146
  }
125
- // All auth combos rejected — token is likely expired. Surface to user.
126
- throw new Error(`Cursor usage export auth failed (${failures.join('; ')})`);
147
+ // Every auth combo rejected — the stored token no longer works. Surface an
148
+ // actionable message: re-signing in inside Cursor rewrites the token.
149
+ throw new Error(`Cursor session rejected (${failures.join('; ')}). Open Cursor and sign in again (Cursor Settings → Account), then re-run sync.`);
127
150
  }
128
151
 
129
152
  function parseCsv(text) {
@@ -46,24 +46,43 @@ export function roundToHalfHour(date) {
46
46
  return d;
47
47
  }
48
48
 
49
+ // Server column limits (usage_buckets: model varchar(100), project varchar(200)).
50
+ // Anything longer aborts the whole INSERT chunk with 22001, so clamp here.
51
+ const MODEL_MAX_LENGTH = 100;
52
+ const PROJECT_MAX_LENGTH = 200;
53
+
54
+ // Server token columns are bigint — a single fractional/NaN value aborts the
55
+ // whole INSERT chunk with 22P02, taking every other tool's rows in the batch
56
+ // down with it.
57
+ function toTokenCount(value) {
58
+ const n = Number(value);
59
+ if (!Number.isFinite(n) || n <= 0) return 0;
60
+ return Math.round(n);
61
+ }
62
+
49
63
  export function aggregateToBuckets(entries) {
50
64
  const map = new Map();
51
65
 
52
66
  for (const e of entries) {
67
+ const model = String(e.model || 'unknown').slice(0, MODEL_MAX_LENGTH);
68
+ const project = String(e.project || 'unknown').slice(0, PROJECT_MAX_LENGTH);
53
69
  const bucketStart = roundToHalfHour(e.timestamp).toISOString();
54
- const key = `${e.source}|${e.model}|${e.project}|${bucketStart}`;
70
+ const key = `${e.source}|${model}|${project}|${e.hostname || ''}|${bucketStart}`;
55
71
 
56
72
  if (!map.has(key)) {
57
73
  map.set(key, {
58
74
  source: e.source,
59
- model: e.model,
60
- project: e.project,
75
+ model,
76
+ project,
77
+ // Cloud-sourced parsers (cursor) pre-set a fixed hostname sentinel; it
78
+ // must survive aggregation, or sync.js stamps the machine hostname and
79
+ // every machine gets its own duplicate row server-side.
80
+ ...(e.hostname ? { hostname: e.hostname } : {}),
61
81
  bucketStart,
62
82
  inputTokens: 0,
63
83
  outputTokens: 0,
64
84
  cachedInputTokens: 0,
65
85
  reasoningOutputTokens: 0,
66
- totalTokens: 0,
67
86
  });
68
87
  }
69
88
 
@@ -72,10 +91,24 @@ export function aggregateToBuckets(entries) {
72
91
  b.outputTokens += e.outputTokens || 0;
73
92
  b.cachedInputTokens += e.cachedInputTokens || 0;
74
93
  b.reasoningOutputTokens += e.reasoningOutputTokens || 0;
75
- b.totalTokens += (e.inputTokens || 0) + (e.outputTokens || 0) + (e.reasoningOutputTokens || 0);
76
94
  }
77
95
 
78
- return Array.from(map.values());
96
+ // Clamp after summation, not per entry — rounding each entry first would
97
+ // discard sub-integer values instead of letting them accumulate.
98
+ return Array.from(map.values()).map((b) => {
99
+ const inputTokens = toTokenCount(b.inputTokens);
100
+ const outputTokens = toTokenCount(b.outputTokens);
101
+ const cachedInputTokens = toTokenCount(b.cachedInputTokens);
102
+ const reasoningOutputTokens = toTokenCount(b.reasoningOutputTokens);
103
+ return {
104
+ ...b,
105
+ inputTokens,
106
+ outputTokens,
107
+ cachedInputTokens,
108
+ reasoningOutputTokens,
109
+ totalTokens: inputTokens + outputTokens + reasoningOutputTokens,
110
+ };
111
+ });
79
112
  }
80
113
 
81
114
  /**
@@ -736,7 +736,12 @@ export function snapshotsToCreditEntries(snapshots) {
736
736
  continue;
737
737
  }
738
738
 
739
- const delta = Number((snapshot.currentUsage - prev.currentUsage).toFixed(4));
739
+ // The server's token columns are bigint, so fractional credit deltas
740
+ // cannot be uploaded as-is. Diff floored cumulative values instead of
741
+ // rounding each delta: the diffs telescope, so sub-integer usage is never
742
+ // lost or double-counted — it surfaces on whichever snapshot crosses the
743
+ // next whole-credit boundary.
744
+ const delta = Math.floor(snapshot.currentUsage) - Math.floor(prev.currentUsage);
740
745
  if (delta > 0) {
741
746
  entries.push({
742
747
  source: 'kiro',