@vibe-cafe/vibe-usage 0.10.21 → 0.10.22

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
@@ -72,12 +72,12 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
72
72
  | Amp | `~/.local/share/amp/threads/`; cache-creation tokens are included in input usage |
73
73
  | Droid | `~/.factory/sessions/` |
74
74
  | DeepSeek Harness | `$DSH_HOME/sessions/` (default `~/.dsh`, fixture/relocation override: `VIBE_USAGE_DSH_SESSIONS`). Reads multi-frame Zstandard `session.jsonl.zstd` logs (built-in `node:zlib` zstd on Node ≥ 22.15, `zstd` CLI fallback) and plain `session.jsonl` logs. Usage comes from `assistant/message`: cache writes join uncached input, cache reads remain separate, and reasoning is split out of inclusive output. Fork/subagent history is de-duplicated from the immutable header: `parentSession` identifies the source and `seedLength` gives the exact leading event boundary. Inherited messages are skipped only when matching source seqs remain in the parent file; missing parents fail open. `session/end-seed` positions are not used because resumes can append the marker after real history. |
75
- | Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
75
+ | Hermes | `$HERMES_HOME/state.db` (default `~/.hermes/state.db`) + `profiles/<name>/state.db` (SQLite, multi-profile). Cache writes join input; reasoning is separated from inclusive output. Usage is currently a cumulative session total attributed to session start: a session spanning several days does **not** yet provide an accurate daily breakdown. |
76
76
  | 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` |
77
77
  | 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 |
78
78
  | Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
79
79
  | 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). Token usage is summed per unique LLM call (`model.stream.eino`, plus `model.generate` failovers); nested duplicate spans that share a session `traceID` are not max-merged. `traces.jsonl` / `events.jsonl` are streamed line-by-line so a multi-hundred-MB events file cannot hit Node's string-length limit. |
80
- | Antigravity | App 2.0 `~/.gemini/antigravity/conversations/*.db` and `agy` CLI `~/.gemini/antigravity-cli/conversations/*.db`, plus the same paths below explicitly added alternate Homes, are parsed offline (tokens, real model display name when present, project, sessions). Gemini 3.7 CLI blobs omit `chatStartMetadata.createdAt` and `modelDisplayName`; usage still comes from `gen_metadata`, timestamps fall back to `steps.metadata` at the same idx, and the model name falls back to `responseModel`. Legacy App `.pb` history falls back to Connect RPC only for the primary App store while the language server is running |
80
+ | Antigravity | Scans App 2.0 `~/.gemini/antigravity/conversations/`, `agy` CLI `~/.gemini/antigravity-cli/conversations/`, and standalone IDE `~/.gemini/antigravity-ide/conversations/`. `.db` stores, including the same paths below explicitly added alternate Homes, are parsed offline (tokens, model, project, sessions). When Gemini blobs omit `chatStartMetadata.createdAt` or `modelDisplayName`, timestamps fall back to `steps.metadata` and model names to `responseModel`. `.pb` history in the default stores requires the corresponding App/IDE language server to be running; when several servers are open, the parser tries the others for unreadable conversations. Unavailable legacy history produces a warning and preserves prior sync state. |
81
81
  | 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. |
82
82
  | ZCode | `~/.zcode/cli/db/db.sqlite` (SQLite; reads the `message` table for per-message tokens, model, and project `cwd`/`root`, joined to `session.directory`) |
83
83
 
@@ -93,6 +93,24 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
93
93
  - 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
94
94
  - For continuous syncing, use `npx @vibe-cafe/vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
95
95
 
96
+ ## Cursor 网络排查
97
+
98
+ Cursor 用量需要从 `cursor.com` 下载 CSV。`Cursor usage export skipped (network: …)` 表示这次下载失败;CLI 会保留 Cursor 的历史同步状态,下次同步重新尝试。
99
+
100
+ - `ENOTFOUND` / `EAI_AGAIN`:检查 DNS 和终端网络。
101
+ - `UND_ERR_CONNECT_TIMEOUT` / `ETIMEDOUT` / `ECONNRESET`:检查到 Cursor 的连接及终端代理。浏览器或 Cursor 应用能联网,不代表 Node.js 使用了相同代理。
102
+ - `CERT_*` / `UNABLE_TO_VERIFY_LEAF_SIGNATURE` 等:检查系统时间和代理或公司网络的 CA 证书配置;自定义 CA 可通过 `NODE_EXTRA_CA_CERTS` 指定。
103
+ - `timeout after …ms`:导出或下载超时,稍后重试;网络较慢时可设置 `VIBE_USAGE_CURSOR_FETCH_TIMEOUT_MS=60000`。
104
+ - `Cursor session rejected`:在 Cursor 的 Account 设置中重新登录,再同步。
105
+
106
+ 需要 HTTP/HTTPS 代理时,Node.js **22.21+ 或 24.5+** 可用 `NODE_USE_ENV_PROXY=1` 启用环境变量代理([Node.js 官方说明](https://nodejs.org/en/learn/http/enterprise-network-configuration))。以下为 macOS/Linux 终端示例,代理地址须替换为实际地址:
107
+
108
+ ```bash
109
+ HTTPS_PROXY=http://127.0.0.1:7890 NODE_USE_ENV_PROXY=1 npx @vibe-cafe/vibe-usage sync
110
+ ```
111
+
112
+ 这些环境变量只影响当前命令;已经运行的桌面应用和后台服务有各自的进程环境。持续失败时,请提供完整的 Cursor 报错、操作系统、`node -v` 和 CLI 版本,以便区分具体原因。
113
+
96
114
  ## Trust Model
97
115
 
98
116
  vibe-usage parses **local tool logs and local application state** on a machine the user fully controls. The reported data is self-reported telemetry — local logs, parsers, and upload requests can all be modified by the user.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.21",
3
+ "version": "0.10.22",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -86,6 +86,7 @@ export function antigravityConversationDirs(value) {
86
86
  return [
87
87
  join(root, '.gemini', 'antigravity', 'conversations'),
88
88
  join(root, '.gemini', 'antigravity-cli', 'conversations'),
89
+ join(root, '.gemini', 'antigravity-ide', 'conversations'),
89
90
  ];
90
91
  }
91
92
 
@@ -20,9 +20,8 @@ import { listDbCascades, readDbUsageRecords, readDbWorkspaceUri, readDbSessionEv
20
20
  */
