@vibe-cafe/vibe-usage 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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,24 +85,31 @@ 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
- // Update existing command to use latest
91
+ // Migrate broken [[notify]] / [notify] table format from previous versions
92
+ // to correct inline array format: notify = ["sh", "-c", "..."]
93
+ content = content.replace(
94
+ /^\[\[?notify\]\]?\n(?:command\s*=\s*["'][^"']*["']\n?)?/gm,
95
+ notifyLine + '\n',
96
+ );
97
+ // Also update existing inline notify = [...] to use latest command
90
98
  content = content.replace(
91
- /npx @vibe-cafe\/vibe-usage(?:@[\d.]+)? sync[^"']*/g,
92
- SYNC_CMD,
99
+ /^notify\s*=\s*\[.*vibe-usage.*\]$/gm,
100
+ notifyLine,
93
101
  );
94
102
  writeFileSync(configPath, content, 'utf-8');
95
103
  return { injected: false, reason: 'already installed (updated)' };
96
104
  }
97
105
 
98
- const notifySection = `\n[notify]\ncommand = "${SYNC_CMD}"\n`;
99
- const notifyIdx = content.indexOf('[notify]');
100
- if (notifyIdx !== -1) {
101
- const nextSection = content.indexOf('\n[', notifyIdx + 1);
102
- const sectionEnd = nextSection === -1 ? content.length : nextSection;
103
- content = content.slice(0, notifyIdx) + `[notify]\ncommand = "${SYNC_CMD}"` + content.slice(sectionEnd);
106
+ // Check if any notify line already exists
107
+ const hasNotify = /^notify\s*=/m.test(content);
108
+ if (hasNotify) {
109
+ // Append our command to existing notify (replace it)
110
+ content = content.replace(/^notify\s*=\s*\[.*\]$/gm, notifyLine);
104
111
  } else {
105
- content += notifySection;
112
+ content += `\n${notifyLine}\n`;
106
113
  }
107
114
 
108
115
  writeFileSync(configPath, content, 'utf-8');
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.`);