@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 +1 -1
- package/src/api.js +28 -7
- package/src/hooks.js +17 -10
- package/src/sync.js +15 -8
package/package.json
CHANGED
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':
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
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 +=
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.`);
|