@vibe-cafe/vibe-usage 0.2.7 → 0.4.0
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 +18 -6
- package/package.json +1 -1
- package/src/api.js +6 -4
- package/src/daemon.js +37 -0
- package/src/index.js +10 -3
- package/src/init.js +0 -1
- package/src/parsers/claude-code.js +1 -2
- package/src/parsers/codex.js +1 -10
- package/src/parsers/gemini-cli.js +1 -10
- package/src/parsers/openclaw.js +1 -10
- package/src/parsers/opencode.js +5 -19
- package/src/reset.js +42 -18
- package/src/sync.js +14 -8
package/README.md
CHANGED
|
@@ -16,11 +16,13 @@ This will:
|
|
|
16
16
|
## Commands
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npx vibe-usage
|
|
20
|
-
npx vibe-usage init
|
|
21
|
-
npx vibe-usage sync
|
|
22
|
-
npx vibe-usage
|
|
23
|
-
npx vibe-usage
|
|
19
|
+
npx vibe-usage # Init (first run) or sync (subsequent runs)
|
|
20
|
+
npx vibe-usage init # Re-run setup
|
|
21
|
+
npx vibe-usage sync # Manual sync
|
|
22
|
+
npx vibe-usage daemon # Continuous sync (every 5 minutes)
|
|
23
|
+
npx vibe-usage reset # Delete all data and re-upload from local logs
|
|
24
|
+
npx vibe-usage reset --local # Delete this host's data only and re-upload
|
|
25
|
+
npx vibe-usage status # Show config & detected tools
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
## Supported Tools
|
|
@@ -39,12 +41,22 @@ npx vibe-usage status # Show config & detected tools
|
|
|
39
41
|
- Aggregates token usage into 30-minute buckets
|
|
40
42
|
- Uploads to your vibecafe.ai dashboard
|
|
41
43
|
- Only syncs new data since last sync (incremental)
|
|
42
|
-
- For continuous syncing, use the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
|
|
44
|
+
- For continuous syncing, use `npx vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
|
|
43
45
|
|
|
44
46
|
## Config
|
|
45
47
|
|
|
46
48
|
Config stored at `~/.vibe-usage/config.json`. Contains your API key and last sync timestamp.
|
|
47
49
|
|
|
50
|
+
## Daemon Mode
|
|
51
|
+
|
|
52
|
+
Run continuous syncing in the foreground (every 5 minutes):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx vibe-usage daemon
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Press Ctrl+C to stop. For background use: `nohup npx vibe-usage daemon &`
|
|
59
|
+
|
|
48
60
|
## License
|
|
49
61
|
|
|
50
62
|
MIT
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -102,14 +102,16 @@ function _send(apiUrl, apiKey, buckets, onProgress) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* DELETE
|
|
106
|
-
* @param {string} apiUrl
|
|
107
|
-
* @param {string} apiKey
|
|
105
|
+
* DELETE usage data for the authenticated user.
|
|
106
|
+
* @param {string} apiUrl
|
|
107
|
+
* @param {string} apiKey
|
|
108
|
+
* @param {{hostname?: string}} [opts]
|
|
108
109
|
* @returns {Promise<{deleted: number}>}
|
|
109
110
|
*/
|
|
110
|
-
export function deleteAllData(apiUrl, apiKey) {
|
|
111
|
+
export function deleteAllData(apiUrl, apiKey, opts) {
|
|
111
112
|
return new Promise((resolve, reject) => {
|
|
112
113
|
const url = new URL('/api/usage/ingest', apiUrl);
|
|
114
|
+
if (opts?.hostname) url.searchParams.set('hostname', opts.hostname);
|
|
113
115
|
const mod = url.protocol === 'https:' ? https : http;
|
|
114
116
|
|
|
115
117
|
const req = mod.request(url, {
|
package/src/daemon.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { runSync } from './sync.js';
|
|
3
|
+
|
|
4
|
+
const INTERVAL = 5 * 60_000; // 5 minutes
|
|
5
|
+
|
|
6
|
+
function log(msg) {
|
|
7
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
8
|
+
process.stdout.write(`[${ts}] ${msg}\n`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runDaemon() {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
if (!config?.apiKey) {
|
|
18
|
+
console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log('Daemon started (sync every 5m, Ctrl+C to stop)');
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line no-constant-condition
|
|
25
|
+
while (true) {
|
|
26
|
+
try {
|
|
27
|
+
await runSync({ throws: true, quiet: true });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.message === 'UNAUTHORIZED') {
|
|
30
|
+
log('API key invalid. Exiting.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
log(`Sync error: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
await sleep(INTERVAL);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,7 +13,6 @@ async function showStatus() {
|
|
|
13
13
|
console.log(` Config: ${getConfigPath()}`);
|
|
14
14
|
console.log(` API key: ${config.apiKey.slice(0, 8)}...`);
|
|
15
15
|
console.log(` API URL: ${config.apiUrl || 'https://vibecafe.ai'}`);
|
|
16
|
-
console.log(` Last sync: ${config.lastSync || 'never'}`);
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
console.log('\n Detected tools:');
|
|
@@ -35,7 +34,7 @@ async function showStatus() {
|
|
|
35
34
|
console.log();
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl'
|
|
37
|
+
const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl'];
|
|
39
38
|
|
|
40
39
|
function handleConfig(args) {
|
|
41
40
|
const sub = args[0];
|
|
@@ -105,7 +104,13 @@ export async function run(args) {
|
|
|
105
104
|
}
|
|
106
105
|
case 'reset': {
|
|
107
106
|
const { runReset } = await import('./reset.js');
|
|
108
|
-
await runReset();
|
|
107
|
+
await runReset(args.slice(1));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'daemon':
|
|
111
|
+
case '--daemon': {
|
|
112
|
+
const { runDaemon } = await import('./daemon.js');
|
|
113
|
+
await runDaemon();
|
|
109
114
|
break;
|
|
110
115
|
}
|
|
111
116
|
case 'config': {
|
|
@@ -126,7 +131,9 @@ export async function run(args) {
|
|
|
126
131
|
npx vibe-usage Init (first run) or sync
|
|
127
132
|
npx vibe-usage init Set up API key
|
|
128
133
|
npx vibe-usage sync Manually sync usage data
|
|
134
|
+
npx vibe-usage daemon Continuous sync (every 5m)
|
|
129
135
|
npx vibe-usage reset Delete all data and re-upload
|
|
136
|
+
npx vibe-usage reset --local Delete data for this host only and re-upload
|
|
130
137
|
npx vibe-usage status Show config and detected tools
|
|
131
138
|
npx vibe-usage config show Show full config as JSON
|
|
132
139
|
npx vibe-usage config get <key> Get a config value
|
package/src/init.js
CHANGED
|
@@ -35,7 +35,7 @@ export function commitState() {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export async function parse(
|
|
38
|
+
export async function parse() {
|
|
39
39
|
let sessions;
|
|
40
40
|
try {
|
|
41
41
|
sessions = await loadSessionData({ mode: 'display' });
|
|
@@ -50,7 +50,6 @@ export async function parse(lastSync) {
|
|
|
50
50
|
const entries = [];
|
|
51
51
|
|
|
52
52
|
for (const session of sessions) {
|
|
53
|
-
if (lastSync && new Date(session.lastActivity) <= new Date(lastSync)) continue;
|
|
54
53
|
|
|
55
54
|
const project = resolveProject(session);
|
|
56
55
|
const sessionKey = `${session.projectPath}\0${session.sessionId}`;
|
package/src/parsers/codex.js
CHANGED
|
@@ -27,21 +27,13 @@ function findJsonlFiles(dir) {
|
|
|
27
27
|
return results;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export async function parse(
|
|
30
|
+
export async function parse() {
|
|
31
31
|
if (!existsSync(SESSIONS_DIR)) return [];
|
|
32
32
|
|
|
33
33
|
const entries = [];
|
|
34
34
|
const files = findJsonlFiles(SESSIONS_DIR);
|
|
35
35
|
if (files.length === 0) return [];
|
|
36
36
|
for (const filePath of files) {
|
|
37
|
-
if (lastSync) {
|
|
38
|
-
try {
|
|
39
|
-
const stat = statSync(filePath);
|
|
40
|
-
if (stat.mtime <= new Date(lastSync)) continue;
|
|
41
|
-
} catch {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
37
|
|
|
46
38
|
let content;
|
|
47
39
|
try {
|
|
@@ -99,7 +91,6 @@ export async function parse(lastSync) {
|
|
|
99
91
|
|
|
100
92
|
const timestamp = obj.timestamp ? new Date(obj.timestamp) : null;
|
|
101
93
|
if (!timestamp || isNaN(timestamp.getTime())) continue;
|
|
102
|
-
if (lastSync && timestamp <= new Date(lastSync)) continue;
|
|
103
94
|
|
|
104
95
|
// Prefer incremental per-request usage; compute delta from cumulative total as fallback
|
|
105
96
|
let usage = info.last_token_usage;
|
|
@@ -30,21 +30,13 @@ function findSessionFiles(baseDir) {
|
|
|
30
30
|
return results;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export async function parse(
|
|
33
|
+
export async function parse() {
|
|
34
34
|
const sessionFiles = findSessionFiles(TMP_DIR);
|
|
35
35
|
if (sessionFiles.length === 0) return [];
|
|
36
36
|
|
|
37
37
|
const entries = [];
|
|
38
38
|
|
|
39
39
|
for (const filePath of sessionFiles) {
|
|
40
|
-
if (lastSync) {
|
|
41
|
-
try {
|
|
42
|
-
const stat = statSync(filePath);
|
|
43
|
-
if (stat.mtime <= new Date(lastSync)) continue;
|
|
44
|
-
} catch {
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
40
|
|
|
49
41
|
let data;
|
|
50
42
|
try {
|
|
@@ -65,7 +57,6 @@ export async function parse(lastSync) {
|
|
|
65
57
|
if (!timestamp) continue;
|
|
66
58
|
const ts = new Date(timestamp);
|
|
67
59
|
if (isNaN(ts.getTime())) continue;
|
|
68
|
-
if (lastSync && ts <= new Date(lastSync)) continue;
|
|
69
60
|
|
|
70
61
|
if (tokens) {
|
|
71
62
|
// New format: { input, output, cached, thoughts, tool, total }
|
package/src/parsers/openclaw.js
CHANGED
|
@@ -20,7 +20,7 @@ function getTokens(usage, ...keys) {
|
|
|
20
20
|
return 0;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export async function parse(
|
|
23
|
+
export async function parse() {
|
|
24
24
|
const entries = [];
|
|
25
25
|
|
|
26
26
|
for (const root of POSSIBLE_ROOTS) {
|
|
@@ -49,14 +49,6 @@ export async function parse(lastSync) {
|
|
|
49
49
|
|
|
50
50
|
for (const file of files) {
|
|
51
51
|
const filePath = join(sessionsDir, file);
|
|
52
|
-
if (lastSync) {
|
|
53
|
-
try {
|
|
54
|
-
const stat = statSync(filePath);
|
|
55
|
-
if (stat.mtime <= new Date(lastSync)) continue;
|
|
56
|
-
} catch {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
52
|
|
|
61
53
|
let content;
|
|
62
54
|
try {
|
|
@@ -82,7 +74,6 @@ export async function parse(lastSync) {
|
|
|
82
74
|
if (!timestamp) continue;
|
|
83
75
|
const ts = new Date(typeof timestamp === 'number' ? timestamp : timestamp);
|
|
84
76
|
if (isNaN(ts.getTime())) continue;
|
|
85
|
-
if (lastSync && ts <= new Date(lastSync)) continue;
|
|
86
77
|
|
|
87
78
|
entries.push({
|
|
88
79
|
source: 'openclaw',
|
package/src/parsers/opencode.js
CHANGED
|
@@ -12,26 +12,22 @@ const MESSAGES_DIR = join(DATA_DIR, 'storage', 'message');
|
|
|
12
12
|
* Parse opencode usage data.
|
|
13
13
|
* Tries SQLite database first (opencode >= v0.2), falls back to legacy JSON files.
|
|
14
14
|
*/
|
|
15
|
-
export async function parse(
|
|
15
|
+
export async function parse() {
|
|
16
16
|
if (existsSync(DB_PATH)) {
|
|
17
17
|
try {
|
|
18
|
-
return parseFromSqlite(
|
|
18
|
+
return parseFromSqlite();
|
|
19
19
|
} catch (err) {
|
|
20
20
|
process.stderr.write(`warn: opencode sqlite parse failed (${err.message}), trying legacy json...\n`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
return parseFromJson(
|
|
23
|
+
return parseFromJson();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function parseFromSqlite(
|
|
26
|
+
function parseFromSqlite() {
|
|
27
27
|
// Build WHERE clause: only messages with token data
|
|
28
28
|
const conditions = [
|
|
29
29
|
"(json_extract(data, '$.tokens.input') > 0 OR json_extract(data, '$.tokens.output') > 0)",
|
|
30
30
|
];
|
|
31
|
-
if (lastSync) {
|
|
32
|
-
const sinceMs = new Date(lastSync).getTime();
|
|
33
|
-
conditions.push(`time_created > ${sinceMs}`);
|
|
34
|
-
}
|
|
35
31
|
|
|
36
32
|
const query = `SELECT data FROM message WHERE ${conditions.join(' AND ')}`;
|
|
37
33
|
|
|
@@ -76,7 +72,6 @@ function parseFromSqlite(lastSync) {
|
|
|
76
72
|
|
|
77
73
|
const timestamp = new Date(data.time?.created);
|
|
78
74
|
if (isNaN(timestamp.getTime())) continue;
|
|
79
|
-
if (lastSync && timestamp <= new Date(lastSync)) continue;
|
|
80
75
|
|
|
81
76
|
const rootPath = data.path?.root;
|
|
82
77
|
const project = rootPath ? basename(rootPath) : 'unknown';
|
|
@@ -97,7 +92,7 @@ function parseFromSqlite(lastSync) {
|
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
/** Legacy parser: reads JSON files from storage/message directories. */
|
|
100
|
-
function parseFromJson(
|
|
95
|
+
function parseFromJson() {
|
|
101
96
|
if (!existsSync(MESSAGES_DIR)) return [];
|
|
102
97
|
|
|
103
98
|
const entries = [];
|
|
@@ -120,14 +115,6 @@ function parseFromJson(lastSync) {
|
|
|
120
115
|
|
|
121
116
|
for (const file of msgFiles) {
|
|
122
117
|
const filePath = join(sessionPath, file);
|
|
123
|
-
if (lastSync) {
|
|
124
|
-
try {
|
|
125
|
-
const stat = statSync(filePath);
|
|
126
|
-
if (stat.mtime <= new Date(lastSync)) continue;
|
|
127
|
-
} catch {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
118
|
|
|
132
119
|
let data;
|
|
133
120
|
try {
|
|
@@ -144,7 +131,6 @@ function parseFromJson(lastSync) {
|
|
|
144
131
|
|
|
145
132
|
const timestamp = new Date(data.time?.created);
|
|
146
133
|
if (isNaN(timestamp.getTime())) continue;
|
|
147
|
-
if (lastSync && timestamp <= new Date(lastSync)) continue;
|
|
148
134
|
|
|
149
135
|
const rootPath = data.path?.root;
|
|
150
136
|
const project = rootPath ? basename(rootPath) : 'unknown';
|
package/src/reset.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
4
|
+
import { homedir, hostname as getHostname } from 'node:os';
|
|
5
5
|
import { loadConfig, saveConfig } from './config.js';
|
|
6
6
|
import { deleteAllData } from './api.js';
|
|
7
7
|
import { runSync } from './sync.js';
|
|
@@ -20,33 +20,57 @@ function prompt(question) {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export async function runReset() {
|
|
23
|
+
export async function runReset(args = []) {
|
|
24
|
+
const hostOnly = args.includes('--local');
|
|
24
25
|
const config = loadConfig();
|
|
25
26
|
if (!config?.apiKey) {
|
|
26
27
|
console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
|
|
27
28
|
process.exit(1);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const
|
|
31
|
-
if (answer.toLowerCase() !== 'y') {
|
|
32
|
-
console.log('Cancelled.');
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
31
|
+
const currentHost = getHostname();
|
|
36
32
|
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
if (hostOnly) {
|
|
35
|
+
const answer = await prompt(`This will delete usage data for this host (${currentHost}) and re-upload from local logs. Continue? (y/N) `);
|
|
36
|
+
if (answer.toLowerCase() !== 'y') {
|
|
37
|
+
console.log('Cancelled.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 1. Delete remote data for this host
|
|
42
|
+
console.log(`Deleting remote data for host: ${currentHost}...`);
|
|
43
|
+
try {
|
|
44
|
+
const result = await deleteAllData(apiUrl, config.apiKey, { hostname: currentHost });
|
|
45
|
+
console.log(`Deleted ${result.deleted} buckets from server.`);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.message === 'UNAUTHORIZED') {
|
|
48
|
+
console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.error(`Failed to delete remote data: ${err.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
const answer = await prompt('This will delete ALL your usage data and re-upload from local logs. Continue? (y/N) ');
|
|
56
|
+
if (answer.toLowerCase() !== 'y') {
|
|
57
|
+
console.log('Cancelled.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1. Delete all remote data
|
|
62
|
+
console.log('Deleting all remote data...');
|
|
63
|
+
try {
|
|
64
|
+
const result = await deleteAllData(apiUrl, config.apiKey);
|
|
65
|
+
console.log(`Deleted ${result.deleted} buckets from server.`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.message === 'UNAUTHORIZED') {
|
|
68
|
+
console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
console.error(`Failed to delete remote data: ${err.message}`);
|
|
46
72
|
process.exit(1);
|
|
47
73
|
}
|
|
48
|
-
console.error(`Failed to delete remote data: ${err.message}`);
|
|
49
|
-
process.exit(1);
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
// 2. Clear local state
|
package/src/sync.js
CHANGED
|
@@ -11,19 +11,25 @@ function formatBytes(bytes) {
|
|
|
11
11
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export async function runSync() {
|
|
14
|
+
export async function runSync({ throws = false, quiet = false } = {}) {
|
|
15
15
|
const config = loadConfig();
|
|
16
16
|
if (!config?.apiKey) {
|
|
17
17
|
console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
|
|
18
|
+
if (throws) throw new Error('NOT_CONFIGURED');
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// Migration: remove deprecated lastSync field from config
|
|
23
|
+
if ('lastSync' in config) {
|
|
24
|
+
delete config.lastSync;
|
|
25
|
+
saveConfig(config);
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
const allBuckets = [];
|
|
23
29
|
|
|
24
30
|
for (const [source, parse] of Object.entries(parsers)) {
|
|
25
31
|
try {
|
|
26
|
-
const buckets = await parse(
|
|
32
|
+
const buckets = await parse();
|
|
27
33
|
if (buckets.length > 0) {
|
|
28
34
|
allBuckets.push(...buckets);
|
|
29
35
|
}
|
|
@@ -33,7 +39,7 @@ export async function runSync() {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
if (allBuckets.length === 0) {
|
|
36
|
-
console.log('No new usage data found.');
|
|
42
|
+
if (!quiet) console.log('No new usage data found.');
|
|
37
43
|
return 0;
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -63,9 +69,7 @@ export async function runSync() {
|
|
|
63
69
|
});
|
|
64
70
|
totalIngested += result.ingested ?? batch.length;
|
|
65
71
|
|
|
66
|
-
//
|
|
67
|
-
config.lastSync = new Date().toISOString();
|
|
68
|
-
saveConfig(config);
|
|
72
|
+
// State commit happens after ALL batches complete (see postSyncHooks below)
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
|
|
@@ -84,14 +88,16 @@ export async function runSync() {
|
|
|
84
88
|
} catch (err) {
|
|
85
89
|
if (err.message === 'UNAUTHORIZED') {
|
|
86
90
|
console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
|
|
91
|
+
if (throws) throw err;
|
|
87
92
|
process.exit(1);
|
|
88
93
|
}
|
|
89
|
-
//
|
|
94
|
+
// Report partial success
|
|
90
95
|
if (totalIngested > 0) {
|
|
91
96
|
console.error(`Sync partially completed (${totalIngested} buckets uploaded). ${err.message}`);
|
|
92
97
|
} else {
|
|
93
98
|
console.error(`Sync failed: ${err.message}`);
|
|
94
99
|
}
|
|
100
|
+
if (throws) throw err;
|
|
95
101
|
process.exit(1);
|
|
96
102
|
}
|
|
97
103
|
}
|