@vibe-cafe/vibe-usage 0.6.1 → 0.6.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/README.md CHANGED
@@ -5,7 +5,7 @@ Track your AI coding tool token usage and sync to [vibecafe.ai](https://vibecafe
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- npx vibe-usage
8
+ npx @vibe-cafe/vibe-usage
9
9
  ```
10
10
 
11
11
  This will:
@@ -16,13 +16,13 @@ This will:
16
16
  ## Commands
17
17
 
18
18
  ```bash
19
- npx vibe-usage # Init (first run) or sync (subsequent runs)
20
- npx vibe-usage init # Re-run setup
21
- npx vibe-usage sync # Manual sync
22
- npx vibe-usage daemon # Continuous sync (every 5 minutes)
23
- npx vibe-usage reset # Delete all data and re-upload from local logs
24
- npx vibe-usage reset --local # Delete this host's data only and re-upload
25
- npx vibe-usage status # Show config & detected tools
19
+ npx @vibe-cafe/vibe-usage # Init (first run) or sync (subsequent runs)
20
+ npx @vibe-cafe/vibe-usage init # Re-run setup
21
+ npx @vibe-cafe/vibe-usage sync # Manual sync
22
+ npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 5 minutes)
23
+ npx @vibe-cafe/vibe-usage reset # Delete all data and re-upload from local logs
24
+ npx @vibe-cafe/vibe-usage reset --local # Delete this host's data only and re-upload
25
+ npx @vibe-cafe/vibe-usage status # Show config & detected tools
26
26
  ```
27
27
 
28
28
  ## Supported Tools
@@ -31,6 +31,7 @@ npx vibe-usage status # Show config & detected tools
31
31
  |------|---------------|
32
32
  | Claude Code | `~/.claude/projects/` (tokens + sessions), `~/.claude/transcripts/` (sessions only) |
33
33
  | Codex CLI | `~/.codex/sessions/` |
34
+ | GitHub Copilot CLI | `~/.copilot/session-state/*/events.jsonl` |
34
35
  | Gemini CLI | `~/.gemini/tmp/` |
35
36
  | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
36
37
  | OpenClaw | `~/.openclaw/agents/` |
@@ -41,18 +42,18 @@ npx vibe-usage status # Show config & detected tools
41
42
 
42
43
  - Parses local session logs from each AI coding tool
43
44
  - Aggregates token usage into 30-minute buckets
44
- - Extracts session metadata from all 7 parsers: active time (sum of turn durations), total duration, message counts
45
+ - Extracts session metadata from all 8 parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
45
46
  - Uploads buckets + sessions to your vibecafe.ai dashboard
46
47
  - Stateless: computes full totals from local logs each sync (idempotent, no state files)
47
- - For continuous syncing, use `npx vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
48
+ - For continuous syncing, use `npx @vibe-cafe/vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
48
49
 
49
50
  ## Development
50
51
 
51
52
  Test against a local vibe-cafe dev server without publishing:
52
53
 
53
54
  ```bash
54
- VIBE_USAGE_DEV=1 VIBE_USAGE_API_URL=http://localhost:3000 npx vibe-usage init
55
- VIBE_USAGE_DEV=1 npx vibe-usage sync
55
+ VIBE_USAGE_DEV=1 VIBE_USAGE_API_URL=http://localhost:3000 npx @vibe-cafe/vibe-usage init
56
+ VIBE_USAGE_DEV=1 npx @vibe-cafe/vibe-usage sync
56
57
  ```
57
58
 
58
59
  `VIBE_USAGE_DEV=1` uses a separate config file (`~/.vibe-usage/config.dev.json`).
@@ -66,10 +67,10 @@ Config stored at `~/.vibe-usage/config.json` (dev: `config.dev.json`). Contains
66
67
  Run continuous syncing in the foreground (every 5 minutes):
67
68
 
68
69
  ```bash
69
- npx vibe-usage daemon
70
+ npx @vibe-cafe/vibe-usage daemon
70
71
  ```
71
72
 
72
- Press Ctrl+C to stop. For background use: `nohup npx vibe-usage daemon &`
73
+ Press Ctrl+C to stop. For background use: `nohup npx @vibe-cafe/vibe-usage daemon &`
73
74
 
74
75
  ## License
75
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.6.1",
3
+ "version": "0.6.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/index.js CHANGED
@@ -8,7 +8,7 @@ async function showStatus() {
8
8
 
9
9
  if (!config?.apiKey) {
10
10
  console.log(' Config: not configured');
11
- console.log(` Run \`npx vibe-usage init\` to set up.\n`);
11
+ console.log(` Run \`npx @vibe-cafe/vibe-usage init\` to set up.\n`);
12
12
  } else {
13
13
  console.log(` Config: ${getConfigPath()}`);
14
14
  console.log(` API key: ${config.apiKey.slice(0, 8)}...`);
@@ -128,17 +128,17 @@ export async function run(args) {
128
128
  vibe-usage - Vibe Usage Tracker by VibeCafé
129
129
 
130
130
  Usage:
131
- npx vibe-usage Init (first run) or sync
132
- npx vibe-usage init Set up API key
133
- npx vibe-usage sync Manually sync usage data
134
- npx vibe-usage daemon Continuous sync (every 5m)
135
- npx vibe-usage reset Delete all data and re-upload
136
- npx vibe-usage reset --local Delete data for this host only and re-upload
137
- npx vibe-usage status Show config and detected tools
138
- npx vibe-usage config show Show full config as JSON
139
- npx vibe-usage config get <key> Get a config value
140
- npx vibe-usage config set <key> <value> Set a config value
141
- npx vibe-usage help Show this help
131
+ npx @vibe-cafe/vibe-usage Init (first run) or sync
132
+ npx @vibe-cafe/vibe-usage init Set up API key
133
+ npx @vibe-cafe/vibe-usage sync Manually sync usage data
134
+ npx @vibe-cafe/vibe-usage daemon Continuous sync (every 5m)
135
+ npx @vibe-cafe/vibe-usage reset Delete all data and re-upload
136
+ npx @vibe-cafe/vibe-usage reset --local Delete data for this host only and re-upload
137
+ npx @vibe-cafe/vibe-usage status Show config and detected tools
138
+ npx @vibe-cafe/vibe-usage config show Show full config as JSON
139
+ npx @vibe-cafe/vibe-usage config get <key> Get a config value
140
+ npx @vibe-cafe/vibe-usage config set <key> <value> Set a config value
141
+ npx @vibe-cafe/vibe-usage help Show this help
142
142
  `);
143
143
  break;
144
144
  }
@@ -0,0 +1,128 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { aggregateToBuckets, extractSessions } from './index.js';
5
+
6
+ const SESSION_STATE_DIR = join(homedir(), '.copilot', 'session-state');
7
+
8
+ function findEventFiles(baseDir) {
9
+ const results = [];
10
+ if (!existsSync(baseDir)) return results;
11
+
12
+ try {
13
+ for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
14
+ if (!entry.isDirectory()) continue;
15
+
16
+ const eventsFile = join(baseDir, entry.name, 'events.jsonl');
17
+ if (existsSync(eventsFile)) {
18
+ results.push({ filePath: eventsFile, sessionId: entry.name });
19
+ }
20
+ }
21
+ } catch {
22
+ return results;
23
+ }
24
+
25
+ return results;
26
+ }
27
+
28
+ function getProjectFromContext(context) {
29
+ const projectPath = context?.gitRoot || context?.cwd;
30
+ if (!projectPath) return 'unknown';
31
+
32
+ return basename(projectPath) || 'unknown';
33
+ }
34
+
35
+ /**
36
+ * Parse GitHub Copilot CLI session logs from ~/.copilot/session-state.
37
+ * Returns usage buckets from session shutdown summaries and session metadata
38
+ * from user/assistant message timings.
39
+ */
40
+ export async function parse() {
41
+ const eventFiles = findEventFiles(SESSION_STATE_DIR);
42
+ if (eventFiles.length === 0) return { buckets: [], sessions: [] };
43
+
44
+ const entries = [];
45
+ const sessionEvents = [];
46
+
47
+ for (const { filePath, sessionId } of eventFiles) {
48
+ let content;
49
+ try {
50
+ content = readFileSync(filePath, 'utf-8');
51
+ } catch {
52
+ continue;
53
+ }
54
+
55
+ let currentProject = 'unknown';
56
+
57
+ for (const line of content.split('\n')) {
58
+ if (!line.trim()) continue;
59
+
60
+ try {
61
+ const obj = JSON.parse(line);
62
+ const timestamp = obj.timestamp ? new Date(obj.timestamp) : null;
63
+ const hasTimestamp = timestamp && !isNaN(timestamp.getTime());
64
+
65
+ if (obj.type === 'session.start' || obj.type === 'session.resume') {
66
+ currentProject = getProjectFromContext(obj.data?.context);
67
+ }
68
+
69
+ if (hasTimestamp && obj.type === 'user.message') {
70
+ sessionEvents.push({
71
+ sessionId,
72
+ source: 'copilot-cli',
73
+ project: currentProject,
74
+ timestamp,
75
+ role: 'user',
76
+ });
77
+ }
78
+
79
+ if (hasTimestamp && obj.type === 'assistant.message') {
80
+ sessionEvents.push({
81
+ sessionId,
82
+ source: 'copilot-cli',
83
+ project: currentProject,
84
+ timestamp,
85
+ role: 'assistant',
86
+ });
87
+ }
88
+
89
+ if (obj.type !== 'session.shutdown' || !hasTimestamp) continue;
90
+
91
+ const modelMetrics = obj.data?.modelMetrics || {};
92
+ for (const [model, metrics] of Object.entries(modelMetrics)) {
93
+ const usage = metrics?.usage;
94
+ if (!usage) continue;
95
+
96
+ const totalInput = usage.inputTokens || 0;
97
+ const cachedRead = usage.cacheReadTokens || 0;
98
+ const cacheWrite = usage.cacheWriteTokens || 0;
99
+ const output = usage.outputTokens || 0;
100
+
101
+ if (totalInput === 0 && cachedRead === 0 && cacheWrite === 0 && output === 0) {
102
+ continue;
103
+ }
104
+
105
+ entries.push({
106
+ source: 'copilot-cli',
107
+ model,
108
+ project: currentProject,
109
+ timestamp,
110
+ // Copilot reports cache reads separately, but cache writes are part of
111
+ // regular input for this schema because buckets don't have a dedicated field.
112
+ inputTokens: Math.max(0, totalInput - cachedRead),
113
+ outputTokens: output,
114
+ cachedInputTokens: cachedRead,
115
+ reasoningOutputTokens: 0,
116
+ });
117
+ }
118
+ } catch {
119
+ continue;
120
+ }
121
+ }
122
+ }
123
+
124
+ return {
125
+ buckets: aggregateToBuckets(entries),
126
+ sessions: extractSessions(sessionEvents),
127
+ };
128
+ }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { parse as parseClaudeCode } from './claude-code.js';
3
3
  import { parse as parseCodex } from './codex.js';
