@vibe-cafe/vibe-usage 0.6.6 → 0.6.8
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 +26 -1
- package/package.json +1 -1
- package/src/index.js +7 -0
- package/src/parsers/index.js +2 -0
- package/src/parsers/pi-coding-agent.js +144 -0
- package/src/skill.js +134 -0
- package/src/tools.js +5 -0
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ npx @vibe-cafe/vibe-usage sync # Manual sync
|
|
|
22
22
|
npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 5 minutes)
|
|
23
23
|
npx @vibe-cafe/vibe-usage reset # Delete all data and re-upload from local logs
|
|
24
24
|
npx @vibe-cafe/vibe-usage reset --local # Delete this host's data only and re-upload
|
|
25
|
+
npx @vibe-cafe/vibe-usage skill # Install skill for AI coding assistants
|
|
26
|
+
npx @vibe-cafe/vibe-usage skill --remove # Remove installed skills
|
|
25
27
|
npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
26
28
|
```
|
|
27
29
|
|
|
@@ -35,18 +37,41 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
35
37
|
| Gemini CLI | `~/.gemini/tmp/` |
|
|
36
38
|
| OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
|
|
37
39
|
| OpenClaw | `~/.openclaw/agents/` |
|
|
40
|
+
| pi | `~/.pi/agent/sessions/` |
|
|
38
41
|
| Qwen Code | `~/.qwen/tmp/` |
|
|
39
42
|
| Kimi Code | `~/.kimi/sessions/` |
|
|
43
|
+
| Amp | `~/.local/share/amp/threads/` |
|
|
44
|
+
| Droid | `~/.factory/sessions/` |
|
|
40
45
|
|
|
41
46
|
## How It Works
|
|
42
47
|
|
|
43
48
|
- Parses local session logs from each AI coding tool
|
|
44
49
|
- Aggregates token usage into 30-minute buckets
|
|
45
|
-
- Extracts session metadata from all
|
|
50
|
+
- Extracts session metadata from all 10 parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
|
|
46
51
|
- Uploads buckets + sessions to your vibecafe.ai dashboard
|
|
47
52
|
- Stateless: computes full totals from local logs each sync (idempotent, no state files)
|
|
48
53
|
- For continuous syncing, use `npx @vibe-cafe/vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
|
|
49
54
|
|
|
55
|
+
## AI Skill
|
|
56
|
+
|
|
57
|
+
Install vibe-usage as a skill for your AI coding assistant, so it knows how to sync usage data on your behalf:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @vibe-cafe/vibe-usage skill
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This auto-detects installed AI tools (Claude Code, Cursor, Windsurf, Codex CLI) and writes a `SKILL.md` to each tool's global skills directory. To remove:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx @vibe-cafe/vibe-usage skill --remove
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can also install via the [open skills ecosystem](https://github.com/vercel-labs/skills):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx skills add vibe-cafe/vibe-usage
|
|
73
|
+
```
|
|
74
|
+
|
|
50
75
|
## Development
|
|
51
76
|
|
|
52
77
|
Test against a local vibe-cafe dev server without publishing:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -113,6 +113,11 @@ export async function run(args) {
|
|
|
113
113
|
await runDaemon();
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
116
|
+
case 'skill': {
|
|
117
|
+
const { runSkill } = await import('./skill.js');
|
|
118
|
+
await runSkill(args.slice(1));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
116
121
|
case 'config': {
|
|
117
122
|
handleConfig(args.slice(1));
|
|
118
123
|
break;
|
|
@@ -134,6 +139,8 @@ export async function run(args) {
|
|
|
134
139
|
npx @vibe-cafe/vibe-usage daemon Continuous sync (every 5m)
|
|
135
140
|
npx @vibe-cafe/vibe-usage reset Delete all data and re-upload
|
|
136
141
|
npx @vibe-cafe/vibe-usage reset --local Delete data for this host only and re-upload
|
|
142
|
+
npx @vibe-cafe/vibe-usage skill Install skill for AI coding tools
|
|
143
|
+
npx @vibe-cafe/vibe-usage skill --remove Remove installed skills
|
|
137
144
|
npx @vibe-cafe/vibe-usage status Show config and detected tools
|
|
138
145
|
npx @vibe-cafe/vibe-usage config show Show full config as JSON
|
|
139
146
|
npx @vibe-cafe/vibe-usage config get <key> Get a config value
|
package/src/parsers/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parse as parseQwenCode } from './qwen-code.js';
|
|
|
9
9
|
import { parse as parseKimiCode } from './kimi-code.js';
|
|
10
10
|
import { parse as parseAmp } from './amp.js';
|
|
11
11
|
import { parse as parseDroid } from './droid.js';
|
|
12
|
+
import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
|
|
12
13
|
|
|
13
14
|
export const parsers = {
|
|
14
15
|
'claude-code': parseClaudeCode,
|
|
@@ -21,6 +22,7 @@ export const parsers = {
|
|
|
21
22
|
'kimi-code': parseKimiCode,
|
|
22
23
|
'amp': parseAmp,
|
|
23
24
|
'droid': parseDroid,
|
|
25
|
+
'pi-coding-agent': parsePiCodingAgent,
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* pi-coding-agent parser.
|
|
8
|
+
* Reads JSONL session files from ~/.pi/agent/sessions/ (or $PI_CODING_AGENT_DIR/sessions/).
|
|
9
|
+
*
|
|
10
|
+
* Session file layout:
|
|
11
|
+
* sessions/<encoded-cwd>/{timestamp}_{sessionId}.jsonl
|
|
12
|
+
*
|
|
13
|
+
* Each JSONL line is a session entry:
|
|
14
|
+
* - type "session": header with id, cwd, version
|
|
15
|
+
* - type "message": contains message object with role, usage, model, timestamp
|
|
16
|
+
* - type "model_change", "compaction", etc.: metadata (ignored for usage)
|
|
17
|
+
*
|
|
18
|
+
* Assistant messages carry per-message token usage:
|
|
19
|
+
* message.usage = { input, output, cacheRead, cacheWrite, totalTokens }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function getSessionsDir() {
|
|
23
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
24
|
+
if (envDir) return join(envDir, 'sessions');
|
|
25
|
+
return join(homedir(), '.pi', 'agent', 'sessions');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function findJsonlFiles(dir) {
|
|
29
|
+
const results = [];
|
|
30
|
+
if (!existsSync(dir)) return results;
|
|
31
|
+
try {
|
|
32
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const fullPath = join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
results.push(...findJsonlFiles(fullPath));
|
|
36
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
37
|
+
results.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore unreadable directories
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractProjectFromCwd(cwd) {
|
|
47
|
+
if (!cwd) return 'unknown';
|
|
48
|
+
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
49
|
+
return parts.length > 0 ? parts[parts.length - 1] : 'unknown';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractProjectFromDir(filePath, sessionsDir) {
|
|
53
|
+
const relative = filePath.slice(sessionsDir.length + 1);
|
|
54
|
+
const firstSeg = relative.split('/')[0] || relative.split('\\')[0];
|
|
55
|
+
if (!firstSeg) return 'unknown';
|
|
56
|
+
const parts = firstSeg.split('-').filter(Boolean);
|
|
57
|
+
return parts.length > 0 ? parts[parts.length - 1] : 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function parse() {
|
|
61
|
+
const sessionsDir = getSessionsDir();
|
|
62
|
+
const entries = [];
|
|
63
|
+
const sessionEvents = [];
|
|
64
|
+
const seenEntryIds = new Set();
|
|
65
|
+
|
|
66
|
+
const sessionFiles = findJsonlFiles(sessionsDir);
|
|
67
|
+
|
|
68
|
+
for (const filePath of sessionFiles) {
|
|
69
|
+
let content;
|
|
70
|
+
try {
|
|
71
|
+
content = readFileSync(filePath, 'utf-8');
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let sessionId = basename(filePath, '.jsonl');
|
|
77
|
+
let project = extractProjectFromDir(filePath, sessionsDir);
|
|
78
|
+
|
|
79
|
+
for (const line of content.split('\n')) {
|
|
80
|
+
if (!line.trim()) continue;
|
|
81
|
+
|
|
82
|
+
let obj;
|
|
83
|
+
try {
|
|
84
|
+
obj = JSON.parse(line);
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (obj.type === 'session') {
|
|
90
|
+
if (obj.id) sessionId = obj.id;
|
|
91
|
+
if (obj.cwd) project = extractProjectFromCwd(obj.cwd);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (obj.type !== 'message') continue;
|
|
96
|
+
|
|
97
|
+
const msg = obj.message;
|
|
98
|
+
if (!msg) continue;
|
|
99
|
+
|
|
100
|
+
let ts;
|
|
101
|
+
if (obj.timestamp) {
|
|
102
|
+
ts = new Date(obj.timestamp);
|
|
103
|
+
} else if (msg.timestamp) {
|
|
104
|
+
ts = new Date(msg.timestamp);
|
|
105
|
+
}
|
|
106
|
+
if (!ts || isNaN(ts.getTime())) continue;
|
|
107
|
+
|
|
108
|
+
if (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'toolResult') {
|
|
109
|
+
sessionEvents.push({
|
|
110
|
+
sessionId,
|
|
111
|
+
source: 'pi-coding-agent',
|
|
112
|
+
project,
|
|
113
|
+
timestamp: ts,
|
|
114
|
+
role: msg.role === 'user' ? 'user' : 'assistant',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (msg.role !== 'assistant') continue;
|
|
119
|
+
if (!msg.usage) continue;
|
|
120
|
+
|
|
121
|
+
const usage = msg.usage;
|
|
122
|
+
if (usage.input == null && usage.output == null) continue;
|
|
123
|
+
|
|
124
|
+
const entryId = obj.id;
|
|
125
|
+
if (entryId) {
|
|
126
|
+
if (seenEntryIds.has(entryId)) continue;
|
|
127
|
+
seenEntryIds.add(entryId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
entries.push({
|
|
131
|
+
source: 'pi-coding-agent',
|
|
132
|
+
model: msg.model || 'unknown',
|
|
133
|
+
project,
|
|
134
|
+
timestamp: ts,
|
|
135
|
+
inputTokens: usage.input || 0,
|
|
136
|
+
outputTokens: usage.output || 0,
|
|
137
|
+
cachedInputTokens: usage.cacheRead || 0,
|
|
138
|
+
reasoningOutputTokens: 0,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
144
|
+
}
|
package/src/skill.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const SKILL_TARGETS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'Claude Code',
|
|
8
|
+
detectDir: join(homedir(), '.claude'),
|
|
9
|
+
skillDir: join(homedir(), '.claude', 'skills', 'vibe-usage'),
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Codex CLI',
|
|
13
|
+
detectDir: join(homedir(), '.codex'),
|
|
14
|
+
skillDir: join(homedir(), '.codex', 'skills', 'vibe-usage'),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Cursor',
|
|
18
|
+
detectDir: join(homedir(), '.cursor'),
|
|
19
|
+
skillDir: join(homedir(), '.cursor', 'skills', 'vibe-usage'),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'Windsurf',
|
|
23
|
+
detectDir: join(homedir(), '.codeium', 'windsurf'),
|
|
24
|
+
skillDir: join(homedir(), '.codeium', 'windsurf', 'skills', 'vibe-usage'),
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function tildePath(absPath) {
|
|
29
|
+
const home = homedir();
|
|
30
|
+
return absPath.startsWith(home) ? absPath.replace(home, '~') : absPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SKILL_CONTENT = `---
|
|
34
|
+
name: vibe-usage
|
|
35
|
+
description: Track and sync AI coding tool token usage to vibecafe.ai dashboard.
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
# Vibe Usage
|
|
39
|
+
|
|
40
|
+
Track your AI coding tool token usage and sync to [vibecafe.ai](https://vibecafe.ai/usage).
|
|
41
|
+
|
|
42
|
+
## Setup
|
|
43
|
+
|
|
44
|
+
First-time setup (interactive — asks for API key):
|
|
45
|
+
|
|
46
|
+
\`\`\`bash
|
|
47
|
+
npx @vibe-cafe/vibe-usage
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
Get your API key at https://vibecafe.ai/usage/setup
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
When the user asks to sync usage, check costs, or track tokens, run:
|
|
55
|
+
|
|
56
|
+
\`\`\`bash
|
|
57
|
+
npx @vibe-cafe/vibe-usage sync
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
Other available commands:
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| \`npx @vibe-cafe/vibe-usage sync\` | Sync latest usage data |
|
|
65
|
+
| \`npx @vibe-cafe/vibe-usage status\` | Show config and detected tools |
|
|
66
|
+
| \`npx @vibe-cafe/vibe-usage daemon\` | Continuous sync every 5 minutes |
|
|
67
|
+
| \`npx @vibe-cafe/vibe-usage reset\` | Delete all data and re-upload |
|
|
68
|
+
| \`npx @vibe-cafe/vibe-usage reset --local\` | Delete this host's data and re-upload |
|
|
69
|
+
|
|
70
|
+
## When to Use
|
|
71
|
+
|
|
72
|
+
- User says "sync my usage", "upload usage", "track tokens"
|
|
73
|
+
- User asks "how much have I spent?", "what's my cost?"
|
|
74
|
+
- User wants to check if sync is working: run \`status\`
|
|
75
|
+
- User wants continuous background sync: run \`daemon\`
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- Requires initial setup with an API key (run \`npx @vibe-cafe/vibe-usage\` first)
|
|
80
|
+
- Config is stored at \`~/.vibe-usage/config.json\`
|
|
81
|
+
- Supports: Claude Code, Codex CLI, Copilot CLI, Gemini CLI, OpenCode, OpenClaw, Qwen Code, Kimi Code, Amp, Droid
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export async function runSkill(args = []) {
|
|
85
|
+
const remove = args.includes('--remove');
|
|
86
|
+
|
|
87
|
+
console.log('\nvibe-usage skill\n');
|
|
88
|
+
|
|
89
|
+
console.log(' AI coding tools:');
|
|
90
|
+
for (const t of SKILL_TARGETS) {
|
|
91
|
+
const found = existsSync(t.detectDir);
|
|
92
|
+
console.log(` ${found ? '\u2713' : '\u2717'} ${t.name}`);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
const detected = SKILL_TARGETS.filter(t => existsSync(t.detectDir));
|
|
97
|
+
|
|
98
|
+
if (detected.length === 0) {
|
|
99
|
+
console.log(' No supported tools detected. Nothing to do.\n');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (remove) {
|
|
104
|
+
let removed = 0;
|
|
105
|
+
for (const t of detected) {
|
|
106
|
+
const skillFile = join(t.skillDir, 'SKILL.md');
|
|
107
|
+
if (existsSync(skillFile)) {
|
|
108
|
+
unlinkSync(skillFile);
|
|
109
|
+
try { rmdirSync(t.skillDir); } catch {}
|
|
110
|
+
console.log(` Removed: ${tildePath(skillFile)}`);
|
|
111
|
+
removed++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (removed === 0) {
|
|
115
|
+
console.log(' No skills installed to remove.\n');
|
|
116
|
+
} else {
|
|
117
|
+
console.log(`\n Removed vibe-usage skill from ${removed} tool${removed > 1 ? 's' : ''}.\n`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let installed = 0;
|
|
123
|
+
for (const t of detected) {
|
|
124
|
+
const skillFile = join(t.skillDir, 'SKILL.md');
|
|
125
|
+
mkdirSync(t.skillDir, { recursive: true });
|
|
126
|
+
writeFileSync(skillFile, SKILL_CONTENT, 'utf-8');
|
|
127
|
+
console.log(` Installed: ${tildePath(skillFile)}`);
|
|
128
|
+
installed++;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`\n Installed vibe-usage skill for ${installed} tool${installed > 1 ? 's' : ''}.\n`);
|
|
132
|
+
console.log(' Your AI coding assistant now knows how to sync usage data.');
|
|
133
|
+
console.log(' Try asking: "sync my vibe usage" or "how much have I spent?"\n');
|
|
134
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -33,6 +33,11 @@ export const TOOLS = [
|
|
|
33
33
|
id: 'openclaw',
|
|
34
34
|
dataDir: join(homedir(), '.openclaw', 'agents'),
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: 'pi',
|
|
38
|
+
id: 'pi-coding-agent',
|
|
39
|
+
dataDir: join(homedir(), '.pi', 'agent', 'sessions'),
|
|
40
|
+
},
|
|
36
41
|
{
|
|
37
42
|
name: 'Qwen Code',
|
|
38
43
|
id: 'qwen-code',
|