@vibe-cafe/vibe-usage 0.6.6 → 0.7.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/README.md CHANGED
@@ -19,9 +19,16 @@ This will:
19
19
  npx @vibe-cafe/vibe-usage # Init (first run) or sync (subsequent runs)
20
20
  npx @vibe-cafe/vibe-usage init # Re-run setup
21
21
  npx @vibe-cafe/vibe-usage sync # Manual sync
22
- npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 5 minutes)
22
+ npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 5m, foreground)
23
+ npx @vibe-cafe/vibe-usage daemon install # Install background service (systemd/launchd)
24
+ npx @vibe-cafe/vibe-usage daemon uninstall # Remove background service
25
+ npx @vibe-cafe/vibe-usage daemon status # Show background service status
26
+ npx @vibe-cafe/vibe-usage daemon stop # Stop background service
27
+ npx @vibe-cafe/vibe-usage daemon restart # Restart background service
23
28
  npx @vibe-cafe/vibe-usage reset # Delete all data and re-upload from local logs
24
29
  npx @vibe-cafe/vibe-usage reset --local # Delete this host's data only and re-upload
30
+ npx @vibe-cafe/vibe-usage skill # Install skill for AI coding assistants
31
+ npx @vibe-cafe/vibe-usage skill --remove # Remove installed skills
25
32
  npx @vibe-cafe/vibe-usage status # Show config & detected tools
26
33
  ```
27
34
 
@@ -35,18 +42,41 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
35
42
  | Gemini CLI | `~/.gemini/tmp/` |
36
43
  | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
37
44
  | OpenClaw | `~/.openclaw/agents/` |
45
+ | pi | `~/.pi/agent/sessions/` |
38
46
  | Qwen Code | `~/.qwen/tmp/` |
39
47
  | Kimi Code | `~/.kimi/sessions/` |
48
+ | Amp | `~/.local/share/amp/threads/` |
49
+ | Droid | `~/.factory/sessions/` |
40
50
 
41
51
  ## How It Works
42
52
 
43
53
  - Parses local session logs from each AI coding tool
44
54
  - Aggregates token usage into 30-minute buckets
45
- - Extracts session metadata from all 8 parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
55
+ - Extracts session metadata from all 10 parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
46
56
  - Uploads buckets + sessions to your vibecafe.ai dashboard
47
57
  - Stateless: computes full totals from local logs each sync (idempotent, no state files)
48
58
  - 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
59
 
60
+ ## AI Skill
61
+
62
+ Install vibe-usage as a skill for your AI coding assistant, so it knows how to sync usage data on your behalf:
63
+
64
+ ```bash
65
+ npx @vibe-cafe/vibe-usage skill
66
+ ```
67
+
68
+ 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:
69
+
70
+ ```bash
71
+ npx @vibe-cafe/vibe-usage skill --remove
72
+ ```
73
+
74
+ You can also install via the [open skills ecosystem](https://github.com/vercel-labs/skills):
75
+
76
+ ```bash
77
+ npx skills add vibe-cafe/vibe-usage
78
+ ```
79
+
50
80
  ## Development
51
81
 
52
82
  Test against a local vibe-cafe dev server without publishing:
@@ -64,13 +94,34 @@ Config stored at `~/.vibe-usage/config.json` (dev: `config.dev.json`). Contains
64
94
 
65
95
  ## Daemon Mode
66
96
 
97
+ ### Background service (recommended)
98
+
99
+ Install as a system service for automatic background syncing:
100
+
101
+ ```bash
102
+ npx @vibe-cafe/vibe-usage daemon install
103
+ ```
104
+
105
+ This creates a user-level service (systemd on Linux, launchd on macOS) that syncs every 5 minutes and starts automatically on login. Manage with:
106
+
107
+ ```bash
108
+ npx @vibe-cafe/vibe-usage daemon status
109
+ npx @vibe-cafe/vibe-usage daemon stop
110
+ npx @vibe-cafe/vibe-usage daemon restart
111
+ npx @vibe-cafe/vibe-usage daemon uninstall
112
+ ```
113
+
114
+ For reliable operation, install globally first: `npm install -g @vibe-cafe/vibe-usage`
115
+
116
+ ### Foreground mode
117
+
67
118
  Run continuous syncing in the foreground (every 5 minutes):
68
119
 
69
120
  ```bash
70
121
  npx @vibe-cafe/vibe-usage daemon
