@vibe-cafe/vibe-usage 0.10.5 → 0.10.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
@@ -51,39 +51,43 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
51
51
 
52
52
  | Tool | Data Location |
53
53
  |------|---------------|
54
- | Claude Code | `~/.claude/projects/` (tokens + sessions), `~/.claude/transcripts/` (sessions only); also scans `$CLAUDE_CONFIG_DIR` and data-bearing `~/.claude-*` profiles, selects the most complete duplicate session, and streams logs so large active sessions are not silently omitted. Cache creation tokens are included in input usage. |
54
+ | Alma | Electron app-data `alma/chat_threads.db` (macOS: `~/Library/Application Support/alma/chat_threads.db`; fixture/relocation override: `VIBE_USAGE_ALMA_DB`). Reads the `usage_records` ledger plus workspace names without selecting chat bodies, message metadata, provider credentials, or full workspace paths. Cache writes are included in input usage. The ledger contains assistant responses only, so Alma emits token buckets without session timing. |
55
+ | Claude Code + Claude Desktop Code/Cowork | Claude Code data in `~/.claude/projects/` (tokens + sessions) and `~/.claude/transcripts/` (sessions only), plus Claude Desktop Cowork's per-session `.claude/projects/` directories. Also scans `$CLAUDE_CONFIG_DIR` and data-bearing `~/.claude-*` profiles. All variants use the existing `claude-code` source; the parser selects the most complete copy of each session so shared/copied transcripts are not counted twice. Logs are streamed and cache creation tokens are included in input usage. |
55
56
  | Codex CLI | `$CODEX_HOME/sessions/` and `$CODEX_HOME/archived_sessions/` (default `~/.codex`), plus an optional temporary `--extra-codex-home` or manually persisted `codexExtraHome`; a versioned local index avoids re-reading unchanged rollouts and reads only safe append tails for ordinary sessions, while fork/sub-agent replay matching, duplicate suppression, and live/archive/cross-root deduplication retain their existing semantics |
56
57
  | Grok | `$GROK_HOME/sessions/<encoded-cwd>/<session-id>/` (default `~/.grok`); token usage from `updates.jsonl` `turn_completed.usage` (per-model `modelUsage`, cache reads, reasoning); project from `summary.json` cwd; honors `GROK_HOME` |
57
58
  | GitHub Copilot CLI | `~/.copilot/session-state/*/events.jsonl` |
59
+ | CraftAgent | `~/.craft-agent/workspaces/*/sessions/*/.pi-sessions/*.jsonl`; honors `$CRAFT_AGENT_DIR` / `$CRAFTAGENT_DIR`; cache writes are included in input usage |
58
60
  | Cursor | `state.vscdb` (SQLite, reads `cursorAuth/accessToken`, fetches CSV from `cursor.com`); cloud data is stamped with a fixed `cursor-cloud` hostname so multi-machine setups don't double-count |
59
61
  | DimAgent | `$DIMCODE_HOME/dimcode.sqlite` (default `~/.dimcode/v2/dimcode.sqlite`); exact usage from `usage_ledger`, with forked ledger/history copies deduplicated |
60
62
  | Gemini CLI | `~/.gemini/tmp/<project_hash>/chats/session-*.jsonl` (current line-delimited format) and legacy `session-*.json`; recurses into nested subagent sessions |
61
63
  | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
62
- | OpenClaw | `~/.openclaw/agents/`, `~/.openclaw-<profile>/agents/` (profile deployments) |
63
- | pi | `~/.pi/agent/sessions/` |
64
+ | OpenClaw | `~/.openclaw/agents/`, `~/.openclaw-<profile>/agents/` (profile deployments); cache-creation/cache-write tokens are included in input usage |
65
+ | Oh My Pi | `~/.omp/agent/sessions/`, `~/.omp/profiles/*/agent/sessions/`, and `$XDG_DATA_HOME/omp/{sessions,profiles/*/sessions}`; recognizes OMP's `$PI_CODING_AGENT_DIR`, current v3 title slots and path/hashed session directories, deduplicates copied records, includes cache writes in input, and splits reasoning from OMP's inclusive output count |
66
+ | pi | `~/.pi/agent/sessions/` or `$PI_CODING_AGENT_DIR/sessions/`; cache writes are included in input usage |
64
67
  | Qwen Code | `~/.qwen/tmp/` |
