@vibe-cafe/vibe-usage 0.10.8 → 0.10.9

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
@@ -75,7 +75,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
75
75
  | Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
76
76
  | Trae CLI | macOS: `~/Library/Caches/trae-cli/sessions/`; Windows: `%LOCALAPPDATA%/trae-cli/cache/sessions/`; Linux: `~/.cache/trae-cli/sessions/` (CLI telemetry only; Trae IDE/Trae Work chats are not supported) |
77
77
  | Antigravity | App 2.0 `~/.gemini/antigravity/conversations/*.db` and `agy` CLI `~/.gemini/antigravity-cli/conversations/*.db` are parsed offline (tokens, real model display name, project, sessions); legacy App `.pb` history falls back to Connect RPC while the language server is running |
78
- | WorkBuddy | macOS WorkBuddy 5.3.11+: `~/.workbuddy/projects/**/*.jsonl` (fixture/relocation override: `VIBE_USAGE_WORKBUDDY_DIRS`). Uses completed assistant requests and their actual `providerData.requestModelId`, so Auto-routed requests can produce multiple model buckets. Splits cache reads and reasoning from inclusive input/output totals, deduplicates copied request IDs, and extracts local session timing without uploading message content. |
78
+ | WorkBuddy | Current releases: `~/.workbuddy-ai/projects/**/*.jsonl`; legacy releases: `~/.workbuddy/projects/**/*.jsonl` (fixture/relocation override: `VIBE_USAGE_WORKBUDDY_DIRS`). Reads usage-bearing completed assistant and `function_call` records, using the routed model identifier exposed as `providerData.requestModelId`. Splits cache reads and reasoning from inclusive input/output totals, deduplicates copied record IDs, and extracts local session timing without uploading message content. |
79
79
  | ZCode | `~/.zcode/cli/db/db.sqlite` (SQLite; reads the `message` table for per-message tokens, model, and project `cwd`/`root`, joined to `session.directory`) |
80
80
 
81
81
  ## How It Works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -40,7 +40,12 @@ function projectFromFile(filePath, projectsDir) {
40
40
  function projectFromRecord(record) {
41
41
  const cwd = typeof record.cwd === 'string' ? record.cwd.trim() : '';
42
42
  if (!cwd) return null;
43
- return basename(cwd.replace(/[\\/]+$/, '')) || null;
43
+ const parts = cwd
44
+ .replace(/[\\/]+$/, '')
45
+ .split(/[\\/]/)
46
+ .filter(Boolean)
47
+ .filter(part => !/^[a-zA-Z]:$/.test(part));
48
+ return parts.at(-1) || null;
44
49
  }
45
50
 