4
+ import { parse as parseCopilotCli } from './copilot-cli.js';
4
5
  import { parse as parseGeminiCli } from './gemini-cli.js';
5
6
  import { parse as parseOpencode } from './opencode.js';
6
7
  import { parse as parseOpenclaw } from './openclaw.js';
@@ -10,6 +11,7 @@ import { parse as parseKimiCode } from './kimi-code.js';
10
11
  export const parsers = {
11
12
  'claude-code': parseClaudeCode,
12
13
  'codex': parseCodex,
14
+ 'copilot-cli': parseCopilotCli,
13
15
  'gemini-cli': parseGeminiCli,
14
16
  'opencode': parseOpencode,
15
17
  'openclaw': parseOpenclaw,
@@ -60,8 +62,9 @@ export function aggregateToBuckets(entries) {
60
62
  * Extract session metadata from timing events.
61
63
  * Each event: { sessionId, source, project, timestamp: Date, role: 'user'|'assistant' }
62
64
  *
63
- * Turn = user prompt → last agent message before next user prompt.
64
- * activeSeconds = sum(turn durations). durationSeconds = wall clock.
65
+ * Turn = first AI response → last AI response before next user prompt.
66
+ * activeSeconds = sum(generation durations), excluding queue/TTFT wait.
67
+ * durationSeconds = wall clock from first to last message.
65
68
  */
66
69
  export function extractSessions(events) {
67
70
  const groups = new Map();
@@ -81,14 +84,20 @@ export function extractSessions(events) {
81
84
  let activeSeconds = 0;
82
85
  let turnStart = null;
83
86
  let turnEnd = null;
87
+ let waitingForFirstResponse = false;
84
88
 
85
89
  for (const event of sessionEvents) {
86
90
  if (event.role === 'user') {
87
91
  if (turnStart !== null && turnEnd !== null && turnEnd > turnStart) {
88
92
  activeSeconds += Math.round((turnEnd - turnStart) / 1000);
89
93
  }
94
+ turnStart = null;
95
+ turnEnd = null;
96
+ waitingForFirstResponse = true;
97
+ } else if (waitingForFirstResponse) {
90
98
  turnStart = event.timestamp;
91
99
  turnEnd = event.timestamp;
100
+ waitingForFirstResponse = false;
92
101
  } else if (turnStart !== null) {
93
102
  turnEnd = event.timestamp;
94
103
  }
package/src/tools.js CHANGED
@@ -13,6 +13,11 @@ export const TOOLS = [
13
13
  id: 'codex',
14
14
  dataDir: join(homedir(), '.codex', 'sessions'),
15
15
  },
16
+ {
17
+ name: 'GitHub Copilot CLI',
18
+ id: 'copilot-cli',
19
+ dataDir: join(homedir(), '.copilot', 'session-state'),
20
+ },
16
21
  {
17
22
  name: 'Gemini CLI',
18
23
  id: 'gemini-cli',