65
68
  | Kimi Code | Current `~/.kimi-code/sessions/wd_<slug>_<hash>/session_<id>/agents/<agent>/wire.jsonl` (`usage.record` deltas, including retry/compaction scope and cache creation; main/subagent wires form one session), data root resolved via `$KIMI_CODE_HOME` like the CLI itself, with project names from `session_index.jsonl`; legacy `~/.kimi/sessions/` is parsed alongside (`kimi migrate` never carries usage over, so both stores are always merged) |
66
69
  | MiMoCode | `$MIMOCODE_HOME/data/mimocode.db`, `$XDG_DATA_HOME/mimocode/mimocode.db`, or `~/.local/share/mimocode/mimocode.db` (SQLite; exact input, output, reasoning, and cache-read tokens from assistant messages; honors `MIMOCODE_DB`; cache-write tokens are included in input usage) |
67
- | Amp | `~/.local/share/amp/threads/` |
70
+ | Amp | `~/.local/share/amp/threads/`; cache-creation tokens are included in input usage |
68
71
  | Droid | `~/.factory/sessions/` |
69
72
  | Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
70
73
  | Kiro | Kiro CLI native event streams `~/.kiro/sessions/cli/*.jsonl` (estimated tokens from message text: input = prompt + tool results, output = reply + tool calls, reasoning = thinking, cacheRead = re-sent context; thinking-block signatures excluded). Falls back to `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` + optional `~/.kiro_sessions/*.json` archives, then IDE `q-client.log` whole-credit deltas as `kiro-credits` (floored cumulative diff — the server stores token counts as bigint); legacy IDE `dev_data/devdata.sqlite` token telemetry is opt-in with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
71
- | Cline | `<host>/User/globalStorage/saoudrizwan.claude-dev/{state/taskHistory.json,tasks/<id>/ui_messages.json}` (walks all VSCode-fork hosts: Code, Cursor, Windsurf, VSCodium, Trae, ...) |
74
+ | Cline | Standalone `~/.cline/` plus `<host>/User/globalStorage/saoudrizwan.claude-dev/` across VSCode-fork hosts; migrated copies are deduplicated and empty leftover extension stores no longer count as installed |
72
75
  | Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
73
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) |
74
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. |
75
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`) |
76
80
 
77
81
  ## How It Works
78
82
 
79
83
  - Parses local session logs from each AI coding tool
80
84
  - Aggregates token usage into 30-minute buckets
81
- - Extracts session metadata from all parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
85
+ - Extracts session metadata where the source safely exposes user/assistant timing: active time (AI generation time, excluding queue/TTFT wait), total duration, and message counts. Alma intentionally emits buckets only because its usage ledger contains assistant responses and the parser does not read chat records.
82
86
  - Uploads buckets + sessions to your vibecafe.ai dashboard (always gzip-compressed, ~94% smaller)
83
87
  - Incremental upload: every parser emits a complete local snapshot, then only buckets/sessions that are new or changed since the last successful upload are sent — a quiet machine uploads nothing. Upload state remains in `~/.vibe-usage/state.json`; failed or still-indexing parsers retain their prior state, while deleted local logs are pruned. Deleting the state file triggers a one-time full re-upload, and `reset` clears it automatically after deleting cloud data
84
88
  - Incremental Codex parsing: a versioned, disposable cache under `~/.vibe-usage/cache/codex/` stores per-rollout aggregate results and parser continuation state. Unchanged rollouts require no raw-log reads; an ordinary append reads only the new tail; forks, sub-agents, replacements, truncations, and failed safety checks fall back to the full correctness path. A bounded rolling audit occasionally re-reads one historical file. Very large first-time indexes checkpoint before the Mac app timeout and resume on the next sync instead of restarting
85
89
  - The Codex parser cache contains derived aggregates and replay metadata, not raw prompt or response text. It is independent of upload state and can be deleted safely (the next sync rebuilds it). `reset` intentionally keeps it so the required full re-upload does not also require a full disk rescan. Set `VIBE_USAGE_CODEX_CACHE=0` to disable the optimization for diagnosis
86
- - SQLite-backed tools (Cursor, OpenCode, Kiro, Hermes) are read via Node's built-in `node:sqlite` on Node ≥ 22.5 — no `sqlite3` binary needed (works on Windows out of the box); on older Node it falls back to the system `sqlite3` CLI
90
+ - SQLite-backed tools are read via Node's built-in `node:sqlite` on Node ≥ 22.5 — no `sqlite3` binary needed (works on Windows out of the box); on older Node the CLI falls back to the system `sqlite3` executable
87
91
  - For continuous syncing, use `npx @vibe-cafe/vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
