@vibe-cafe/vibe-usage 0.2.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -12,13 +12,14 @@ const INITIAL_DELAY = 1000;
12
12
  * @param {string} apiUrl - Base URL (e.g. "https://vibecafe.ai")
13
13
  * @param {string} apiKey - Bearer token (vbu_xxx)
14
14
  * @param {Array} buckets - Array of usage bucket objects
15
+ * @param {{onProgress?: (sent: number, total: number) => void}} [opts]
15
16
  * @returns {Promise<{ingested: number}>}
16
17
  */
17
- export async function ingest(apiUrl, apiKey, buckets) {
18
+ export async function ingest(apiUrl, apiKey, buckets, opts) {
18
19
  let lastError;
19
20
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
20
21
  try {
21
- return await _send(apiUrl, apiKey, buckets);
22
+ return await _send(apiUrl, apiKey, buckets, opts?.onProgress);
22
23
  } catch (err) {
23
24
  lastError = err;
24
25
  // Don't retry auth errors or client errors
@@ -34,10 +35,11 @@ export async function ingest(apiUrl, apiKey, buckets) {
34
35
  throw lastError;
35
36
  }
36
37
 
37
- function _send(apiUrl, apiKey, buckets) {
38
+ function _send(apiUrl, apiKey, buckets, onProgress) {
38
39
  return new Promise((resolve, reject) => {
39
40
  const url = new URL('/api/usage/ingest', apiUrl);
40
- const body = JSON.stringify({ buckets });
41
+ const body = Buffer.from(JSON.stringify({ buckets }));
42
+ const totalBytes = body.length;
41
43
  const mod = url.protocol === 'https:' ? https : http;
42
44
 
43
45
  const req = mod.request(url, {
@@ -46,7 +48,7 @@ function _send(apiUrl, apiKey, buckets) {
46
48
  headers: {
47
49
  'Content-Type': 'application/json',
48
50
  'Authorization': `Bearer ${apiKey}`,
49
- 'Content-Length': Buffer.byteLength(body),
51
+ 'Content-Length': totalBytes,
50
52
  },
51
53
  }, (res) => {
52
54
  let data = '';
@@ -75,7 +77,26 @@ function _send(apiUrl, apiKey, buckets) {
75
77
  req.destroy();
76
78
  reject(new Error('Request timed out (60s)'));
77
79
  });
78
- req.write(body);
79
- req.end();
80
+
81
+ // Write body in chunks to report upload progress
82
+ const CHUNK = 16 * 1024;
83
+ let sent = 0;
84
+
85
+ function writeNext() {
86
+ let ok = true;
87
+ while (ok && sent < totalBytes) {
88
+ const slice = body.subarray(sent, sent + CHUNK);
89
+ sent += slice.length;
90
+ if (onProgress) onProgress(sent, totalBytes);
91
+ ok = req.write(slice);
92
+ }
93
+ if (sent < totalBytes) {
94
+ req.once('drain', writeNext);
95
+ } else {
96
+ req.end();
97
+ }
98
+ }
99
+
100
+ writeNext();
80
101
  });
81
102
  }
package/src/hooks.js CHANGED
@@ -85,26 +85,36 @@ export function injectCodex() {
85
85
  mkdirSync(dirname(configPath), { recursive: true });
86
86
  }
87
87
 
88
+ const notifyLine = `notify = "sh -c \\"${SYNC_CMD}\\""`;
89
+
88
90
  if (content.includes('vibe-usage')) {
89
- // Fix broken [notify] [[notify]] from previous versions
90
- content = content.replace(/^\[notify\]$/gm, '[[notify]]');
91
- // Update existing command to use latest
91
+ // Migrate broken [[notify]] / [notify] table format and array format from previous versions
92
+ // to correct string format: notify = "sh -c \"...\""
93
+ content = content.replace(
94
+ /^\[\[?notify\]\]?\n(?:command\s*=\s*["'][^"']*["']\n?)?/gm,
95
+ notifyLine + '\n',
96
+ );
97
+ // Migrate array format: notify = ["sh", "-c", "..."]
98
+ content = content.replace(
99
+ /^notify\s*=\s*\[.*vibe-usage.*\]$/gm,
100
+ notifyLine,
101
+ );
102
+ // Update existing string format notify = "..." to use latest command
92
103
  content = content.replace(
93
- /npx @vibe-cafe\/vibe-usage(?:@[\d.]+)? sync[^"']*/g,
94
- SYNC_CMD,
104
+ /^notify\s*=\s*".*vibe-usage.*"$/gm,
105
+ notifyLine,
95
106
  );
96
107
  writeFileSync(configPath, content, 'utf-8');
97
108
  return { injected: false, reason: 'already installed (updated)' };
98
109
  }
99
110
 
100
- const notifySection = `\n[[notify]]\ncommand = "${SYNC_CMD}"\n`;
101
- const notifyIdx = content.indexOf('[[notify]]');
102
- if (notifyIdx !== -1) {
103
- const nextSection = content.indexOf('\n[', notifyIdx + 1);
104
- const sectionEnd = nextSection === -1 ? content.length : nextSection;
105
- content = content.slice(0, notifyIdx) + `[[notify]]\ncommand = "${SYNC_CMD}"` + content.slice(sectionEnd);
111
+ // Check if any notify line already exists
112
+ const hasNotify = /^notify\s*=/m.test(content);
113
+ if (hasNotify) {
114
+ // Replace existing notify value
115
+ content = content.replace(/^notify\s*=\s*.+$/gm, notifyLine);
106
116
  } else {
107
- content += notifySection;
117
+ content += `\n${notifyLine}\n`;
108
118
  }
109
119
 
110
120
  writeFileSync(configPath, content, 'utf-8');
@@ -72,6 +72,8 @@ export async function parse(lastSync) {
72
72
  } catch { break; }
73
73
  }
74
74
 
75
+ // Track model from turn_context events (fallback when token_count lacks model)
76
+ let turnContextModel = 'unknown';
75
77
  // Track previous cumulative totals per model to compute deltas when only total_token_usage is available
76
78
  const prevTotal = new Map();
77
79
  for (const line of content.split('\n')) {
@@ -83,7 +85,15 @@ export async function parse(lastSync) {
83
85
  if (obj.type !== 'event_msg') continue;
84
86
 
85
87
  const payload = obj.payload;
86
- if (!payload || payload.type !== 'token_count') continue;
88
+ if (!payload) continue;
89
+
90
+ // Capture model from turn_context events
91
+ if (payload.type === 'turn_context' && payload.model) {
92
+ turnContextModel = payload.model;
93
+ continue;
94
+ }
95
+
96
+ if (payload.type !== 'token_count') continue;
87
97
 
88
98
  const info = payload.info;
89
99
  if (!info) continue;
@@ -113,7 +123,7 @@ export async function parse(lastSync) {
113
123
  }
114
124
  if (!usage) continue;
115
125
 
116
- const model = info.model || payload.model || sessionModel;
126
+ const model = info.model || payload.model || turnContextModel || sessionModel;
117
127
 
118
128
  entries.push({
119
129
  source: 'codex',
package/src/sync.js CHANGED
@@ -9,6 +9,12 @@ import { TOOLS } from './hooks.js';
9
9
 
10
10
  const BATCH_SIZE = 500;
11
11
 
12
+ function formatBytes(bytes) {
13
+ if (bytes < 1024) return `${bytes}B`;
14
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
15
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
16
+ }
17
+
12
18
  export async function runSync() {
13
19
  // Self-heal: re-inject any missing hooks before syncing
14
20
  ensureHooks();
@@ -54,13 +60,14 @@ export async function runSync() {
54
60
  for (let i = 0; i < allBuckets.length; i += BATCH_SIZE) {
55
61
  const batch = allBuckets.slice(i, i + BATCH_SIZE);
56
62
  const batchNum = Math.floor(i / BATCH_SIZE) + 1;
57
- const uploaded = Math.min(i + BATCH_SIZE, allBuckets.length);
58
-
59
- if (totalBatches > 1) {
60
- process.stdout.write(` [${batchNum}/${totalBatches}] ${uploaded}/${allBuckets.length} buckets...\r`);
61
- }
62
-
63
- const result = await ingest(apiUrl, config.apiKey, batch);
63
+ const prefix = totalBatches > 1 ? ` [${batchNum}/${totalBatches}] ` : ' ';
64
+
65
+ const result = await ingest(apiUrl, config.apiKey, batch, {
66
+ onProgress(sent, total) {
67
+ const pct = Math.round((sent / total) * 100);
68
+ process.stdout.write(`${prefix}${formatBytes(sent)}/${formatBytes(total)} (${pct}%)\r`);
69
+ },
70
+ });
64
71
  totalIngested += result.ingested ?? batch.length;
65
72
 
66
73
  // Save progress after each successful batch so partial uploads survive interruptions
@@ -68,7 +75,7 @@ export async function runSync() {
68
75
  saveConfig(config);
69
76
  }
70
77
 
71
- if (totalBatches > 1) {
78
+ if (totalBatches > 1 || allBuckets.length > 0) {
72
79
  process.stdout.write('\n');
73
80
  }
74
81
  console.log(`Synced ${totalIngested} buckets.`);