46
51
  function findJsonlFiles(dir, ctx) {
@@ -115,6 +120,13 @@ function isCompletedAssistant(record) {
115
120
  return status === 'completed' || status === 'complete' || status === 'success';
116
121
  }
117
122
 
123
+ function isUsageRecord(record) {
124
+ return isCompletedAssistant(record)
125
+ || (record.type === 'function_call'
126
+ && record.providerData
127
+ && typeof record.providerData === 'object');
128
+ }
129
+
118
130
  function modelFor(record) {
119
131
  const providerData = record.providerData && typeof record.providerData === 'object'
120
132
  ? record.providerData
@@ -156,20 +168,24 @@ function usageFor(record) {
156
168
 
157
169
  const inputDetails = primary?.input_details
158
170
  ?? primary?.inputDetails
159
- ?? primary?.inputTokensDetails;
171
+ ?? primary?.inputTokensDetails
172
+ ?? raw?.prompt_tokens_details;
160
173
  const outputDetails = primary?.output_details
161
174
  ?? primary?.outputDetails
162
- ?? primary?.outputTokensDetails;
175
+ ?? primary?.outputTokensDetails
176
+ ?? raw?.completion_tokens_details;
163
177
  const cachedInputTokens = firstDetailValue(inputDetails, 'cached_tokens', 'cachedTokens')
164
178
  || finite(
165
- primary?.cache_read_input_tokens
179
+ primary?.cachedInputTokens
180
+ ?? primary?.cache_read_input_tokens
166
181
  ?? primary?.cacheReadInputTokens
167
182
  ?? raw?.prompt_cache_hit_tokens
168
183
  ?? raw?.cache_read_input_tokens
169
184
  );
170
185
  const reasoningOutputTokens = firstDetailValue(outputDetails, 'reasoning_tokens', 'reasoningTokens')
171
186
  || finite(
172
- primary?.completion_thinking_tokens
187
+ primary?.reasoningOutputTokens
188
+ ?? primary?.completion_thinking_tokens
173
189
  ?? primary?.reasoning_tokens
174
190
  ?? primary?.reasoningTokens
175
191
  ?? raw?.completion_thinking_tokens
@@ -207,10 +223,16 @@ function timestampFor(record) {
207
223
  );
208
224
  }
209
225
 
226
+ function sessionEventsWithPrompts(events) {
227
+ const sessionsWithUsers = new Set(
228
+ events.filter(event => event.role === 'user').map(event => event.sessionId)
229
+ );
230
+ return events.filter(event => sessionsWithUsers.has(event.sessionId));
231
+ }
232
+
210
233
  export async function parse() {
211
234
  const entriesById = new Map();
212
- const eventsById = new Map();
213
- const anonymousEvents = [];
235
+ const eventsByKey = new Map();
214
236
  const ctx = { skipped: false, warnings: [] };
215
237
  const projectDirs = [...new Set(findWorkbuddyDataDirs().map(root => (
216
238
  basename(root) === 'projects' ? root : join(root, 'projects')
@@ -226,7 +248,7 @@ export async function parse() {
226
248
  continue;
227
249
  }
228
250
 
229
- const sessionId = basename(filePath, '.jsonl');
251
+ const fallbackSessionId = basename(filePath, '.jsonl');
230
252
  let project = projectFromFile(filePath, projectsDir);
231
253
  const fileEntries = [];
232
254
  const fileEvents = [];
@@ -236,14 +258,22 @@ export async function parse() {
236
258
  const timestamp = timestampFor(record);
237
259
  const id = recordId(record);
238
260
  const role = roleFor(record);
261
+ const explicitSessionId = record.sessionId ?? record.session_id;
262
+ const sessionId = explicitSessionId == null || String(explicitSessionId).trim() === ''
263
+ ? fallbackSessionId
264
+ : String(explicitSessionId);
239
265
 
240
- if (timestamp && (role === 'user' || isCompletedAssistant(record))) {
241
- fileEvents.push({ id, sessionId, timestamp, role });
266
+ const usage = isUsageRecord(record) ? usageFor(record) : null;
267
+ const eventRole = role === 'user'
268
+ ? 'user'
269
+ : isCompletedAssistant(record) || (record.type === 'function_call' && usage)
270
+ ? 'assistant'
271
+ : null;
272
+ if (timestamp && eventRole) {
273
+ fileEvents.push({ id, sessionId, timestamp, role: eventRole });
242
274
  }
243
275
 
244
- if (!id || !timestamp || !isCompletedAssistant(record)) return;
245
- const usage = usageFor(record);
246
- if (!usage) return;
276
+ if (!id || !timestamp || !usage) return;
247
277
  fileEntries.push({
248
278
  id,
249
279
  score: usage.score,
@@ -272,15 +302,17 @@ export async function parse() {
272
302
  timestamp: candidate.timestamp,
273
303
  role: candidate.role,
274
304
  };
275
- if (candidate.id) eventsById.set(candidate.id, event);
276
- else anonymousEvents.push(event);
305
+ const key = candidate.id
306
+ ? `id:${candidate.sessionId}:${candidate.id}:${candidate.role}`
307
+ : `fallback:${candidate.sessionId}:${candidate.role}:${candidate.timestamp.toISOString()}`;
308
+ eventsByKey.set(key, event);
277
309
  }
278
310
  }
279
311
  }
280
312
 
281
313
  return {
282
314
  buckets: aggregateToBuckets([...entriesById.values()].map(({ entry }) => entry)),
283
- sessions: extractSessions([...eventsById.values(), ...anonymousEvents]),
315
+ sessions: extractSessions(sessionEventsWithPrompts([...eventsByKey.values()])),
284
316
  ...(ctx.skipped ? { skipped: true } : {}),
285
317
  ...(ctx.warnings.length > 0 ? { warnings: ctx.warnings } : {}),
286
318
  };
package/src/tools.js CHANGED
@@ -335,7 +335,7 @@ export const TOOLS = [
335
335
  {
336
336
  name: 'WorkBuddy',
337
337
  id: 'workbuddy',
338
- dataDir: join(homedir(), '.workbuddy', 'projects'),
338
+ dataDir: join(homedir(), '.workbuddy-ai', 'projects'),
339
339
  detectDataDirs: () => findWorkbuddyDataDirs().filter(existsSync),
340
340
  },
341
341
  {
@@ -1,15 +1,18 @@
1
1
  import { delimiter, join } from 'node:path';
2
2
  import { homedir } from 'node:os';
3
3
 
4
- export function getDefaultWorkbuddyProjectsDir() {
5
- return join(homedir(), '.workbuddy', 'projects');
4
+ export function getDefaultWorkbuddyProjectsDirs(home = homedir()) {
5
+ return [
6
+ join(home, '.workbuddy-ai', 'projects'),
7
+ join(home, '.workbuddy', 'projects'),
8
+ ];
6
9
  }
7
10
 
8
11
  // Fixture/relocation hook. Entries may name either the WorkBuddy home or its
9
12
  // projects/ directory; the parser normalizes both forms.
10
13
  export function findWorkbuddyDataDirs() {
11
14
  const override = process.env.VIBE_USAGE_WORKBUDDY_DIRS?.trim();
12
- if (!override) return [getDefaultWorkbuddyProjectsDir()];
15
+ if (!override) return getDefaultWorkbuddyProjectsDirs();
13
16
  return [...new Set(
14
17
  override
15
18
  .split(delimiter)