@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 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 8 parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
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
@@ -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',