71
122
  ```
72
123
 
73
- Press Ctrl+C to stop. For background use: `nohup npx @vibe-cafe/vibe-usage daemon &`
124
+ Press Ctrl+C to stop.
74
125
 
75
126
  ## License
76
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,272 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir, platform } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const SERVICE_NAME = 'vibe-usage';
8
+ const LAUNCHD_LABEL = 'ai.vibecafe.vibe-usage';
9
+
10
+ function detectPlatform() {
11
+ const os = platform();
12
+ if (os === 'linux') {
13
+ if (existsSync('/run/systemd/system')) return 'systemd';
14
+ return null;
15
+ }
16
+ if (os === 'darwin') {
17
+ return 'launchd';
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function resolvePaths() {
23
+ const nodePath = process.execPath;
24
+ const thisFile = fileURLToPath(import.meta.url);
25
+ const binPath = join(thisFile, '..', '..', 'bin', 'vibe-usage.js');
26
+
27
+ // npx cache paths are unstable — service will break when cache is cleared
28
+ const isNpxCache = binPath.includes('.npm/_npx');
29
+
30
+ return { nodePath, binPath, isNpxCache };
31
+ }
32
+
33
+ function getServicePaths(plat) {
34
+ if (plat === 'systemd') {
35
+ const dir = join(homedir(), '.config', 'systemd', 'user');
36
+ return { dir, file: join(dir, `${SERVICE_NAME}.service`) };
37
+ }
38
+ if (plat === 'launchd') {
39
+ const dir = join(homedir(), 'Library', 'LaunchAgents');
40
+ return { dir, file: join(dir, `${LAUNCHD_LABEL}.plist`) };
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function generateSystemdUnit(nodePath, binPath) {
46
+ return `[Unit]
47
+ Description=VibeCafe Usage Tracker
48
+ After=network.target
49
+
50
+ [Service]
51
+ Type=simple
52
+ ExecStart=${nodePath} ${binPath} daemon
53
+ Restart=on-failure
54
+ RestartSec=10
55
+ Environment=NODE_ENV=production
56
+ WorkingDirectory=${homedir()}
57
+
58
+ [Install]
59
+ WantedBy=default.target
60
+ `;
61
+ }
62
+
63
+ function generateLaunchdPlist(nodePath, binPath) {
64
+ const logDir = join(homedir(), '.vibe-usage');
65
+ return `<?xml version="1.0" encoding="UTF-8"?>
66
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
67
+ <plist version="1.0">
68
+ <dict>
69
+ <key>Label</key>
70
+ <string>${LAUNCHD_LABEL}</string>
71
+ <key>ProgramArguments</key>
72
+ <array>
73
+ <string>${nodePath}</string>
74
+ <string>${binPath}</string>
75
+ <string>daemon</string>
76
+ </array>
77
+ <key>RunAtLoad</key>
78
+ <true/>
79
+ <key>KeepAlive</key>
80
+ <true/>
81
+ <key>WorkingDirectory</key>
82
+ <string>${homedir()}</string>
83
+ <key>StandardOutPath</key>
84
+ <string>${join(logDir, 'daemon.log')}</string>
85
+ <key>StandardErrorPath</key>
86
+ <string>${join(logDir, 'daemon.err')}</string>
87
+ <key>EnvironmentVariables</key>
88
+ <dict>
89
+ <key>NODE_ENV</key>
90
+ <string>production</string>
91
+ </dict>
92
+ </dict>
93
+ </plist>
94
+ `;
95
+ }
96
+
97
+ function run(cmd, args) {
98
+ try {
99
+ const output = execFileSync(cmd, args, {
100
+ encoding: 'utf-8',
101
+ stdio: ['pipe', 'pipe', 'pipe'],
102
+ });
103
+ return { ok: true, output: output.trim() };
104
+ } catch (err) {
105
+ return { ok: false, output: (err.stderr || err.stdout || err.message || '').trim() };
106
+ }
107
+ }
108
+
109
+ function install() {
110
+ const plat = detectPlatform();
111
+ if (!plat) {
112
+ console.log('Daemon install is not supported on this platform.');
113
+ console.log('Supported: Linux (systemd), macOS (launchd).');
114
+ return;
115
+ }
116
+
117
+ const { nodePath, binPath, isNpxCache } = resolvePaths();
118
+
119
+ if (isNpxCache) {
120
+ console.log('Warning: vibe-usage appears to be running from the npx cache.');
121
+ console.log('The daemon may break when the cache is cleared.');
122
+ console.log('For reliable operation, install globally first:');
123
+ console.log(' npm install -g @vibe-cafe/vibe-usage\n');
124
+ }
125
+
126
+ const paths = getServicePaths(plat);
127
+
128
+ if (existsSync(paths.file)) {
129
+ console.log('Service is already installed. Run `vibe-usage daemon restart` or `daemon uninstall` first.');
130
+ return;
131
+ }
132
+
133
+ mkdirSync(paths.dir, { recursive: true });
134
+
135
+ if (plat === 'systemd') {
136
+ writeFileSync(paths.file, generateSystemdUnit(nodePath, binPath), 'utf-8');
137
+ console.log(`Created ${paths.file}`);
138
+
139
+ run('systemctl', ['--user', 'daemon-reload']);
140
+ const result = run('systemctl', ['--user', 'enable', '--now', `${SERVICE_NAME}.service`]);
141
+ if (!result.ok) {
142
+ console.error(`Failed to start service: ${result.output}`);
143
+ return;
144
+ }
145
+ console.log('Service enabled and started.');
146
+ }
147
+
148
+ if (plat === 'launchd') {
149
+ mkdirSync(join(homedir(), '.vibe-usage'), { recursive: true });
150
+ writeFileSync(paths.file, generateLaunchdPlist(nodePath, binPath), 'utf-8');
151
+ console.log(`Created ${paths.file}`);
152
+
153
+ const result = run('launchctl', ['load', paths.file]);
154
+ if (!result.ok) {
155
+ console.error(`Failed to load service: ${result.output}`);
156
+ return;
157
+ }
158
+ console.log('Service loaded and started.');
159
+ }
160
+
161
+ console.log('\nDaemon installed. Usage data will sync automatically every 5 minutes.');
162
+ console.log('Run `vibe-usage daemon status` to check.');
163
+ }
164
+
165
+ function uninstall() {
166
+ const plat = detectPlatform();
167
+ if (!plat) {
168
+ console.log('No supported service platform detected.');
169
+ return;
170
+ }
171
+
172
+ const paths = getServicePaths(plat);
173
+
174
+ if (!existsSync(paths.file)) {
175
+ console.log('No daemon service is installed.');
176
+ return;
177
+ }
178
+
179
+ if (plat === 'systemd') {
180
+ run('systemctl', ['--user', 'stop', `${SERVICE_NAME}.service`]);
181
+ run('systemctl', ['--user', 'disable', `${SERVICE_NAME}.service`]);
182
+ unlinkSync(paths.file);
183
+ run('systemctl', ['--user', 'daemon-reload']);
184
+ console.log('Service stopped, disabled, and removed.');
185
+ }
186
+
187
+ if (plat === 'launchd') {
188
+ run('launchctl', ['unload', paths.file]);
189
+ unlinkSync(paths.file);
190
+ console.log('Service unloaded and removed.');
191
+ }
192
+ }
193
+
194
+ function status() {
195
+ const plat = detectPlatform();
196
+ if (!plat) {
197
+ console.log('No supported service platform detected.');
198
+ return;
199
+ }
200
+
201
+ const paths = getServicePaths(plat);
202
+
203
+ if (!existsSync(paths.file)) {
204
+ console.log('No daemon service is installed.');
205
+ console.log('Run `vibe-usage daemon install` to set up.');
206
+ return;
207
+ }
208
+
209
+ if (plat === 'systemd') {
210
+ const result = run('systemctl', ['--user', 'status', `${SERVICE_NAME}.service`]);
211
+ console.log(result.output);
212
+ }
213
+
214
+ if (plat === 'launchd') {
215
+ const result = run('launchctl', ['list', LAUNCHD_LABEL]);
216
+ if (result.ok) {
217
+ console.log(`Service: ${LAUNCHD_LABEL}`);
218
+ console.log(result.output);
219
+ } else {
220
+ console.log('Service is installed but not currently running.');
221
+ }
222
+ }
223
+ }
224
+
225
+ function stop() {
226
+ const plat = detectPlatform();
227
+ if (!plat) {
228
+ console.log('No supported service platform detected.');
229
+ return;
230
+ }
231
+
232
+ if (plat === 'systemd') {
233
+ const result = run('systemctl', ['--user', 'stop', `${SERVICE_NAME}.service`]);
234
+ console.log(result.ok ? 'Service stopped.' : `Failed: ${result.output}`);
235
+ }
236
+
237
+ if (plat === 'launchd') {
238
+ const result = run('launchctl', ['stop', LAUNCHD_LABEL]);
239
+ console.log(result.ok ? 'Service stopped.' : `Failed: ${result.output}`);
240
+ }
241
+ }
242
+
243
+ function restart() {
244
+ const plat = detectPlatform();
245
+ if (!plat) {
246
+ console.log('No supported service platform detected.');
247
+ return;
248
+ }
249
+
250
+ if (plat === 'systemd') {
251
+ const result = run('systemctl', ['--user', 'restart', `${SERVICE_NAME}.service`]);
252
+ console.log(result.ok ? 'Service restarted.' : `Failed: ${result.output}`);
253
+ }
254
+
255
+ if (plat === 'launchd') {
256
+ run('launchctl', ['stop', LAUNCHD_LABEL]);
257
+ const result = run('launchctl', ['start', LAUNCHD_LABEL]);
258
+ console.log(result.ok ? 'Service restarted.' : `Failed: ${result.output}`);
259
+ }
260
+ }
261
+
262
+ const SUBCOMMANDS = { install, uninstall, status, stop, restart };
263
+
264
+ export async function manageDaemon(subcommand) {
265
+ const fn = SUBCOMMANDS[subcommand];
266
+ if (!fn) {
267
+ console.error(`Unknown daemon subcommand: ${subcommand}`);
268
+ console.error('Usage: vibe-usage daemon <install|uninstall|status|stop|restart>');
269
+ process.exit(1);
270
+ }
271
+ fn();
272
+ }
package/src/index.js CHANGED
@@ -109,8 +109,19 @@ export async function run(args) {
109
109
  }
110
110
  case 'daemon':
111
111
  case '--daemon': {
112
- const { runDaemon } = await import('./daemon.js');
113
- await runDaemon();
112
+ const sub = args[1];
113
+ if (sub && ['install', 'uninstall', 'status', 'stop', 'restart'].includes(sub)) {
114
+ const { manageDaemon } = await import('./daemon-service.js');
115
+ await manageDaemon(sub);
116
+ } else {
117
+ const { runDaemon } = await import('./daemon.js');
118
+ await runDaemon();
119
+ }
120
+ break;
121
+ }
122
+ case 'skill': {
123
+ const { runSkill } = await import('./skill.js');
124
+ await runSkill(args.slice(1));
114
125
  break;
115
126
  }
116
127
  case 'config': {
@@ -131,9 +142,16 @@ export async function run(args) {
131
142
  npx @vibe-cafe/vibe-usage Init (first run) or sync
132
143
  npx @vibe-cafe/vibe-usage init Set up API key
133
144
  npx @vibe-cafe/vibe-usage sync Manually sync usage data
134
- npx @vibe-cafe/vibe-usage daemon Continuous sync (every 5m)
145
+ npx @vibe-cafe/vibe-usage daemon Continuous sync (every 5m, foreground)
146
+ npx @vibe-cafe/vibe-usage daemon install Install background service (systemd/launchd)
147
+ npx @vibe-cafe/vibe-usage daemon uninstall Remove background service
148
+ npx @vibe-cafe/vibe-usage daemon status Show background service status
149
+ npx @vibe-cafe/vibe-usage daemon stop Stop background service
150
+ npx @vibe-cafe/vibe-usage daemon restart Restart background service
135
151
  npx @vibe-cafe/vibe-usage reset Delete all data and re-upload
136
152
  npx @vibe-cafe/vibe-usage reset --local Delete data for this host only and re-upload
153
+ npx @vibe-cafe/vibe-usage skill Install skill for AI coding tools
154
+ npx @vibe-cafe/vibe-usage skill --remove Remove installed skills
137
155
  npx @vibe-cafe/vibe-usage status Show config and detected tools
138
156
  npx @vibe-cafe/vibe-usage config show Show full config as JSON
139
157
  npx @vibe-cafe/vibe-usage config get <key> Get a config value
package/src/init.js CHANGED
@@ -75,4 +75,8 @@ export async function runInit() {
75
75
  await runSync();
76
76
 
77
77
  console.log(`\nSetup complete! View your dashboard at: ${apiUrl}/usage`);
78
+
79
+ if (process.platform === 'linux' || process.platform === 'darwin') {
80
+ console.log('\nTip: Run `npx @vibe-cafe/vibe-usage daemon install` to sync automatically in the background.');
81
+ }
78
82
  }
@@ -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',