@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 +11 -7
- package/package.json +1 -1
- package/src/claude-roots.js +86 -4
- package/src/cline-roots.js +40 -0
- package/src/craft-roots.js +15 -0
- package/src/parsers/alma.js +91 -0
- package/src/parsers/amp.js +16 -4
- package/src/parsers/claude-code.js +3 -1
- package/src/parsers/cline.js +26 -40
- package/src/parsers/craft-agent.js +21 -0
- package/src/parsers/index.js +8 -0
- package/src/parsers/omp.js +10 -0
- package/src/parsers/openclaw.js +24 -2
- package/src/parsers/pi-coding-agent.js +7 -141
- package/src/parsers/pi-session-jsonl.js +148 -0
- package/src/parsers/workbuddy.js +287 -0
- package/src/pi-roots.js +89 -0
- package/src/sync.js +13 -2
- package/src/tools.js +48 -3
- package/src/workbuddy-roots.js +19 -0
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
|
-
|
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
package/src/claude-roots.js
CHANGED
|
@@ -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
|
-
*
|
|
27
|
-
*
|
|
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
|
+
}
|
package/src/parsers/amp.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/parsers/cline.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/parsers/index.js
CHANGED
|
@@ -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
|
+
}
|
package/src/parsers/openclaw.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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,
|