@vibe-cafe/vibe-usage 0.1.1 → 0.1.3

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.1",
3
+ "version": "0.1.3",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
package/src/hooks.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
- const SYNC_CMD = 'npx vibe-usage sync 2>/dev/null &';
5
+ const SYNC_CMD = 'npx @vibe-cafe/vibe-usage sync 2>/dev/null &';
6
6
 
7
7
  function hasVibeUsageHook(hooks) {
8
8
  if (!Array.isArray(hooks)) return false;
package/src/init.js CHANGED
@@ -86,7 +86,7 @@ export async function runInit() {
86
86
  console.log(`Hooks installed for: ${hooked.join(', ')}`);
87
87
  }
88
88
  for (const name of manualOnly) {
89
- console.log(`${name} detected — use \`npx vibe-usage sync\` to sync manually.`);
89
+ console.log(`${name} detected — use \`npx @vibe-cafe/vibe-usage sync\` to sync manually.`);
90
90
  }
91
91
  if (tools.length === 0) {
92
92
  console.log('No AI coding tools detected. Install one and re-run init.');
@@ -95,5 +95,5 @@ export async function runInit() {
95
95
  console.log('\nRunning initial sync...');
96
96
  await runSync();
97
97
 
98
- console.log('\nSetup complete! Usage data will sync automatically after each session.');
98
+ console.log(`\nSetup complete! View your dashboard at: ${apiUrl}/usage`);
99
99
  }
@@ -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
  }