@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 +20 -2
- package/package.json +1 -1
- package/src/extra-roots.js +1 -0
- package/src/parsers/antigravity.js +39 -31
- package/src/parsers/cursor.js +43 -9
- package/src/parsers/hermes.js +14 -5
- package/src/tools.js +1 -2
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` +
|
|
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
|
|
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
package/src/extra-roots.js
CHANGED
|
@@ -20,9 +20,8 @@ import { listDbCascades, readDbUsageRecords, readDbWorkspaceUri, readDbSessionEv
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
const SOURCE = 'antigravity';
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
35
|
+
// ── Process discovery ───────────────────────────────────────────────
|
|
37
36
|
|
|
38
37
|
const IS_WIN = process.platform === 'win32';
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
|
-
* Find
|
|
42
|
-
*
|
|
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
|
|
43
|
+
function findLanguageServers() {
|
|
45
44
|
try {
|
|
46
|
-
return IS_WIN ?
|
|
45
|
+
return IS_WIN ? findLanguageServersWin() : findLanguageServersUnix();
|
|
47
46
|
} catch {
|
|
48
|
-
return
|
|
47
|
+
return [];
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
function
|
|
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)
|
|
62
|
+
if (csrfToken) servers.push({ pid, csrfToken });
|
|
63
63
|
}
|
|
64
|
-
return
|
|
64
|
+
return servers;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function
|
|
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
|
|
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
|
|
121
|
-
* --csrf_token
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
|
448
|
-
|
|
448
|
+
const pbCascades = [...new Set(defaultDirs.flatMap(listPbCascades))]
|
|
449
|
+
.filter((id) => !dbHandled.has(id));
|
|
449
450
|
if (pbCascades.length > 0) {
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
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
|
}
|
package/src/parsers/cursor.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
120
|
-
|
|
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 →
|
|
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.
|
package/src/parsers/hermes.js
CHANGED
|
@@ -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
|
|
56
|
-
outputTokens:
|
|
57
|
-
cachedInputTokens: row.cacheReadTokens
|
|
58
|
-
reasoningOutputTokens:
|
|
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
|
-
|
|
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
|
}
|