@vibe-cafe/vibe-usage 0.6.1 → 0.6.2

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
@@ -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,7 +42,7 @@ 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 (sum of turn durations), 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
48
  - For continuous syncing, use `npx vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
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.2",
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,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,
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',