@vibe-cafe/vibe-usage 0.2.7 → 0.3.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/package.json +1 -1
- package/src/api.js +6 -4
- package/src/index.js +3 -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 +9 -6
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/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,7 @@ 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));
|
|
109
108
|
break;
|
|
110
109
|
}
|
|
111
110
|
case 'config': {
|
|
@@ -127,6 +126,7 @@ export async function run(args) {
|
|
|
127
126
|
npx vibe-usage init Set up API key
|
|
128
127
|
npx vibe-usage sync Manually sync usage data
|
|
129
128
|
npx vibe-usage reset Delete all data and re-upload
|
|
129
|
+
npx vibe-usage reset --host Delete data for this host only and re-upload
|
|
130
130
|
npx vibe-usage status Show config and detected tools
|
|
131
131
|
npx vibe-usage config show Show full config as JSON
|
|
132
132
|
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('--host');
|
|
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
|
@@ -18,12 +18,17 @@ export async function runSync() {
|
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Migration: remove deprecated lastSync field from config
|
|
22
|
+
if ('lastSync' in config) {
|
|
23
|
+
delete config.lastSync;
|
|
24
|
+
saveConfig(config);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
const allBuckets = [];
|
|
23
28
|
|
|
24
29
|
for (const [source, parse] of Object.entries(parsers)) {
|
|
25
30
|
try {
|
|
26
|
-
const buckets = await parse(
|
|
31
|
+
const buckets = await parse();
|
|
27
32
|
if (buckets.length > 0) {
|
|
28
33
|
allBuckets.push(...buckets);
|
|
29
34
|
}
|
|
@@ -63,9 +68,7 @@ export async function runSync() {
|
|
|
63
68
|
});
|
|
64
69
|
totalIngested += result.ingested ?? batch.length;
|
|
65
70
|
|
|
66
|
-
//
|
|
67
|
-
config.lastSync = new Date().toISOString();
|
|
68
|
-
saveConfig(config);
|
|
71
|
+
// State commit happens after ALL batches complete (see postSyncHooks below)
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
|
|
@@ -86,7 +89,7 @@ export async function runSync() {
|
|
|
86
89
|
console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
|
|
87
90
|
process.exit(1);
|
|
88
91
|
}
|
|
89
|
-
//
|
|
92
|
+
// Report partial success
|
|
90
93
|
if (totalIngested > 0) {
|
|
91
94
|
console.error(`Sync partially completed (${totalIngested} buckets uploaded). ${err.message}`);
|
|
92
95
|
} else {
|