@vibe-cafe/vibe-usage 0.3.0 → 0.4.1
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 +9 -1
- package/src/api.js +39 -0
- package/src/daemon.js +37 -0
- package/src/index.js +8 -1
- package/src/reset.js +1 -1
- package/src/sync.js +19 -3
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-cafe/vibe-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Track your AI coding tool token usage and sync to vibecafe.ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,5 +25,13 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"ccusage": "18.0.5"
|
|
27
27
|
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/vibe-cafe/vibe-usage.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/vibe-cafe/vibe-usage#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/vibe-cafe/vibe-usage/issues"
|
|
35
|
+
},
|
|
28
36
|
"license": "MIT"
|
|
29
37
|
}
|
package/src/api.js
CHANGED
|
@@ -151,3 +151,42 @@ export function deleteAllData(apiUrl, apiKey, opts) {
|
|
|
151
151
|
req.end();
|
|
152
152
|
});
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* GET user settings from the vibecafe API.
|
|
157
|
+
* Returns null on any failure (network, auth, timeout) — caller should fail-safe.
|
|
158
|
+
* @param {string} apiUrl
|
|
159
|
+
* @param {string} apiKey
|
|
160
|
+
* @returns {Promise<{uploadProject: boolean} | null>}
|
|
161
|
+
*/
|
|
162
|
+
export function fetchSettings(apiUrl, apiKey) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const url = new URL('/api/usage/settings', apiUrl);
|
|
165
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
166
|
+
|
|
167
|
+
const req = mod.request(url, {
|
|
168
|
+
method: 'GET',
|
|
169
|
+
timeout: 10_000,
|
|
170
|
+
headers: {
|
|
171
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
172
|
+
},
|
|
173
|
+
}, (res) => {
|
|
174
|
+
let data = '';
|
|
175
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
176
|
+
res.on('end', () => {
|
|
177
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
178
|
+
resolve(null);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
resolve(JSON.parse(data));
|
|
183
|
+
} catch {
|
|
184
|
+
resolve(null);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
req.on('error', () => resolve(null));
|
|
190
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
191
|
+
});
|
|
192
|
+
}
|
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
|
@@ -107,6 +107,12 @@ export async function run(args) {
|
|
|
107
107
|
await runReset(args.slice(1));
|
|
108
108
|
break;
|
|
109
109
|
}
|
|
110
|
+
case 'daemon':
|
|
111
|
+
case '--daemon': {
|
|
112
|
+
const { runDaemon } = await import('./daemon.js');
|
|
113
|
+
await runDaemon();
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
110
116
|
case 'config': {
|
|
111
117
|
handleConfig(args.slice(1));
|
|
112
118
|
break;
|
|
@@ -125,8 +131,9 @@ export async function run(args) {
|
|
|
125
131
|
npx vibe-usage Init (first run) or sync
|
|
126
132
|
npx vibe-usage init Set up API key
|
|
127
133
|
npx vibe-usage sync Manually sync usage data
|
|
134
|
+
npx vibe-usage daemon Continuous sync (every 5m)
|
|
128
135
|
npx vibe-usage reset Delete all data and re-upload
|
|
129
|
-
npx vibe-usage reset --
|
|
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/reset.js
CHANGED
|
@@ -21,7 +21,7 @@ function prompt(question) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export async function runReset(args = []) {
|
|
24
|
-
const hostOnly = args.includes('--
|
|
24
|
+
const hostOnly = args.includes('--local');
|
|
25
25
|
const config = loadConfig();
|
|
26
26
|
if (!config?.apiKey) {
|
|
27
27
|
console.error('Not configured. Run `npx @vibe-cafe/vibe-usage init` first.');
|
package/src/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hostname as osHostname } from 'node:os';
|
|
2
2
|
import { loadConfig, saveConfig } from './config.js';
|
|
3
|
-
import { ingest } from './api.js';
|
|
3
|
+
import { ingest, fetchSettings } from './api.js';
|
|
4
4
|
import { parsers, postSyncHooks } from './parsers/index.js';
|
|
5
5
|
|
|
6
6
|
const BATCH_SIZE = 100;
|
|
@@ -11,10 +11,11 @@ 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
|
|
|
@@ -38,7 +39,7 @@ export async function runSync() {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if (allBuckets.length === 0) {
|
|
41
|
-
console.log('No new usage data found.');
|
|
42
|
+
if (!quiet) console.log('No new usage data found.');
|
|
42
43
|
return 0;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -48,7 +49,20 @@ export async function runSync() {
|
|
|
48
49
|
b.hostname = host;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Privacy: check if user allows project name upload
|
|
51
53
|
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
54
|
+
const settings = await fetchSettings(apiUrl, config.apiKey);
|
|
55
|
+
const uploadProject = settings?.uploadProject === true;
|
|
56
|
+
|
|
57
|
+
if (uploadProject) {
|
|
58
|
+
console.log('📂 项目名: 上传 (可在 vibecafe.ai/usage 设置中关闭)');
|
|
59
|
+
} else {
|
|
60
|
+
console.log('🔒 项目名: 已隐藏');
|
|
61
|
+
for (const b of allBuckets) {
|
|
62
|
+
b.project = 'unknown';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
let totalIngested = 0;
|
|
53
67
|
const totalBatches = Math.ceil(allBuckets.length / BATCH_SIZE);
|
|
54
68
|
|
|
@@ -87,6 +101,7 @@ export async function runSync() {
|
|
|
87
101
|
} catch (err) {
|
|
88
102
|
if (err.message === 'UNAUTHORIZED') {
|
|
89
103
|
console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
|
|
104
|
+
if (throws) throw err;
|
|
90
105
|
process.exit(1);
|
|
91
106
|
}
|
|
92
107
|
// Report partial success
|
|
@@ -95,6 +110,7 @@ export async function runSync() {
|
|
|
95
110
|
} else {
|
|
96
111
|
console.error(`Sync failed: ${err.message}`);
|
|
97
112
|
}
|
|
113
|
+
if (throws) throw err;
|
|
98
114
|
process.exit(1);
|
|
99
115
|
}
|
|
100
116
|
}
|