@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
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 { exec } from 'node:child_process';
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
- exec(`${cmd} ${url}`, () => {});
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({
@@ -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
- const usage = info.last_token_usage || info.total_token_usage;
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';
@@ -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
- const AGENTS_DIR = join(homedir(), '.openclaw', 'agents');
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
- export async function parse(lastSync) {
9
- if (!existsSync(AGENTS_DIR)) return [];
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 agentDir of agentDirs) {
21
- const project = agentDir.name;
22
- const sessionsDir = join(AGENTS_DIR, agentDir.name, 'sessions');
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 files;
30
+ let agentDirs;
26
31
  try {
27
- files = readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
32
+ agentDirs = readdirSync(agentsDir, { withFileTypes: true })
33
+ .filter(d => d.isDirectory());
28
34
  } catch {
29
35
  continue;
30
36
  }
31
37
 
32
- for (const file of files) {
33
- const filePath = join(sessionsDir, file);
34
- if (lastSync) {
35
- try {
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 content;
43
+ let files;
44
44
  try {
45
- content = readFileSync(filePath, 'utf-8');
45
+ files = readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
46
46
  } catch {
47
47
  continue;
48
48
  }
49
49
 
50
- for (const line of content.split('\n')) {
51
- if (!line.trim()) continue;
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
- const obj = JSON.parse(line);
63
+ content = readFileSync(filePath, 'utf-8');
64
+ } catch {
65
+ continue;
66
+ }
54
67
 
55
- const usage = obj.usage || obj.message?.usage;
56
- if (!usage) continue;
68
+ for (const line of content.split('\n')) {
69
+ if (!line.trim()) continue;
70
+ try {
71
+ const obj = JSON.parse(line);
57
72
 
58
- const timestamp = obj.timestamp || obj.created_at;
59
- if (!timestamp) continue;
60
- const ts = new Date(timestamp);
61
- if (isNaN(ts.getTime())) continue;
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
- entries.push({
65
- source: 'openclaw',
66
- model: obj.model || obj.message?.model || 'unknown',
67
- project,
68
- timestamp: ts,
69
- inputTokens: usage.input_tokens || 0,
70
- outputTokens: usage.output_tokens || 0,
71
- cachedInputTokens: usage.cache_read_input_tokens || 0,
72
- reasoningOutputTokens: 0,
73
- });
74
- } catch {
75
- continue;
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
- console.error(`Sync failed: ${err.message}`);
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
  }