@vibe-cafe/vibe-usage 0.1.2 → 0.1.4
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 +5 -0
- package/src/init.js +3 -2
- package/src/parsers/claude-code.js +1 -1
- package/src/parsers/codex.js +22 -2
- package/src/parsers/openclaw.js +74 -50
- package/src/sync.js +10 -3
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -18,6 +18,7 @@ export function ingest(apiUrl, apiKey, buckets) {
|
|
|
18
18
|
|
|
19
19
|
const req = mod.request(url, {
|
|
20
20
|
method: 'POST',
|
|
21
|
+
timeout: 30_000,
|
|
21
22
|
headers: {
|
|
22
23
|
'Content-Type': 'application/json',
|
|
23
24
|
'Authorization': `Bearer ${apiKey}`,
|
|
@@ -44,6 +45,10 @@ export function ingest(apiUrl, apiKey, buckets) {
|
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
req.on('error', (err) => reject(err));
|
|
48
|
+
req.on('timeout', () => {
|
|
49
|
+
req.destroy();
|
|
50
|
+
reject(new Error('Request timed out (30s)'));
|
|
51
|
+
});
|
|
47
52
|
req.write(body);
|
|
48
53
|
req.end();
|
|
49
54
|
});
|
package/src/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { loadConfig, saveConfig } from './config.js';
|
|
@@ -20,7 +20,8 @@ function prompt(question) {
|
|
|
20
20
|
function openBrowser(url) {
|
|
21
21
|
const cmds = { darwin: 'open', linux: 'xdg-open', win32: 'start' };
|
|
22
22
|
const cmd = cmds[platform()] || cmds.linux;
|
|
23
|
-
|
|
23
|
+
// Use execFile with args array to avoid shell injection via VIBE_USAGE_API_URL
|
|
24
|
+
execFile(cmd, [url], () => {});
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export async function runInit() {
|
|
@@ -14,7 +14,7 @@ export async function parse(lastSync) {
|
|
|
14
14
|
const entries = [];
|
|
15
15
|
|
|
16
16
|
for (const session of sessions) {
|
|
17
|
-
if (lastSync && session.lastActivity <= lastSync) continue;
|
|
17
|
+
if (lastSync && new Date(session.lastActivity) <= new Date(lastSync)) continue;
|
|
18
18
|
|
|
19
19
|
for (const breakdown of session.modelBreakdowns || []) {
|
|
20
20
|
entries.push({
|
package/src/parsers/codex.js
CHANGED
|
@@ -36,6 +36,9 @@ export async function parse(lastSync) {
|
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Track previous cumulative totals per model to compute deltas when only total_token_usage is available
|
|
40
|
+
const prevTotal = new Map();
|
|
41
|
+
|
|
39
42
|
for (const line of content.split('\n')) {
|
|
40
43
|
if (!line.trim()) continue;
|
|
41
44
|
try {
|
|
@@ -54,8 +57,25 @@ export async function parse(lastSync) {
|
|
|
54
57
|
if (!timestamp || isNaN(timestamp.getTime())) continue;
|
|
55
58
|
if (lastSync && timestamp <= new Date(lastSync)) continue;
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
// Prefer incremental per-request usage; compute delta from cumulative total as fallback
|
|
61
|
+
let usage = info.last_token_usage;
|
|
62
|
+
if (!usage && info.total_token_usage) {
|
|
63
|
+
const totalKey = `${info.model || payload.model || ''}`;
|
|
64
|
+
const prev = prevTotal.get(totalKey);
|
|
65
|
+
const curr = info.total_token_usage;
|
|
66
|
+
if (prev) {
|
|
67
|
+
usage = {
|
|
68
|
+
input_tokens: (curr.input_tokens || 0) - (prev.input_tokens || 0),
|
|
69
|
+
output_tokens: (curr.output_tokens || 0) - (prev.output_tokens || 0),
|
|
70
|
+
cached_input_tokens: (curr.cached_input_tokens || 0) - (prev.cached_input_tokens || 0),
|
|
71
|
+
reasoning_output_tokens: (curr.reasoning_output_tokens || 0) - (prev.reasoning_output_tokens || 0),
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
// First cumulative entry — use as-is (it's the first event's total)
|
|
75
|
+
usage = curr;
|
|
76
|
+
}
|
|
77
|
+
prevTotal.set(totalKey, { ...curr });
|
|
78
|
+
}
|
|
59
79
|
if (!usage) continue;
|
|
60
80
|
|
|
61
81
|
const model = info.model || payload.model || 'unknown';
|
package/src/parsers/openclaw.js
CHANGED
|
@@ -3,76 +3,100 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { aggregateToBuckets } from './index.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// OpenClaw stores data at ~/.openclaw/agents/<agentId>/sessions/*.jsonl
|
|
7
|
+
// Legacy paths: ~/.clawdbot, ~/.moltbot, ~/.moldbot
|
|
8
|
+
const POSSIBLE_ROOTS = [
|
|
9
|
+
join(homedir(), '.openclaw'),
|
|
10
|
+
join(homedir(), '.clawdbot'),
|
|
11
|
+
join(homedir(), '.moltbot'),
|
|
12
|
+
join(homedir(), '.moldbot'),
|
|
13
|
+
];
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
/** Normalize usage fields — OpenClaw supports multiple naming conventions */
|
|
16
|
+
function getTokens(usage, ...keys) {
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
if (usage[key] != null && usage[key] > 0) return usage[key];
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
10
22
|
|
|
23
|
+
export async function parse(lastSync) {
|
|
11
24
|
const entries = [];
|
|
12
|
-
let agentDirs;
|
|
13
|
-
try {
|
|
14
|
-
agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true })
|
|
15
|
-
.filter(d => d.isDirectory());
|
|
16
|
-
} catch {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
25
|
|
|
20
|
-
for (const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (!existsSync(sessionsDir)) continue;
|
|
26
|
+
for (const root of POSSIBLE_ROOTS) {
|
|
27
|
+
const agentsDir = join(root, 'agents');
|
|
28
|
+
if (!existsSync(agentsDir)) continue;
|
|
24
29
|
|
|
25
|
-
let
|
|
30
|
+
let agentDirs;
|
|
26
31
|
try {
|
|
27
|
-
|
|
32
|
+
agentDirs = readdirSync(agentsDir, { withFileTypes: true })
|
|
33
|
+
.filter(d => d.isDirectory());
|
|
28
34
|
} catch {
|
|
29
35
|
continue;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
for (const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const stat = statSync(filePath);
|
|
37
|
-
if (stat.mtime <= new Date(lastSync)) continue;
|
|
38
|
-
} catch {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
38
|
+
for (const agentDir of agentDirs) {
|
|
39
|
+
const project = agentDir.name;
|
|
40
|
+
const sessionsDir = join(agentsDir, agentDir.name, 'sessions');
|
|
41
|
+
if (!existsSync(sessionsDir)) continue;
|
|
42
42
|
|
|
43
|
-
let
|
|
43
|
+
let files;
|
|
44
44
|
try {
|
|
45
|
-
|
|
45
|
+
files = readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
|
|
46
46
|
} catch {
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
for (const
|
|
51
|
-
|
|
50
|
+
for (const file of files) {
|
|
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
|
+
|
|
61
|
+
let content;
|
|
52
62
|
try {
|
|
53
|
-
|
|
63
|
+
content = readFileSync(filePath, 'utf-8');
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
if (!
|
|
68
|
+
for (const line of content.split('\n')) {
|
|
69
|
+
if (!line.trim()) continue;
|
|
70
|
+
try {
|
|
71
|
+
const obj = JSON.parse(line);
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (lastSync && ts <= new Date(lastSync)) continue;
|
|
73
|
+
// Only process message entries with assistant role
|
|
74
|
+
if (obj.type !== 'message') continue;
|
|
75
|
+
const msg = obj.message;
|
|
76
|
+
if (!msg || msg.role !== 'assistant') continue;
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
timestamp
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
const usage = msg.usage;
|
|
79
|
+
if (!usage) continue;
|
|
80
|
+
|
|
81
|
+
const timestamp = obj.timestamp || msg.timestamp;
|
|
82
|
+
if (!timestamp) continue;
|
|
83
|
+
const ts = new Date(typeof timestamp === 'number' ? timestamp : timestamp);
|
|
84
|
+
if (isNaN(ts.getTime())) continue;
|
|
85
|
+
if (lastSync && ts <= new Date(lastSync)) continue;
|
|
86
|
+
|
|
87
|
+
entries.push({
|
|
88
|
+
source: 'openclaw',
|
|
89
|
+
model: msg.model || obj.model || 'unknown',
|
|
90
|
+
project,
|
|
91
|
+
timestamp: ts,
|
|
92
|
+
inputTokens: getTokens(usage, 'input', 'inputTokens', 'input_tokens', 'promptTokens', 'prompt_tokens'),
|
|
93
|
+
outputTokens: getTokens(usage, 'output', 'outputTokens', 'output_tokens', 'completionTokens', 'completion_tokens'),
|
|
94
|
+
cachedInputTokens: getTokens(usage, 'cacheRead', 'cache_read', 'cache_read_input_tokens'),
|
|
95
|
+
reasoningOutputTokens: 0,
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
76
100
|
}
|
|
77
101
|
}
|
|
78
102
|
}
|
package/src/sync.js
CHANGED
|
@@ -39,13 +39,15 @@ export async function runSync() {
|
|
|
39
39
|
const result = await ingest(apiUrl, config.apiKey, batch);
|
|
40
40
|
totalIngested += result.ingested ?? batch.length;
|
|
41
41
|
|
|
42
|
+
// Save progress after each successful batch so partial uploads survive interruptions
|
|
43
|
+
config.lastSync = new Date().toISOString();
|
|
44
|
+
saveConfig(config);
|
|
45
|
+
|
|
42
46
|
if (allBuckets.length > BATCH_SIZE) {
|
|
43
47
|
process.stdout.write(` ${Math.min(i + BATCH_SIZE, allBuckets.length)}/${allBuckets.length} buckets...\r`);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
config.lastSync = new Date().toISOString();
|
|
48
|
-
saveConfig(config);
|
|
49
51
|
console.log(`Synced ${totalIngested} buckets.`);
|
|
50
52
|
return totalIngested;
|
|
51
53
|
} catch (err) {
|
|
@@ -53,7 +55,12 @@ export async function runSync() {
|
|
|
53
55
|
console.error('Invalid API key. Run `npx @vibe-cafe/vibe-usage init` to reconfigure.');
|
|
54
56
|
process.exit(1);
|
|
55
57
|
}
|
|
56
|
-
|
|
58
|
+
// Progress already saved per-batch — report partial success
|
|
59
|
+
if (totalIngested > 0) {
|
|
60
|
+
console.error(`Sync partially completed (${totalIngested} buckets uploaded). ${err.message}`);
|
|
61
|
+
} else {
|
|
62
|
+
console.error(`Sync failed: ${err.message}`);
|
|
63
|
+
}
|
|
57
64
|
process.exit(1);
|
|
58
65
|
}
|
|
59
66
|
}
|