21
21
 
22
22
  const SOURCE = 'antigravity';
23
- const CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity', 'conversations');
24
- // `agy` CLI stores conversations in a separate, App-independent directory.
25
- const CLI_CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity-cli', 'conversations');
23
+ // App, CLI, and the standalone IDE each have their own conversation store.
24
+ const CONVERSATIONS_DIRS = antigravityConversationDirs(homedir());
26
25
 
27
26
  // User sources → role 'user'; Model source → role 'assistant'; System sources → skip
28
27
  const USER_SOURCES = new Set([
@@ -33,24 +32,25 @@ const ASSISTANT_SOURCES = new Set([
33
32
  'CORTEX_STEP_SOURCE_MODEL',
34
33
  ]);
35
34
 
36
- // ── Process discovery (single instance) ──────────────────────────────
35
+ // ── Process discovery ───────────────────────────────────────────────
37
36
 
38
37
  const IS_WIN = process.platform === 'win32';
39
38
 
40
39
  /**
41
- * Find ONE running language server process with a CSRF token.
42
- * Returns { pid, csrfToken } or null.
40
+ * Find running language servers with CSRF tokens. The App and standalone IDE
41
+ * can run together, and one server cannot read the other's legacy cascades.
43
42
  */
44
- function findLanguageServer() {
43
+ function findLanguageServers() {
45
44
  try {
46
- return IS_WIN ? findLanguageServerWin() : findLanguageServerUnix();
45
+ return IS_WIN ? findLanguageServersWin() : findLanguageServersUnix();
47
46
  } catch {
48
- return null;
47
+ return [];
49
48
  }
50
49
  }
51
50
 
52
- function findLanguageServerUnix() {
51
+ function findLanguageServersUnix() {
53
52
  const out = execSync("ps aux | grep -i 'antigravity.*language_server'", { encoding: 'utf-8', timeout: 5000 });
53
+ const servers = [];
54
54
  for (const line of out.split('\n')) {
55
55
  if (!line.trim()) continue;
56
56
  if (line.includes('grep')) continue;
@@ -59,19 +59,19 @@ function findLanguageServerUnix() {
59
59
  const pid = parts[1];
60
60
  const csrfMatch = line.match(/--csrf_token\s+([0-9a-f-]+)/);
61
61
  const csrfToken = csrfMatch ? csrfMatch[1] : '';
62
- if (csrfToken) return { pid, csrfToken };
62
+ if (csrfToken) servers.push({ pid, csrfToken });
63
63
  }
64
- return null;
64
+ return servers;
65
65
  }
66
66
 
67
- function findLanguageServerWin() {
67
+ function findLanguageServersWin() {
68
68
  // Prefer PowerShell/CIM: wmic is disabled by default on Windows 11 23H2+
69
69
  // and removed entirely from 25H2 onward. Fall back to wmic for old/stripped
70
70
  // environments without PowerShell. Each probe is independently time-boxed and
71
71
  // failures are swallowed, so a missing/hung tool never blocks the next one or
72
72
  // the parsers that run after antigravity.
73
73
  const out = queryProcessesWinPowerShell() ?? queryProcessesWinWmic();
74
- if (!out) return null;
74
+ if (!out) return [];
75
75
  return parseWinProcessList(out);
76
76
  }
77
77
 
@@ -117,20 +117,20 @@ function queryProcessesWinWmic() {
117
117
 
118
118
  /**
119
119
  * Parse "ProcessId=..." / "CommandLine=..." records (from either PowerShell or
120
- * wmic /format:list) and return the first language_server that carries a
121
- * --csrf_token, or null. PowerShell emits an explicit "---" separator per
120
+ * wmic /format:list) and return each language_server that carries a
121
+ * --csrf_token. PowerShell emits an explicit "---" separator per
122
122
  * process; wmic does not and may emit the two fields in either order, so a
123
123
  * record also ends whenever a field we've already captured reappears.
124
124
  */
125
- function parseWinProcessList(out) {
125
+ export function parseWinProcessList(out) {
126
+ const servers = [];
126
127
  let pid = '';
127
128
  let cmdLine = '';
128
129
  const finish = () => {
129
130
  if (pid && cmdLine && !/WMIC\.exe|powershell\.exe|pwsh\.exe/i.test(cmdLine)) {
130
131
  const csrfMatch = cmdLine.match(/--csrf_token\s+([0-9a-f-]+)/);
131
- if (csrfMatch) return { pid, csrfToken: csrfMatch[1] };
132
+ if (csrfMatch) servers.push({ pid, csrfToken: csrfMatch[1] });
132
133
  }
133
- return null;
134
134
  };
135
135
  const reset = () => { pid = ''; cmdLine = ''; };
136
136
  for (const line of out.split('\n')) {
@@ -140,14 +140,14 @@ function parseWinProcessList(out) {
140
140
  // Record boundary: explicit "---", or a field that would overwrite one we
141
141
  // already hold (next process began without a separator, e.g. wmic output).
142
142
  if (trimmed === '---' || (isPid && pid) || (isCmd && cmdLine)) {
143
- const found = finish();
144
- if (found) return found;
143
+ finish();
145
144
  reset();
146
145
  }
147
146
  if (isPid) pid = trimmed.slice('ProcessId='.length);
148
147
  else if (isCmd) cmdLine = trimmed.slice('CommandLine='.length);
149
148
  }
150
- return finish();
149
+ finish();
150
+ return servers;
151
151
  }
152
152
 
153
153
  function findListeningPorts(pid) {
@@ -296,7 +296,7 @@ function projectFromUri(uri) {
296
296
  * List cascade IDs backed by a legacy `.pb` file (App history). `.db` cascades
297
297
  * are handled separately via offline parsing.
298
298
  */
299
- function listPbCascades(conversationsDir = CONVERSATIONS_DIR) {
299
+ function listPbCascades(conversationsDir) {
300
300
  try {
301
301
  const out = [];
302
302
  for (const f of readdirSync(conversationsDir)) {
@@ -320,6 +320,7 @@ function modelFromRecord(rec) {
320
320
  export async function parse({ extraRoots = [] } = {}) {
321
321
  const entries = [];
322
322
  const sessionEvents = [];
323
+ const warnings = [];
323
324
  const seenResponseIds = new Set();
324
325
 
325
326
  const extraDirs = [];
@@ -356,7 +357,7 @@ export async function parse({ extraRoots = [] } = {}) {
356
357
  const fixtureDirs = process.env.VIBE_USAGE_ANTIGRAVITY_DIRS?.trim();
357
358
  const defaultDirs = fixtureDirs
358
359
  ? fixtureDirs.split(delimiter).filter(Boolean)
359
- : [CONVERSATIONS_DIR, CLI_CONVERSATIONS_DIR];
360
+ : CONVERSATIONS_DIRS;
360
361
  const strictDirs = new Set(extraDirs);
361
362
  const conversationDirs = [...new Set([...defaultDirs, ...extraDirs])];
362
363
  const candidates = [];
@@ -444,13 +445,14 @@ export async function parse({ extraRoots = [] } = {}) {
444
445
  }
445
446
 
446
447
  // ── Path 2: RPC fallback, only for legacy .pb cascades not already parsed ──
447
- const pbDir = defaultDirs[0] || CONVERSATIONS_DIR;
448
- const pbCascades = listPbCascades(pbDir).filter((id) => !dbHandled.has(id));
448
+ const pbCascades = [...new Set(defaultDirs.flatMap(listPbCascades))]
449
+ .filter((id) => !dbHandled.has(id));
449
450
  if (pbCascades.length > 0) {
450
- const server = findLanguageServer();
451
- const ports = server ? findListeningPorts(server.pid) : [];
452
- const baseUrl = ports.length > 0 ? await probeHttpPort(ports, server.csrfToken) : null;
453
- if (baseUrl) {
451
+ const pending = new Set(pbCascades);
452
+ for (const server of findLanguageServers()) {
453
+ const ports = findListeningPorts(server.pid);
454
+ const baseUrl = ports.length > 0 ? await probeHttpPort(ports, server.csrfToken) : null;
455
+ if (!baseUrl) continue;
454
456
  const rpc = (method, body) =>
455
457
  rpcPost(
456
458
  baseUrl,
@@ -459,7 +461,7 @@ export async function parse({ extraRoots = [] } = {}) {
459
461
  server.csrfToken,
460
462
  );
461
463
 
462
- for (const cascadeId of pbCascades) {
464
+ for (const cascadeId of pending) {
463
465
  let resp;
464
466
  try {
465
467
  resp = await rpc('GetCascadeTrajectory', { cascadeId });
@@ -468,6 +470,7 @@ export async function parse({ extraRoots = [] } = {}) {
468
470
  }
469
471
  const trajectory = resp?.trajectory;
470
472
  if (!trajectory) continue;
473
+ pending.delete(cascadeId);
471
474
 
472
475
  const steps = trajectory.steps || [];
473
476
  const metadataList = trajectory.generatorMetadata || [];
@@ -519,11 +522,16 @@ export async function parse({ extraRoots = [] } = {}) {
519
522
  sessionEvents.push({ sessionId: cascadeId, source: SOURCE, project, timestamp: ts, role });
520
523
  }
521
524
  }
525
+ if (pending.size === 0) break;
526
+ }
527
+ if (pending.size > 0) {
528
+ warnings.push(`antigravity: ${pending.size} 个旧格式会话未能读取,请打开对应的 Antigravity IDE/App 后重新同步;已保留上次同步状态。`);
522
529
  }
523
530
  }
524
531
 
525
532
  return {
526
533
  buckets: aggregateToBuckets(entries),
527
534
  sessions: extractSessions(sessionEvents),
535
+ ...(warnings.length > 0 ? { skipped: true, warnings } : {}),
528
536
  };
529
537
  }
@@ -82,6 +82,41 @@ export function resolveCursorFetchTimeout(value) {
82
82
 
83
83
  const FETCH_TIMEOUT_MS = resolveCursorFetchTimeout(process.env.VIBE_USAGE_CURSOR_FETCH_TIMEOUT_MS);
84
84
 
85
+ function usageNetworkError(error, signal) {
86
+ // Node fetch usually exposes only "fetch failed" at the top level. DNS,
87
+ // TLS and connection codes live in cause (or AggregateError.errors when
88
+ // both IPv4 and IPv6 fail). Report codes, not raw messages that may contain
89
+ // URLs with proxy credentials or authentication headers.
90
+ const codes = new Set();
91
+ const seen = new Set();
92
+ function visit(err) {
93
+ if (!err || typeof err !== 'object' || seen.has(err)) return;
94
+ seen.add(err);
95
+ if (typeof err.code === 'string' && /^[A-Z][A-Z0-9_]{1,63}$/.test(err.code)) {
96
+ codes.add(err.code);
97
+ }
98
+ visit(err.cause);
99
+ if (Array.isArray(err.errors)) err.errors.forEach(visit);
100
+ }
101
+ visit(error);
102
+
103
+ const timedOut = error?.name === 'TimeoutError' || signal.reason?.name === 'TimeoutError';
104
+ const reason = timedOut
105
+ ? `timeout after ${FETCH_TIMEOUT_MS}ms`
106
+ : `network: ${[...codes].join(', ') || 'fetch failed'}`;
107
+ let hint = '请检查终端网络和代理配置;浏览器能访问 Cursor 不代表 Node.js 使用了相同代理。';
108
+ if (timedOut) {
109
+ hint = 'Cursor 用量导出超时,请稍后重试;可通过 VIBE_USAGE_CURSOR_FETCH_TIMEOUT_MS 增大导出等待时间。';
110
+ } else if (codes.has('ENOTFOUND') || codes.has('EAI_AGAIN')) {
111
+ hint = '域名解析失败,请检查 DNS 和终端代理配置。';
112
+ } else if ([...codes].some(code => /CERT|SELF_SIGNED|UNABLE_TO_VERIFY|ERR_TLS_/.test(code))) {
113
+ hint = 'TLS 证书校验失败,请检查系统时间及代理或公司网络的 CA 证书配置。';
114
+ }
115
+ const err = new Error(`Cursor usage export skipped (${reason}). ${hint}`);
116
+ err.skip = true;
117
+ return err;
118
+ }
119
+
85
120
  async function fetchUsageCsv(token) {
86
121
  const url = `${(process.env.CURSOR_WEB_BASE_URL?.trim() || 'https://cursor.com').replace(/\/+$/, '')}/api/dashboard/export-usage-events-csv?strategy=tokens`;
87
122
  const sub = decodeJwtSub(token);
@@ -110,20 +145,19 @@ async function fetchUsageCsv(token) {
110
145
  const failures = [];
111
146
  for (const headers of attempts) {
112
147
  let resp;
148
+ const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
113
149
  try {
114
150
  resp = await fetch(url, {
115
151
  headers: { ...baseHeaders, ...headers },
116
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
152
+ signal,
117
153
  });
154
+ // The export may time out or disconnect after headers have arrived.
155
+ // Keep body consumption under the same deadline and soft-skip handling.
156
+ if (resp.ok) return await resp.text();
118
157
  } catch (e) {
119
- // Hard-fail on network/timeout: stop trying further headers (won't fix
120
- // a downed host) and signal a soft skip to the caller.
121
- const reason = e.name === 'TimeoutError' ? 'timeout' : `network: ${e.message}`;
122
- const err = new Error(`Cursor usage export skipped (${reason})`);
123
- err.skip = true;
124
- throw err;
158
+ // Changing credentials cannot repair a network failure.
159
+ throw usageNetworkError(e, signal);
125
160
  }
126
- if (resp.ok) return await resp.text();
127
161
  failures.push(`${resp.status} ${resp.statusText}`);
128
162
  // Only auth rejections are worth retrying with different credentials.
129
163
  // 429/5xx are transient server-side states — soft-skip like network errors
@@ -198,7 +232,7 @@ export async function parse() {
198
232
  try {
199
233
  csv = await fetchUsageCsv(token);
200
234
  } catch (err) {
201
- // Network/timeout → silent skip (avoid noisy daemon logs every 5 min).
235
+ // Network/timeout → skip with diagnostics (quiet sync suppresses these).
202
236
  // Auth failure → bubble up so user sees they need to re-login in Cursor.
203
237
  // Tell sync.js this was not a successful empty snapshot so it preserves
204
238
  // Cursor's incremental state instead of pruning it as dead history.
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { aggregateToBuckets, extractSessions } from './aggregate.js';
5
+ import { toCount } from './fs-utils.js';
5
6
  import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
6
7
 
7
8
  const HERMES_HOME = process.env.HERMES_HOME || join(homedir(), '.hermes');
@@ -26,6 +27,8 @@ export async function parse() {
26
27
  for (const { path: dbPath, profile } of dbs) {
27
28
  let sessionRows;
28
29
  try {
30
+ const columns = new Set(queryDb(dbPath, 'PRAGMA table_info(sessions)').map(row => row.name));
31
+ const cacheWriteColumn = columns.has('cache_write_tokens') ? 'cache_write_tokens' : '0';
29
32
  sessionRows = queryDb(dbPath, `SELECT
30
33
  id,
31
34
  model,
@@ -33,9 +36,11 @@ export async function parse() {
33
36
  input_tokens as inputTokens,
34
37
  output_tokens as outputTokens,
35
38
  cache_read_tokens as cacheReadTokens,
39
+ ${cacheWriteColumn} as cacheWriteTokens,
36
40
  reasoning_tokens as reasoningTokens
37
41
  FROM sessions
38
- WHERE input_tokens > 0 OR output_tokens > 0`);
42
+ WHERE input_tokens > 0 OR output_tokens > 0
43
+ OR cache_read_tokens > 0 OR ${cacheWriteColumn} > 0 OR reasoning_tokens > 0`);
39
44
  } catch (err) {
40
45
  if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('Hermes');
41
46
  throw err;
@@ -47,15 +52,19 @@ export async function parse() {
47
52
  if (isNaN(timestamp.getTime())) continue;
48
53
 
49
54
  // Hermes stores input_tokens exclusive of cache (Anthropic-style semantics)
55
+ // and output_tokens inclusive of reasoning (CanonicalUsage.total_tokens
56
+ // adds prompt + output only). Split reasoning instead of counting it twice.
57
+ const output = toCount(row.outputTokens);
58
+ const reasoning = Math.min(output, toCount(row.reasoningTokens));
50
59
  entries.push({
51
60
  source: 'hermes',
52
61
  model: row.model || 'unknown',
53
62
  project: profile,
54
63
  timestamp,
55
- inputTokens: row.inputTokens || 0,
56
- outputTokens: row.outputTokens || 0,
57
- cachedInputTokens: row.cacheReadTokens || 0,
58
- reasoningOutputTokens: row.reasoningTokens || 0,
64
+ inputTokens: toCount(row.inputTokens) + toCount(row.cacheWriteTokens),
65
+ outputTokens: output - reasoning,
66
+ cachedInputTokens: toCount(row.cacheReadTokens),
67
+ reasoningOutputTokens: reasoning,
59
68
  });
60
69
  }
61
70
 
package/src/tools.js CHANGED
@@ -174,8 +174,7 @@ export function getMimocodeDbPath(env = process.env) {
174
174
 
175
175
  export function findAntigravityDataDirs(extraRoots = []) {
176
176
  return [...new Set([
177
- join(homedir(), '.gemini', 'antigravity'),
178
- join(homedir(), '.gemini', 'antigravity-cli'),
177
+ ...antigravityConversationDirs(homedir()).map(dirname),
179
178
  ...extraRoots.flatMap(root => antigravityConversationDirs(root).map(dirname)),
180
179
  ])].filter(existsSync);
181
180
  }