88
92
 
89
93
  ## Trust Model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.5",
3
+ "version": "0.10.8",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -2,6 +2,9 @@ import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
2
2
  import { delimiter, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
+ const MAX_DESKTOP_DISCOVERY_DEPTH = 8;
6
+ const DESKTOP_NON_SESSION_DIRS = new Set(['rpm', 'skills']);
7
+
5
8
  function expandHome(value) {
6
9
  const trimmed = value.trim().replace(/[/\\]+$/, '');
7
10
  if (trimmed === '~') return homedir();
@@ -15,18 +18,95 @@ function hasClaudeData(root) {
15
18
  return existsSync(join(root, 'projects')) || existsSync(join(root, 'transcripts'));
16
19
  }
17
20
 
21
+ function defaultClaudeDesktopDataDir() {
22
+ if (process.platform === 'darwin') {
23
+ return join(homedir(), 'Library', 'Application Support', 'Claude');
24
+ }
25
+ if (process.platform === 'win32') {
26
+ const appData = process.env.APPDATA?.trim();
27
+ return appData
28
+ ? join(expandHome(appData), 'Claude')
29
+ : join(homedir(), 'AppData', 'Roaming', 'Claude');
30
+ }
31
+ const configHome = process.env.XDG_CONFIG_HOME?.trim();
32
+ return join(configHome ? expandHome(configHome) : join(homedir(), '.config'), 'Claude');
33
+ }
34
+
35
+ function getClaudeDesktopDataDirs() {
36
+ const override = process.env.VIBE_USAGE_CLAUDE_DESKTOP_DIRS?.trim();
37
+ return override
38
+ ? override.split(delimiter).map(expandHome).filter(Boolean)
39
+ : [defaultClaudeDesktopDataDir()];
40
+ }
41
+
42
+ function discoverDesktopRoots(dir, depth, roots, onWarning) {
43
+ let entries;
44
+ try {
45
+ entries = readdirSync(dir, { withFileTypes: true });
46
+ } catch (err) {
47
+ if (err?.code !== 'ENOENT') {
48
+ onWarning(`Claude Desktop: cannot read directory ${dir}: ${err.message}`);
49
+ }
50
+ return;
51
+ }
52
+
53
+ // Once a session root is found, do not descend into the user's Cowork files.
54
+ // Those directories can be large and are unrelated to Claude's transcript.
55
+ const claudeEntry = entries.find(
56
+ (entry) => entry.name === '.claude' && entry.isDirectory(),
57
+ );
58
+ if (claudeEntry) {
59
+ roots.push(join(dir, claudeEntry.name));
60
+ return;
61
+ }
62
+
63
+ for (const entry of entries) {
64
+ if (!entry.isDirectory()) continue;
65
+ if (DESKTOP_NON_SESSION_DIRS.has(entry.name)) continue;
66
+ const candidate = join(dir, entry.name);
67
+ if (depth < MAX_DESKTOP_DISCOVERY_DEPTH) {
68
+ discoverDesktopRoots(candidate, depth + 1, roots, onWarning);
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Find the private Claude Code state roots created for Claude Desktop Cowork.
75
+ * Desktop Code itself uses the normal ~/.claude root, while Cowork isolates
76
+ * each local-agent session below the Electron user-data directory.
77
+ */
78
+ export function findClaudeDesktopRoots(
79
+ desktopDataDirs = getClaudeDesktopDataDirs(),
80
+ onWarning = () => {},
81
+ ) {
82
+ const roots = [];
83
+ for (const dataDir of desktopDataDirs) {
84
+ discoverDesktopRoots(
85
+ join(dataDir, 'local-agent-mode-sessions'),
86
+ 0,
87
+ roots,
88
+ onWarning,
89
+ );
90
+ }
91
+ return roots;
92
+ }
93
+
18
94
  /**
19
- * Return every Claude Code state root visible from this process.
95
+ * Return every Claude Code-compatible state root visible from this process.
20
96
  *
21
97
  * In addition to the default and CLAUDE_CONFIG_DIR, discover the documented
22
98
  * multi-profile convention (~/.claude-work, ~/.claude-personal, ...). This is
23
99
  * important for launchd/systemd and GUI processes, which commonly do not
24
100
  * inherit the shell environment used to launch Claude Code.
25
101
  *
26
- * VIBE_USAGE_CLAUDE_DIRS is a test/diagnostic override. It replaces discovery
27
- * with a path.delimiter-separated root list.
102
+ * Claude Desktop Code uses the default Claude Code root. Cowork creates a
103
+ * private .claude root per local-agent session, so those roots are discovered
104
+ * recursively under the app's user-data directory.
105
+ *
106
+ * VIBE_USAGE_CLAUDE_DIRS is a test/diagnostic override. It replaces all normal
107
+ * and Desktop discovery with a path.delimiter-separated root list.
28
108
  */
29
- export function getClaudeRoots() {
109
+ export function getClaudeRoots({ onWarning = () => {} } = {}) {
30
110
  const override = process.env.VIBE_USAGE_CLAUDE_DIRS?.trim();
31
111
  const roots = override
32
112
  ? override.split(delimiter).map(expandHome).filter(Boolean)
@@ -47,6 +127,8 @@ export function getClaudeRoots() {
47
127
  } catch {
48
128
  // The default/configured roots remain usable if home discovery fails.
49
129
  }
130
+
131
+ roots.push(...findClaudeDesktopRoots(getClaudeDesktopDataDirs(), onWarning));
50
132
  }
51
133
 
52
134
  const seen = new Set();
@@ -0,0 +1,40 @@
1
+ import { statSync } from 'node:fs';
2
+ import { delimiter, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ const EXTENSION_ID = 'saoudrizwan.claude-dev';
6
+ const HOSTS = ['Code', 'Cursor', 'Windsurf', 'VSCodium', 'Code - Insiders', 'Trae', 'Trae CN'];
7
+
8
+ function hasTaskHistory(root) {
9
+ try {
10
+ return statSync(join(root, 'state', 'taskHistory.json')).isFile();
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ function hostRoots() {
17
+ const out = [];
18
+ if (process.platform === 'darwin') {
19
+ const base = join(homedir(), 'Library', 'Application Support');
20
+ for (const host of HOSTS) out.push(join(base, host));
21
+ } else if (process.platform === 'win32') {
22
+ const base = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
23
+ for (const host of HOSTS) out.push(join(base, host));
24
+ } else {
25
+ const base = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
26
+ for (const host of HOSTS) out.push(join(base, host));
27
+ }
28
+ return out;
29
+ }
30
+
31
+ export function findClineDataDirs() {
32
+ const override = process.env.VIBE_USAGE_CLINE_DIRS?.trim();
33
+ const candidates = override
34
+ ? override.split(delimiter).map((value) => value.trim()).filter(Boolean)
35
+ : [
36
+ join(homedir(), '.cline'),
37
+ ...hostRoots().map((root) => join(root, 'User', 'globalStorage', EXTENSION_ID)),
38
+ ];
39
+ return [...new Set(candidates)].filter(hasTaskHistory);
40
+ }
@@ -0,0 +1,15 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ export function getCraftWorkspacesDir() {
6
+ const root = process.env.CRAFT_AGENT_DIR?.trim()
7
+ || process.env.CRAFTAGENT_DIR?.trim()
8
+ || join(homedir(), '.craft-agent');
9
+ return join(root, 'workspaces');
10
+ }
11
+
12
+ export function findCraftDataDirs() {
13
+ const workspaces = getCraftWorkspacesDir();
14
+ return existsSync(workspaces) ? [workspaces] : [];
15
+ }
@@ -0,0 +1,91 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { getAlmaDbPath } from '../tools.js';
4
+ import { aggregateToBuckets } from './index.js';
5
+ import { queryDbJson } from './sqlite.js';
6
+
7
+ export { getAlmaDbPath as resolveAlmaDbPath };
8
+
9
+ const ALMA_USAGE_SQL = `
10
+ SELECT
11
+ usage_records.model AS model,
12
+ usage_records.timestamp AS timestamp,
13
+ usage_records.input_tokens AS inputTokens,
14
+ usage_records.output_tokens AS outputTokens,
15
+ usage_records.cached_input_tokens AS cachedInputTokens,
16
+ usage_records.reasoning_tokens AS reasoningOutputTokens,
17
+ usage_records.cache_write_input_tokens AS cacheWriteInputTokens,
18
+ workspaces.name AS workspaceName
19
+ FROM usage_records
20
+ LEFT JOIN chat_threads ON chat_threads.id = usage_records.thread_id
21
+ LEFT JOIN workspaces ON workspaces.id = chat_threads.workspace_id
22
+ `;
23
+
24
+ function tokenCount(value) {
25
+ const number = Number(value);
26
+ return Number.isFinite(number) && number > 0 ? number : 0;
27
+ }
28
+
29
+ function safeWorkspaceName(value) {
30
+ if (typeof value !== 'string') return 'unknown';
31
+ const normalized = value.trim().replace(/\\/g, '/').replace(/\/+$/, '');
32
+ return basename(normalized) || 'unknown';
33
+ }
34
+
35
+ function skippedResult(error) {
36
+ const message = error?.message || String(error);
37
+ let reason = message;
38
+ if (/database is locked/i.test(message)) reason = 'database is locked';
39
+ else if (/no such (table|column)/i.test(message)) reason = 'incompatible database schema';
40
+ return {
41
+ buckets: [],
42
+ sessions: [],
43
+ skipped: true,
44
+ warnings: [`alma: cannot read usage database (${reason})`],
45
+ };
46
+ }
47
+
48
+ export async function parse() {
49
+ const dbPath = getAlmaDbPath();
50
+ if (!existsSync(dbPath)) return { buckets: [], sessions: [] };
51
+
52
+ let rows;
53
+ try {
54
+ rows = queryDbJson(dbPath, ALMA_USAGE_SQL);
55
+ } catch (error) {
56
+ if (error?.status === 127 || /ENOENT/i.test(error?.message)) {
57
+ throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Alma data.');
58
+ }
59
+ return skippedResult(error);
60
+ }
61
+
62
+ const entries = [];
63
+ for (const row of rows) {
64
+ const timestamp = new Date(row.timestamp);
65
+ if (Number.isNaN(timestamp.getTime())) continue;
66
+
67
+ const inputTokens = tokenCount(row.inputTokens) + tokenCount(row.cacheWriteInputTokens);
68
+ const outputTokens = tokenCount(row.outputTokens);
69
+ const cachedInputTokens = tokenCount(row.cachedInputTokens);
70
+ const reasoningOutputTokens = tokenCount(row.reasoningOutputTokens);
71
+ if (inputTokens + outputTokens + cachedInputTokens + reasoningOutputTokens === 0) continue;
72
+
73
+ entries.push({
74
+ source: 'alma',
75
+ model: row.model || 'unknown',
76
+ project: safeWorkspaceName(row.workspaceName),
77
+ timestamp,
78
+ inputTokens,
79
+ outputTokens,
80
+ cachedInputTokens,
81
+ reasoningOutputTokens,
82
+ });
83
+ }
84
+
85
+ return {
86
+ buckets: aggregateToBuckets(entries),
87
+ // Alma's usage ledger contains assistant responses only. Reconstructing
88
+ // user turns would require reading chat records outside the usage contract.
89
+ sessions: [],
90
+ };
91
+ }
@@ -79,17 +79,23 @@ export async function parse() {
79
79
 
80
80
  const inputTokens = event?.tokens?.input || 0;
81
81
  const outputTokens = event?.tokens?.output || 0;
82
- if (inputTokens === 0 && outputTokens === 0) continue;
83
82
 
84
83
  const toMessage = Number.isInteger(event.toMessageId) ? messages[event.toMessageId] : null;
85
84
  const cacheReadInputTokens = toMessage?.usage?.cacheReadInputTokens || 0;
85
+ const cacheCreationInputTokens = toMessage?.usage?.cacheCreationInputTokens || 0;
86
+ if (
87
+ inputTokens === 0
88
+ && outputTokens === 0
89
+ && cacheReadInputTokens === 0
90
+ && cacheCreationInputTokens === 0
91
+ ) continue;
86
92
 
87
93
  entries.push({
88
94
  source: 'amp',
89
95
  model: event?.model || 'unknown',
90
96
  project: 'unknown',
91
97
  timestamp: ts,
92
- inputTokens,
98
+ inputTokens: inputTokens + cacheCreationInputTokens,
93
99
  outputTokens,
94
100
  cachedInputTokens: cacheReadInputTokens,
95
101
  reasoningOutputTokens: 0,
@@ -105,14 +111,20 @@ export async function parse() {
105
111
 
106
112
  const inputTokens = usage.inputTokens || 0;
107
113
  const outputTokens = usage.outputTokens || 0;
108
- if (inputTokens === 0 && outputTokens === 0 && (usage.cacheReadInputTokens || 0) === 0) continue;
114
+ const cacheCreationInputTokens = usage.cacheCreationInputTokens || 0;
115
+ if (
116
+ inputTokens === 0
117
+ && outputTokens === 0
118
+ && (usage.cacheReadInputTokens || 0) === 0
119
+ && cacheCreationInputTokens === 0
120
+ ) continue;
109
121
 
110
122
  entries.push({
111
123
  source: 'amp',
112
124
  model: usage.model || 'unknown',
113
125
  project: 'unknown',
114
126
  timestamp: ts,
115
- inputTokens,
127
+ inputTokens: inputTokens + cacheCreationInputTokens,
116
128
  outputTokens,
117
129
  cachedInputTokens: usage.cacheReadInputTokens || 0,
118
130
  reasoningOutputTokens: 0,
@@ -272,7 +272,9 @@ export async function parse() {
272
272
  warnings: [],
273
273
  incomplete: false,
274
274
  };
275
- const roots = getClaudeRoots();
275
+ const roots = getClaudeRoots({
276
+ onWarning: (message) => addWarning(ctx, message),
277
+ });
276
278
  const projectGroups = collectCandidates(roots, 'projects', ctx);
277
279
  const projectSessionIds = new Set();
278
280
 
@@ -1,40 +1,7 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
- import { homedir } from 'node:os';
4
3
  import { aggregateToBuckets, extractSessions } from './index.js';
5
-
6
- const EXTENSION_ID = 'saoudrizwan.claude-dev';
7
-
8
- // VSCode-fork application names that may host extensions.
9
- const HOSTS = ['Code', 'Cursor', 'Windsurf', 'VSCodium', 'Code - Insiders', 'Trae', 'Trae CN'];
10
-
11
- function getHostRoots() {
12
- const out = [];
13
- if (process.platform === 'darwin') {
14
- const base = join(homedir(), 'Library', 'Application Support');
15
- for (const h of HOSTS) out.push(join(base, h));
16
- } else if (process.platform === 'win32') {
17
- const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
18
- for (const h of HOSTS) out.push(join(appData, h));
19
- } else {
20
- const xdg = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
21
- for (const h of HOSTS) out.push(join(xdg, h));
22
- }
23
- return out;
24
- }
25
-
26
- export function findClineExtensionDirs() {
27
- const dirs = [];
28
- for (const root of getHostRoots()) {
29
- const ext = join(root, 'User', 'globalStorage', EXTENSION_ID);
30
- try {
31
- if (statSync(ext).isDirectory()) dirs.push(ext);
32
- } catch {
33
- // not installed in this host; skip
34
- }
35
- }
36
- return dirs;
37
- }
4
+ import { findClineDataDirs } from '../cline-roots.js';
38
5
 
39
6
  function readJsonSafe(path) {
40
7
  try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
@@ -48,24 +15,44 @@ function projectFromPath(absPath) {
48
15
  }
49
16
 
50
17
  export async function parse() {
51
- const extDirs = findClineExtensionDirs();
18
+ const extDirs = findClineDataDirs();
52
19
  if (extDirs.length === 0) return { buckets: [], sessions: [] };
53
20
 
54
21
  const entries = [];
55
22
  const events = [];
56
23
 
24
+ const candidates = new Map();
57
25
  for (const extDir of extDirs) {
58
26
  const history = readJsonSafe(join(extDir, 'state', 'taskHistory.json'));
59
27
  if (!Array.isArray(history)) continue;
60
28
 
61
29
  for (const item of history) {
30
+ if (!item || typeof item !== 'object' || !item.id) continue;
31
+ const taskId = String(item.id);
32
+ const messagesPath = join(extDir, 'tasks', taskId, 'ui_messages.json');
33
+ let stat;
34
+ try {
35
+ stat = statSync(messagesPath);
36
+ } catch {
37
+ continue;
38
+ }
39
+ const key = item.ulid ? String(item.ulid) : taskId;
40
+ const next = { item, taskId, messagesPath, size: stat.size, mtimeMs: stat.mtimeMs };
41
+ const current = candidates.get(key);
42
+ if (!current || next.size > current.size || (
43
+ next.size === current.size && next.mtimeMs > current.mtimeMs
44
+ )) candidates.set(key, next);
45
+ }
46
+ }
47
+
48
+ for (const { item, taskId, messagesPath } of candidates.values()) {
62
49
  try {
63
- if (!item || typeof item !== 'object' || !item.id) continue;
64
- const taskId = String(item.id);
65
- const project = projectFromPath(item.cwdOnTaskInitialization || item.shadowGitConfigWorkTree);
50
+ const project = projectFromPath(
51
+ item.cwdOnTaskInitialization || item.shadowGitConfigWorkTree || item.cwd,
52
+ );
66
53
  const fallbackModel = (item.modelId && String(item.modelId).trim()) || 'cline-unknown';
67
54
 
68
- const messages = readJsonSafe(join(extDir, 'tasks', taskId, 'ui_messages.json'));
55
+ const messages = readJsonSafe(messagesPath);
69
56
  if (!Array.isArray(messages)) continue;
70
57
 
71
58
  for (const msg of messages) {
@@ -109,7 +96,6 @@ export async function parse() {
109
96
  } catch {
110
97
  // Skip this task; keep going for the rest of the history.
111
98
  }
112
- }
113
99
  }
114
100
 
115
101
  return { buckets: aggregateToBuckets(entries), sessions: extractSessions(events) };
@@ -0,0 +1,21 @@
1
+ import { getCraftWorkspacesDir } from '../craft-roots.js';
2
+ import { parsePiSessionJsonl } from './pi-session-jsonl.js';
3
+
4
+ function isCraftPiSession(filePath) {
5
+ return filePath.split(/[\\/]/).includes('.pi-sessions');
6
+ }
7
+
8
+ function projectFromCraftPath(filePath) {
9
+ const parts = filePath.replace(/\\/g, '/').split('/');
10
+ const sessionsIndex = parts.lastIndexOf('sessions');
11
+ return parts[sessionsIndex + 1] || 'unknown';
12
+ }
13
+
14
+ export async function parse() {
15
+ return parsePiSessionJsonl({
16
+ source: 'craft-agent',
17
+ sessionsDirs: [getCraftWorkspacesDir()],
18
+ includeFile: isCraftPiSession,
19
+ projectFromPath: projectFromCraftPath,
20
+ });
21
+ }
@@ -3,6 +3,7 @@ import { parse as parseClaudeCode } from './claude-code.js';
3
3
  import { parse as parseCline } from './cline.js';
4
4
  import { parse as parseCodex } from './codex.js';
5
5
  import { parse as parseCopilotCli } from './copilot-cli.js';
6
+ import { parse as parseCraftAgent } from './craft-agent.js';
6
7
  import { parse as parseCursor } from './cursor.js';
7
8
  import { parse as parseDimAgent } from './dimagent.js';
8
9
  import { parse as parseRooCode } from './roo-code.js';
@@ -10,9 +11,11 @@ import { parse as parseGeminiCli } from './gemini-cli.js';
10
11
  import { parse as parseGrok } from './grok.js';
11
12
  import { parse as parseOpencode } from './opencode.js';
12
13
  import { parse as parseOpenclaw } from './openclaw.js';
14
+ import { parse as parseOmp } from './omp.js';
13
15
  import { parse as parseQwenCode } from './qwen-code.js';
14
16
  import { parse as parseKimiCode } from './kimi-code.js';
15
17
  import { parse as parseAmp } from './amp.js';
18
+ import { parse as parseAlma } from './alma.js';
16
19
  import { parse as parseDroid } from './droid.js';
17
20
  import { parse as parseAntigravity } from './antigravity.js';
18
21
  import { parse as parseHermes } from './hermes.js';
@@ -21,21 +24,25 @@ import { parse as parseMimocode } from './mimocode.js';
21
24
  import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
22
25
  import { parse as parseZcode } from './zcode.js';
23
26
  import { parse as parseTraeCli } from './trae-cli.js';
27
+ import { parse as parseWorkbuddy } from './workbuddy.js';
24
28
 
25
29
  export const parsers = {
26
30
  'claude-code': parseClaudeCode,
27
31
  'codex': parseCodex,
28
32
  'grok': parseGrok,
29
33
  'copilot-cli': parseCopilotCli,
34
+ 'craft-agent': parseCraftAgent,
30
35
  'cursor': parseCursor,
31
36
  'dimagent': parseDimAgent,
32
37
  'gemini-cli': parseGeminiCli,
33
38
  'opencode': parseOpencode,
34
39
  'openclaw': parseOpenclaw,
40
+ 'omp': parseOmp,
35
41
  'pi-coding-agent': parsePiCodingAgent,
36
42
  'qwen-code': parseQwenCode,
37
43
  'kimi-code': parseKimiCode,
38
44
  'amp': parseAmp,
45
+ 'alma': parseAlma,
39
46
  'droid': parseDroid,
40
47
  'antigravity': parseAntigravity,
41
48
  'trae-cli': parseTraeCli,
@@ -44,6 +51,7 @@ export const parsers = {
44
51
  'mimocode': parseMimocode,
45
52
  'cline': parseCline,
46
53
  'roo-code': parseRooCode,
54
+ 'workbuddy': parseWorkbuddy,
47
55
  'zcode': parseZcode,
48
56
  };
49
57
 
@@ -0,0 +1,10 @@
1
+ import { getOmpSessionDirs } from '../pi-roots.js';
2
+ import { parsePiSessionJsonl } from './pi-session-jsonl.js';
3
+
4
+ /** Parse Oh My Pi's Pi-compatible JSONL sessions, including profiles/XDG. */
5
+ export async function parse() {
6
+ return parsePiSessionJsonl({
7
+ source: 'omp',
8
+ sessionsDirs: getOmpSessionDirs(),
9
+ });
10
+ }
@@ -7,6 +7,8 @@ import { aggregateToBuckets, extractSessions } from './index.js';
7
7
  // Profile deployments use ~/.openclaw-<profile>/agents/...
8
8
  // Legacy paths: ~/.clawdbot, ~/.moltbot, ~/.moldbot
9
9
  function getPossibleRoots() {
10
+ const override = process.env.VIBE_USAGE_OPENCLAW_DIRS?.trim();
11
+ if (override) return override.split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
10
12
  const home = homedir();
11
13
  const roots = [
12
14
  join(home, '.clawdbot'),
@@ -29,7 +31,8 @@ function getPossibleRoots() {
29
31
  /** Normalize usage fields — OpenClaw supports multiple naming conventions */
30
32
  function getTokens(usage, ...keys) {
31
33
  for (const key of keys) {
32
- if (usage[key] != null && usage[key] > 0) return usage[key];
34
+ const value = Number(usage[key]);
35
+ if (Number.isFinite(value) && value > 0) return value;
33
36
  }
34
37
  return 0;
35
38
  }
@@ -98,12 +101,31 @@ export async function parse() {
98
101
  const usage = msg.usage;
99
102
  if (!usage) continue;
100
103
 
104
+ const inputTokens = getTokens(
105
+ usage,
106
+ 'input',
107
+ 'inputTokens',
108
+ 'input_tokens',
109
+ 'promptTokens',
110
+ 'prompt_tokens',
111
+ );
112
+ const cacheWriteTokens = getTokens(
113
+ usage,
114
+ 'cacheCreation',
115
+ 'cacheCreationInputTokens',
116
+ 'cacheWrite',
117
+ 'cache_creation',
118
+ 'cache_write',
119
+ 'cache_creation_input_tokens',
120
+ 'cache_write_input_tokens',
121
+ );
122
+
101
123
  entries.push({
102
124
  source: 'openclaw',
103
125
  model: msg.model || obj.model || 'unknown',
104
126
  project,
105
127
  timestamp: ts,
106
- inputTokens: getTokens(usage, 'input', 'inputTokens', 'input_tokens', 'promptTokens', 'prompt_tokens'),
128
+ inputTokens: inputTokens + cacheWriteTokens,
107
129
  outputTokens: getTokens(usage, 'output', 'outputTokens', 'output_tokens', 'completionTokens', 'completion_tokens'),
108
130
  cachedInputTokens: getTokens(usage, 'cacheRead', 'cache_read', 'cache_read_input_tokens'),
109
131
  reasoningOutputTokens: 0,