@vibe-cafe/vibe-usage 0.9.10 → 0.9.11
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 +2 -1
- package/package.json +1 -1
- package/src/parsers/antigravity-db.js +307 -0
- package/src/parsers/antigravity.js +144 -117
- package/src/parsers/index.js +2 -0
- package/src/parsers/trae-cli.js +155 -0
- package/src/tools.js +30 -0
package/README.md
CHANGED
|
@@ -66,7 +66,8 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
66
66
|
| 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` |
|
|
67
67
|
| 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, ...) |
|
|
68
68
|
| Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
|
|
69
|
-
|
|
|
69
|
+
| 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) |
|
|
70
|
+
| 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 |
|
|
70
71
|
| ZCode | `~/.zcode/cli/db/db.sqlite` (SQLite; reads the `message` table for per-message tokens, model, and project `cwd`/`root`, joined to `session.directory`) |
|
|
71
72
|
|
|
72
73
|
## How It Works
|
package/package.json
CHANGED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { queryDbJson } from './sqlite.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Offline reader for Antigravity SQLite conversation stores.
|
|
8
|
+
*
|
|
9
|
+
* Antigravity 2.0 (standalone app) and the `agy` CLI both persist each cascade
|
|
10
|
+
* as a per-conversation SQLite `.db` file whose `gen_metadata` table holds one
|
|
11
|
+
* protobuf-encoded GeneratorMetadata per row. Unlike the legacy `.pb` files
|
|
12
|
+
* (which are encrypted/opaque and only decodable by a running language server),
|
|
13
|
+
* these blobs are plain protobuf, so we can extract token usage directly from
|
|
14
|
+
* disk — no running process, no RPC.
|
|
15
|
+
*
|
|
16
|
+
* The wire-format tag numbers below were cross-verified against the language
|
|
17
|
+
* server's GetCascadeTrajectory JSON for the same responseId:
|
|
18
|
+
*
|
|
19
|
+
* chatModel = field 1
|
|
20
|
+
* usage = field 4
|
|
21
|
+
* inputTokens = 4.2
|
|
22
|
+
* outputTokens = 4.3
|
|
23
|
+
* cacheReadTokens = 4.5
|
|
24
|
+
* thinkingOutputTokens = 4.9
|
|
25
|
+
* responseId = 4.11
|
|
26
|
+
* chatStartMetadata = field 9
|
|
27
|
+
* createdAt (Timestamp) = 9.4 → seconds = 9.4.1
|
|
28
|
+
* responseModel = field 19 ("gemini-3-flash-a" / "gemini-default")
|
|
29
|
+
* modelDisplayName = field 21 ("Gemini 3.5 Flash (High/Medium/Low)")
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// ── Minimal protobuf wire-format decoder (no dependency) ──────────────
|
|
33
|
+
|
|
34
|
+
/** Read a base-128 varint. Returns [value: number, newPos]. */
|
|
35
|
+
function readVarint(buf, pos) {
|
|
36
|
+
let result = 0n;
|
|
37
|
+
let shift = 0n;
|
|
38
|
+
while (pos < buf.length) {
|
|
39
|
+
const byte = buf[pos++];
|
|
40
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
41
|
+
if ((byte & 0x80) === 0) break;
|
|
42
|
+
shift += 7n;
|
|
43
|
+
}
|
|
44
|
+
return [Number(result), pos];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decode one protobuf message into Map<fieldNumber, Array<{wireType, value}>>.
|
|
49
|
+
* Length-delimited values are kept as raw Buffers (caller decides string vs
|
|
50
|
+
* sub-message). Unknown wire types abort parsing of the rest of the message.
|
|
51
|
+
*/
|
|
52
|
+
function decodeMessage(buf) {
|
|
53
|
+
const fields = new Map();
|
|
54
|
+
let pos = 0;
|
|
55
|
+
while (pos < buf.length) {
|
|
56
|
+
let tag;
|
|
57
|
+
[tag, pos] = readVarint(buf, pos);
|
|
58
|
+
const fieldNum = tag >> 3;
|
|
59
|
+
const wireType = tag & 0x7;
|
|
60
|
+
let value;
|
|
61
|
+
if (wireType === 0) {
|
|
62
|
+
[value, pos] = readVarint(buf, pos);
|
|
63
|
+
} else if (wireType === 2) {
|
|
64
|
+
let len;
|
|
65
|
+
[len, pos] = readVarint(buf, pos);
|
|
66
|
+
value = buf.subarray(pos, pos + len);
|
|
67
|
+
pos += len;
|
|
68
|
+
} else if (wireType === 5) {
|
|
69
|
+
value = buf.subarray(pos, pos + 4);
|
|
70
|
+
pos += 4;
|
|
71
|
+
} else if (wireType === 1) {
|
|
72
|
+
value = buf.subarray(pos, pos + 8);
|
|
73
|
+
pos += 8;
|
|
74
|
+
} else {
|
|
75
|
+
break; // group/unknown — stop
|
|
76
|
+
}
|
|
77
|
+
if (!fields.has(fieldNum)) fields.set(fieldNum, []);
|
|
78
|
+
fields.get(fieldNum).push({ wireType, value });
|
|
79
|
+
}
|
|
80
|
+
return fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function firstVarint(fields, num) {
|
|
84
|
+
const arr = fields.get(num);
|
|
85
|
+
const e = arr && arr.find((x) => x.wireType === 0);
|
|
86
|
+
return e ? e.value : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function firstBytes(fields, num) {
|
|
90
|
+
const arr = fields.get(num);
|
|
91
|
+
const e = arr && arr.find((x) => x.wireType === 2);
|
|
92
|
+
return e ? e.value : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function firstString(fields, num) {
|
|
96
|
+
const b = firstBytes(fields, num);
|
|
97
|
+
return b ? Buffer.from(b).toString('utf-8') : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function firstMessage(fields, num) {
|
|
101
|
+
const b = firstBytes(fields, num);
|
|
102
|
+
return b ? decodeMessage(b) : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── GeneratorMetadata parsing ─────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse one gen_metadata blob into a normalized usage record, or null if it
|
|
109
|
+
* carries no token usage (error/planning placeholders have none).
|
|
110
|
+
*
|
|
111
|
+
* @param {Buffer} buf raw protobuf bytes of a GeneratorMetadata row
|
|
112
|
+
* @returns {{inputTokens, outputTokens, cacheReadTokens, thinkingOutputTokens,
|
|
113
|
+
* responseId, timestamp: Date|null, displayName, responseModel}|null}
|
|
114
|
+
*/
|
|
115
|
+
export function parseGenMetadataBlob(buf) {
|
|
116
|
+
const chatModel = firstMessage(decodeMessage(buf), 1);
|
|
117
|
+
if (!chatModel) return null;
|
|
118
|
+
|
|
119
|
+
const usage = firstMessage(chatModel, 4);
|
|
120
|
+
if (!usage) return null;
|
|
121
|
+
|
|
122
|
+
const inputTokens = firstVarint(usage, 2) || 0;
|
|
123
|
+
const outputTokens = firstVarint(usage, 3) || 0;
|
|
124
|
+
const cacheReadTokens = firstVarint(usage, 5) || 0;
|
|
125
|
+
const thinkingOutputTokens = firstVarint(usage, 9) || 0;
|
|
126
|
+
const responseId = firstString(usage, 11) || '';
|
|
127
|
+
|
|
128
|
+
// Skip rows with no real usage (errors, planning-only steps).
|
|
129
|
+
if (!inputTokens && !outputTokens && !cacheReadTokens && !thinkingOutputTokens) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const chatStartMetadata = firstMessage(chatModel, 9);
|
|
134
|
+
const createdAt = chatStartMetadata ? firstMessage(chatStartMetadata, 4) : undefined;
|
|
135
|
+
const seconds = createdAt ? firstVarint(createdAt, 1) : undefined;
|
|
136
|
+
const timestamp = seconds ? new Date(seconds * 1000) : null;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
inputTokens,
|
|
140
|
+
outputTokens,
|
|
141
|
+
cacheReadTokens,
|
|
142
|
+
thinkingOutputTokens,
|
|
143
|
+
responseId,
|
|
144
|
+
timestamp,
|
|
145
|
+
displayName: firstString(chatModel, 21) || '',
|
|
146
|
+
responseModel: firstString(chatModel, 19) || '',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── SQLite store reading ──────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function isLockError(err) {
|
|
153
|
+
return err && typeof err.message === 'string' && /database is locked/i.test(err.message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isSqliteUnavailableError(err) {
|
|
157
|
+
return err && typeof err.message === 'string' && /ENOENT|sqlite3.*not found/i.test(err.message);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function queryCascadeDb(conversationsDir, cascadeId, sql) {
|
|
161
|
+
const dbPath = join(conversationsDir, `${cascadeId}.db`);
|
|
162
|
+
try {
|
|
163
|
+
return queryDbJson(dbPath, sql);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (isSqliteUnavailableError(err)) {
|
|
166
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Antigravity data.');
|
|
167
|
+
}
|
|
168
|
+
if (!isLockError(err)) throw err;
|
|
169
|
+
|
|
170
|
+
// The App can hold the live DB open. Query a WAL-consistent snapshot so
|
|
171
|
+
// one active cascade does not make the whole Antigravity parser go empty.
|
|
172
|
+
const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-antigravity-'));
|
|
173
|
+
const snapshotPath = join(snapshotDir, `${cascadeId}.db`);
|
|
174
|
+
try {
|
|
175
|
+
copyFileSync(dbPath, snapshotPath);
|
|
176
|
+
for (const suffix of ['-shm', '-wal']) {
|
|
177
|
+
const companion = `${dbPath}${suffix}`;
|
|
178
|
+
if (existsSync(companion)) copyFileSync(companion, `${snapshotPath}${suffix}`);
|
|
179
|
+
}
|
|
180
|
+
return queryDbJson(snapshotPath, sql);
|
|
181
|
+
} finally {
|
|
182
|
+
rmSync(snapshotDir, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** List cascade IDs backed by a `.db` file in a conversations directory. */
|
|
188
|
+
export function listDbCascades(conversationsDir) {
|
|
189
|
+
try {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const f of readdirSync(conversationsDir)) {
|
|
192
|
+
if (f.endsWith('.db') && f !== 'db.sqlite') out.push(f.slice(0, -3));
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read all gen_metadata blobs from one cascade `.db` and return parsed usage
|
|
202
|
+
* records. blob is fetched as hex text so it round-trips through both the
|
|
203
|
+
* node:sqlite and sqlite3-CLI backends uniformly.
|
|
204
|
+
*/
|
|
205
|
+
export function readDbUsageRecords(conversationsDir, cascadeId) {
|
|
206
|
+
let rows;
|
|
207
|
+
try {
|
|
208
|
+
rows = queryCascadeDb(conversationsDir, cascadeId, 'SELECT hex(data) AS h FROM gen_metadata ORDER BY idx');
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (isSqliteUnavailableError(err)) throw err;
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
const records = [];
|
|
214
|
+
for (const row of rows) {
|
|
215
|
+
if (!row.h) continue;
|
|
216
|
+
let rec;
|
|
217
|
+
try {
|
|
218
|
+
rec = parseGenMetadataBlob(Buffer.from(row.h, 'hex'));
|
|
219
|
+
} catch {
|
|
220
|
+
continue; // one malformed blob must not kill the rest
|
|
221
|
+
}
|
|
222
|
+
if (rec) records.push(rec);
|
|
223
|
+
}
|
|
224
|
+
return records;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Read the workspace URI for a cascade from trajectory_metadata_blob.
|
|
229
|
+
* Structure: field 1 = workspaces[0], 1.1 = workspaceFolderAbsoluteUri.
|
|
230
|
+
* Returns the raw file:// URI, or null.
|
|
231
|
+
*/
|
|
232
|
+
export function readDbWorkspaceUri(conversationsDir, cascadeId) {
|
|
233
|
+
let rows;
|
|
234
|
+
try {
|
|
235
|
+
rows = queryCascadeDb(conversationsDir, cascadeId, 'SELECT hex(data) AS h FROM trajectory_metadata_blob LIMIT 1');
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (isSqliteUnavailableError(err)) throw err;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (!rows.length || !rows[0].h) return null;
|
|
241
|
+
try {
|
|
242
|
+
const meta = decodeMessage(Buffer.from(rows[0].h, 'hex'));
|
|
243
|
+
const ws0 = firstMessage(meta, 1);
|
|
244
|
+
return (ws0 && firstString(ws0, 1)) || null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Step source enum in steps.metadata field 3 (behavior-verified against payload
|
|
251
|
+
// contents, since it mirrors the RPC's CORTEX_STEP_SOURCE_*): 4 = user turn,
|
|
252
|
+
// 2 = model turn. Everything else (system, tool, unspecified) is skipped.
|
|
253
|
+
const STEP_SOURCE_USER = 4;
|
|
254
|
+
const STEP_SOURCE_MODEL = 2;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Parse one steps.metadata blob into a session-timing event, or null if the
|
|
258
|
+
* step is neither a user nor a model turn (system/tool/unspecified). createdAt
|
|
259
|
+
* is a Timestamp at field 1 (seconds at 1.1); source enum is field 3.
|
|
260
|
+
*
|
|
261
|
+
* @param {Buffer} buf
|
|
262
|
+
* @returns {{role:'user'|'assistant', timestamp: Date}|null}
|
|
263
|
+
*/
|
|
264
|
+
export function parseStepMetadata(buf) {
|
|
265
|
+
const meta = decodeMessage(buf);
|
|
266
|
+
const source = firstVarint(meta, 3);
|
|
267
|
+
let role;
|
|
268
|
+
if (source === STEP_SOURCE_USER) role = 'user';
|
|
269
|
+
else if (source === STEP_SOURCE_MODEL) role = 'assistant';
|
|
270
|
+
else return null;
|
|
271
|
+
|
|
272
|
+
const createdAt = firstMessage(meta, 1);
|
|
273
|
+
const seconds = createdAt ? firstVarint(createdAt, 1) : undefined;
|
|
274
|
+
if (!seconds) return null;
|
|
275
|
+
|
|
276
|
+
return { role, timestamp: new Date(seconds * 1000) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Read session timing events (user/assistant turns) for a cascade from the
|
|
281
|
+
* steps table, chronological by idx.
|
|
282
|
+
*/
|
|
283
|
+
export function readDbSessionEvents(conversationsDir, cascadeId) {
|
|
284
|
+
let rows;
|
|
285
|
+
try {
|
|
286
|
+
rows = queryCascadeDb(
|
|
287
|
+
conversationsDir,
|
|
288
|
+
cascadeId,
|
|
289
|
+
'SELECT hex(metadata) AS h FROM steps WHERE metadata IS NOT NULL ORDER BY idx',
|
|
290
|
+
);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (isSqliteUnavailableError(err)) throw err;
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
const events = [];
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
if (!row.h) continue;
|
|
298
|
+
let ev;
|
|
299
|
+
try {
|
|
300
|
+
ev = parseStepMetadata(Buffer.from(row.h, 'hex'));
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (ev) events.push(ev);
|
|
305
|
+
}
|
|
306
|
+
return events;
|
|
307
|
+
}
|
|
@@ -3,18 +3,25 @@ import { readdirSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
6
|
+
import { listDbCascades, readDbUsageRecords, readDbWorkspaceUri, readDbSessionEvents } from './antigravity-db.js';
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Antigravity parser
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
11
|
+
* Antigravity parser.
|
|
12
|
+
*
|
|
13
|
+
* Two conversation stores, two read paths:
|
|
14
|
+
* - `.db` cascades (App 2.0 + `agy` CLI): plain-protobuf SQLite, parsed offline
|
|
15
|
+
* from disk — no running process required (see antigravity-db.js).
|
|
16
|
+
* - `.pb` cascades (legacy App history): encrypted/opaque, only decodable via a
|
|
17
|
+
* running language server's GetCascadeTrajectory RPC (fallback below).
|
|
18
|
+
* A cascade backed by a `.db` never uses RPC, so the two paths never double-count.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
const SOURCE = 'antigravity';
|
|
17
22
|
const CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity', 'conversations');
|
|
23
|
+
// `agy` CLI stores conversations in a separate, App-independent directory.
|
|
24
|
+
const CLI_CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity-cli', 'conversations');
|
|
18
25
|
|
|
19
26
|
// User sources → role 'user'; Model source → role 'assistant'; System sources → skip
|
|
20
27
|
const USER_SOURCES = new Set([
|
|
@@ -221,12 +228,14 @@ async function probeHttpPort(ports, csrfToken) {
|
|
|
221
228
|
/**
|
|
222
229
|
* Normalize model names to canonical forms.
|
|
223
230
|
*/
|
|
231
|
+
// Normalize model names to canonical forms. NOTE: only legacy .pb data (which
|
|
232
|
+
// exposes bare slugs via responseModel) reaches this; .db data uses the
|
|
233
|
+
// human-readable modelDisplayName verbatim (e.g. "Gemini 3.5 Flash (High)"),
|
|
234
|
+
// which is never normalized. Flash reasoning tiers (-a/-b/-c) are intentionally
|
|
235
|
+
// NOT merged: each tier is a distinct choice and left as-is ("as it is").
|
|
224
236
|
const MODEL_NORMALIZE_MAP = {
|
|
225
237
|
'claude-opus-4-6-thinking': 'claude-opus-4-6',
|
|
226
238
|
'claude-sonnet-4-6-thinking': 'claude-sonnet-4-6',
|
|
227
|
-
'gemini-3-flash-a': 'gemini-3-flash',
|
|
228
|
-
'gemini-3-flash-b': 'gemini-3-flash',
|
|
229
|
-
'gemini-3-flash-c': 'gemini-3-flash',
|
|
230
239
|
"gemini-3.1-pro-high": "gemini-3.1-pro",
|
|
231
240
|
"gemini-3.1-pro-low": "gemini-3.1-pro",
|
|
232
241
|
"gemini-3-pro-high": "gemini-3-pro",
|
|
@@ -244,10 +253,6 @@ const PLACEHOLDER_MODEL_MAP = {
|
|
|
244
253
|
'MODEL_PLACEHOLDER_M35': 'claude-sonnet-4-6',
|
|
245
254
|
'MODEL_PLACEHOLDER_M26': 'claude-opus-4-6',
|
|
246
255
|
'MODEL_OPENAI_GPT_OSS_120B_MEDIUM': 'gpt-oss-120b',
|
|
247
|
-
// Antigravity 2.0+ standalone app placeholders (verified via responseModel)
|
|
248
|
-
'MODEL_PLACEHOLDER_M132': 'gemini-3-flash',
|
|
249
|
-
'MODEL_PLACEHOLDER_M133': 'gemini-3-flash',
|
|
250
|
-
'MODEL_PLACEHOLDER_M20': 'gemini-3-flash',
|
|
251
256
|
};
|
|
252
257
|
|
|
253
258
|
function normalizeModel(raw) {
|
|
@@ -255,9 +260,16 @@ function normalizeModel(raw) {
|
|
|
255
260
|
}
|
|
256
261
|
|
|
257
262
|
/**
|
|
258
|
-
* Resolve model name
|
|
263
|
+
* Resolve a display model name from a chatModel-like object.
|
|
264
|
+
* Priority: modelDisplayName (real name, e.g. "Gemini 3.5 Flash (High)") →
|
|
265
|
+
* responseModel slug (normalized) → placeholder map → "unknown".
|
|
266
|
+
*
|
|
267
|
+
* modelDisplayName is present on .db data (App 2.0 + CLI) and is authoritative;
|
|
268
|
+
* it is used verbatim (carries the reasoning tier). Legacy .pb data has no
|
|
269
|
+
* display name, so it falls back to the responseModel slug.
|
|
259
270
|
*/
|
|
260
271
|
function resolveModel(chatModel) {
|
|
272
|
+
if (chatModel.modelDisplayName) return chatModel.modelDisplayName;
|
|
261
273
|
if (chatModel.responseModel) return normalizeModel(chatModel.responseModel);
|
|
262
274
|
const placeholder = chatModel.model || '';
|
|
263
275
|
if (PLACEHOLDER_MODEL_MAP[placeholder]) return PLACEHOLDER_MODEL_MAP[placeholder];
|
|
@@ -280,21 +292,16 @@ function projectFromUri(uri) {
|
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
/**
|
|
283
|
-
* List cascade IDs
|
|
284
|
-
*
|
|
295
|
+
* List cascade IDs backed by a legacy `.pb` file (App history). `.db` cascades
|
|
296
|
+
* are handled separately via offline parsing.
|
|
285
297
|
*/
|
|
286
|
-
function
|
|
298
|
+
function listPbCascades() {
|
|
287
299
|
try {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
// SQLite auxiliaries (foo.db-wal / foo.db-shm) don't end with .db, so no
|
|
292
|
-
// extra filtering is needed here.
|
|
293
|
-
if (f.endsWith('.pb') || f.endsWith('.db')) {
|
|
294
|
-
seen.add(f.slice(0, -3));
|
|
295
|
-
}
|
|
300
|
+
const out = [];
|
|
301
|
+
for (const f of readdirSync(CONVERSATIONS_DIR)) {
|
|
302
|
+
if (f.endsWith('.pb')) out.push(f.slice(0, -3));
|
|
296
303
|
}
|
|
297
|
-
return
|
|
304
|
+
return out;
|
|
298
305
|
} catch {
|
|
299
306
|
return [];
|
|
300
307
|
}
|
|
@@ -302,113 +309,133 @@ function listCascades() {
|
|
|
302
309
|
|
|
303
310
|
// ── Main parse ───────────────────────────────────────────────────────
|
|
304
311
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const server = findLanguageServer();
|
|
312
|
-
if (!server) return { buckets: [], sessions: [] };
|
|
313
|
-
|
|
314
|
-
const ports = findListeningPorts(server.pid);
|
|
315
|
-
if (ports.length === 0) return { buckets: [], sessions: [] };
|
|
316
|
-
|
|
317
|
-
const baseUrl = await probeHttpPort(ports, server.csrfToken);
|
|
318
|
-
if (!baseUrl) return { buckets: [], sessions: [] };
|
|
319
|
-
|
|
320
|
-
const rpc = (method, body) =>
|
|
321
|
-
rpcPost(
|
|
322
|
-
baseUrl,
|
|
323
|
-
`/exa.language_server_pb.LanguageServerService/${method}`,
|
|
324
|
-
body,
|
|
325
|
-
server.csrfToken,
|
|
326
|
-
);
|
|
312
|
+
/** Model name for an offline .db record: real display name → slug → unknown. */
|
|
313
|
+
function modelFromRecord(rec) {
|
|
314
|
+
if (rec.displayName) return rec.displayName;
|
|
315
|
+
if (rec.responseModel) return normalizeModel(rec.responseModel);
|
|
316
|
+
return 'unknown';
|
|
317
|
+
}
|
|
327
318
|
|
|
328
|
-
|
|
319
|
+
export async function parse() {
|
|
329
320
|
const entries = [];
|
|
330
321
|
const sessionEvents = [];
|
|
331
322
|
const seenResponseIds = new Set();
|
|
332
323
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (!chatModel) continue;
|
|
359
|
-
|
|
360
|
-
const responseModel = resolveModel(chatModel);
|
|
361
|
-
const createdAt = chatModel?.chatStartMetadata?.createdAt;
|
|
362
|
-
const ts = createdAt ? new Date(createdAt) : null;
|
|
363
|
-
if (!ts || isNaN(ts.getTime())) continue;
|
|
364
|
-
|
|
365
|
-
const retryInfos = chatModel.retryInfos || [];
|
|
366
|
-
for (const retry of retryInfos) {
|
|
367
|
-
const usage = retry.usage;
|
|
368
|
-
if (!usage) continue;
|
|
369
|
-
|
|
370
|
-
const responseId = usage.responseId || '';
|
|
371
|
-
if (responseId && seenResponseIds.has(responseId)) continue;
|
|
372
|
-
if (responseId) seenResponseIds.add(responseId);
|
|
324
|
+
// ── Path 1: offline .db parsing (App 2.0 + agy CLI, no process needed) ──
|
|
325
|
+
const dbHandled = new Set();
|
|
326
|
+
for (const dir of [CONVERSATIONS_DIR, CLI_CONVERSATIONS_DIR]) {
|
|
327
|
+
for (const cascadeId of listDbCascades(dir)) {
|
|
328
|
+
const records = readDbUsageRecords(dir, cascadeId);
|
|
329
|
+
const project = projectFromUri(readDbWorkspaceUri(dir, cascadeId)) || 'unknown';
|
|
330
|
+
|
|
331
|
+
if (records.length > 0) {
|
|
332
|
+
dbHandled.add(cascadeId);
|
|
333
|
+
for (const rec of records) {
|
|
334
|
+
if (rec.responseId && seenResponseIds.has(rec.responseId)) continue;
|
|
335
|
+
if (rec.responseId) seenResponseIds.add(rec.responseId);
|
|
336
|
+
if (!rec.timestamp || isNaN(rec.timestamp.getTime())) continue;
|
|
337
|
+
entries.push({
|
|
338
|
+
source: SOURCE,
|
|
339
|
+
model: modelFromRecord(rec),
|
|
340
|
+
project,
|
|
341
|
+
timestamp: rec.timestamp,
|
|
342
|
+
inputTokens: toSafeNumber(rec.inputTokens),
|
|
343
|
+
outputTokens: toSafeNumber(rec.outputTokens),
|
|
344
|
+
cachedInputTokens: toSafeNumber(rec.cacheReadTokens),
|
|
345
|
+
reasoningOutputTokens: toSafeNumber(rec.thinkingOutputTokens),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
373
349
|
|
|
374
|
-
|
|
350
|
+
// Session timing from steps (independent of token usage presence).
|
|
351
|
+
for (const ev of readDbSessionEvents(dir, cascadeId)) {
|
|
352
|
+
sessionEvents.push({
|
|
353
|
+
sessionId: cascadeId,
|
|
375
354
|
source: SOURCE,
|
|
376
|
-
model: responseModel,
|
|
377
355
|
project,
|
|
378
|
-
timestamp:
|
|
379
|
-
|
|
380
|
-
outputTokens: toSafeNumber(usage.outputTokens),
|
|
381
|
-
cachedInputTokens: toSafeNumber(usage.cacheReadTokens),
|
|
382
|
-
reasoningOutputTokens: toSafeNumber(usage.thinkingOutputTokens),
|
|
356
|
+
timestamp: ev.timestamp,
|
|
357
|
+
role: ev.role,
|
|
383
358
|
});
|
|
384
359
|
}
|
|
385
360
|
}
|
|
361
|
+
}
|
|
386
362
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
363
|
+
// ── Path 2: RPC fallback, only for legacy .pb cascades not already parsed ──
|
|
364
|
+
const pbCascades = listPbCascades().filter((id) => !dbHandled.has(id));
|
|
365
|
+
if (pbCascades.length > 0) {
|
|
366
|
+
const server = findLanguageServer();
|
|
367
|
+
const ports = server ? findListeningPorts(server.pid) : [];
|
|
368
|
+
const baseUrl = ports.length > 0 ? await probeHttpPort(ports, server.csrfToken) : null;
|
|
369
|
+
if (baseUrl) {
|
|
370
|
+
const rpc = (method, body) =>
|
|
371
|
+
rpcPost(
|
|
372
|
+
baseUrl,
|
|
373
|
+
`/exa.language_server_pb.LanguageServerService/${method}`,
|
|
374
|
+
body,
|
|
375
|
+
server.csrfToken,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
for (const cascadeId of pbCascades) {
|
|
379
|
+
let resp;
|
|
380
|
+
try {
|
|
381
|
+
resp = await rpc('GetCascadeTrajectory', { cascadeId });
|
|
382
|
+
} catch {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const trajectory = resp?.trajectory;
|
|
386
|
+
if (!trajectory) continue;
|
|
387
|
+
|
|
388
|
+
const steps = trajectory.steps || [];
|
|
389
|
+
const metadataList = trajectory.generatorMetadata || [];
|
|
390
|
+
|
|
391
|
+
let project = 'unknown';
|
|
392
|
+
const workspaces = trajectory.metadata?.workspaces || [];
|
|
393
|
+
if (workspaces.length > 0) {
|
|
394
|
+
project = workspaces[0].repository?.computedName
|
|
395
|
+
|| projectFromUri(workspaces[0].workspaceFolderAbsoluteUri)
|
|
396
|
+
|| 'unknown';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const meta of metadataList) {
|
|
400
|
+
const chatModel = meta?.chatModel;
|
|
401
|
+
if (!chatModel) continue;
|
|
402
|
+
const model = resolveModel(chatModel);
|
|
403
|
+
const createdAt = chatModel?.chatStartMetadata?.createdAt;
|
|
404
|
+
const ts = createdAt ? new Date(createdAt) : null;
|
|
405
|
+
if (!ts || isNaN(ts.getTime())) continue;
|
|
406
|
+
|
|
407
|
+
for (const retry of (chatModel.retryInfos || [])) {
|
|
408
|
+
const usage = retry.usage;
|
|
409
|
+
if (!usage) continue;
|
|
410
|
+
const responseId = usage.responseId || '';
|
|
411
|
+
if (responseId && seenResponseIds.has(responseId)) continue;
|
|
412
|
+
if (responseId) seenResponseIds.add(responseId);
|
|
413
|
+
entries.push({
|
|
414
|
+
source: SOURCE,
|
|
415
|
+
model,
|
|
416
|
+
project,
|
|
417
|
+
timestamp: ts,
|
|
418
|
+
inputTokens: toSafeNumber(usage.inputTokens),
|
|
419
|
+
outputTokens: toSafeNumber(usage.outputTokens),
|
|
420
|
+
cachedInputTokens: toSafeNumber(usage.cacheReadTokens),
|
|
421
|
+
reasoningOutputTokens: toSafeNumber(usage.thinkingOutputTokens),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const step of steps) {
|
|
427
|
+
const stepSource = step?.metadata?.source || '';
|
|
428
|
+
let role;
|
|
429
|
+
if (USER_SOURCES.has(stepSource)) role = 'user';
|
|
430
|
+
else if (ASSISTANT_SOURCES.has(stepSource)) role = 'assistant';
|
|
431
|
+
else continue;
|
|
432
|
+
const createdAt = step?.metadata?.createdAt;
|
|
433
|
+
const ts = createdAt ? new Date(createdAt) : null;
|
|
434
|
+
if (!ts || isNaN(ts.getTime())) continue;
|
|
435
|
+
sessionEvents.push({ sessionId: cascadeId, source: SOURCE, project, timestamp: ts, role });
|
|
436
|
+
}
|
|
397
437
|
}
|
|
398
|
-
|
|
399
|
-
const createdAt = step?.metadata?.createdAt;
|
|
400
|
-
const ts = createdAt ? new Date(createdAt) : null;
|
|
401
|
-
if (!ts || isNaN(ts.getTime())) continue;
|
|
402
|
-
|
|
403
|
-
sessionEvents.push({
|
|
404
|
-
sessionId: cascadeId,
|
|
405
|
-
source: SOURCE,
|
|
406
|
-
project,
|
|
407
|
-
timestamp: ts,
|
|
408
|
-
role,
|
|
409
|
-
});
|
|
410
438
|
}
|
|
411
|
-
|
|
412
439
|
}
|
|
413
440
|
|
|
414
441
|
return {
|
package/src/parsers/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { parse as parseHermes } from './hermes.js';
|
|
|
17
17
|
import { parse as parseKiro } from './kiro.js';
|
|
18
18
|
import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
|
|
19
19
|
import { parse as parseZcode } from './zcode.js';
|
|
20
|
+
import { parse as parseTraeCli } from './trae-cli.js';
|
|
20
21
|
|
|
21
22
|
export const parsers = {
|
|
22
23
|
'claude-code': parseClaudeCode,
|
|
@@ -37,6 +38,7 @@ export const parsers = {
|
|
|
37
38
|
'kiro': parseKiro,
|
|
38
39
|
'pi-coding-agent': parsePiCodingAgent,
|
|
39
40
|
'zcode': parseZcode,
|
|
41
|
+
'trae-cli': parseTraeCli,
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { findTraeCliDataDirs } from '../tools.js';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
|
|
6
|
+
function readJsonSafe(path) {
|
|
7
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function projectFromPath(absPath) {
|
|
11
|
+
if (!absPath || typeof absPath !== 'string') return 'unknown';
|
|
12
|
+
const trimmed = absPath.replace(/[\\/]+$/, '');
|
|
13
|
+
const name = basename(trimmed);
|
|
14
|
+
return name || 'unknown';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseJsonlSafe(path) {
|
|
18
|
+
try {
|
|
19
|
+
const content = readFileSync(path, 'utf-8');
|
|
20
|
+
return content
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(line => line.trim())
|
|
23
|
+
.filter(line => line.length > 0)
|
|
24
|
+
.map(line => {
|
|
25
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
26
|
+
})
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function parse() {
|
|
34
|
+
const cacheDirs = findTraeCliDataDirs();
|
|
35
|
+
if (cacheDirs.length === 0) return { buckets: [], sessions: [] };
|
|
36
|
+
|
|
37
|
+
const entries = [];
|
|
38
|
+
const events = [];
|
|
39
|
+
|
|
40
|
+
for (const cacheDir of cacheDirs) {
|
|
41
|
+
let sessionDirs = [];
|
|
42
|
+
try {
|
|
43
|
+
sessionDirs = readdirSync(cacheDir, { withFileTypes: true })
|
|
44
|
+
.filter(entry => entry.isDirectory())
|
|
45
|
+
.map(entry => entry.name);
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const sessionId of sessionDirs) {
|
|
51
|
+
const sessionPath = join(cacheDir, sessionId);
|
|
52
|
+
const sessionJson = readJsonSafe(join(sessionPath, 'session.json')) || {};
|
|
53
|
+
const project = projectFromPath(sessionJson.metadata?.cwd);
|
|
54
|
+
const fallbackModel = sessionJson.metadata?.model_name || 'trae-unknown';
|
|
55
|
+
|
|
56
|
+
// 1. Parse traces.jsonl for token usage
|
|
57
|
+
const traceLines = parseJsonlSafe(join(sessionPath, 'traces.jsonl'));
|
|
58
|
+
const tracesMap = new Map();
|
|
59
|
+
|
|
60
|
+
for (const line of traceLines) {
|
|
61
|
+
if (!line.traceID) continue;
|
|
62
|
+
const tags = Array.isArray(line.tags) ? line.tags : [];
|
|
63
|
+
const tagMap = {};
|
|
64
|
+
for (const t of tags) {
|
|
65
|
+
if (t && typeof t === 'object' && t.key) {
|
|
66
|
+
tagMap[t.key] = t.value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const model = tagMap['model.name'] || tagMap['semantic.name'] || null;
|
|
71
|
+
const inputTokens = Math.max(0, Number(tagMap['usage.input_tokens']) || 0);
|
|
72
|
+
const outputTokens = Math.max(0, Number(tagMap['usage.output_tokens']) || 0);
|
|
73
|
+
const cacheReadTokens = Math.max(0, Number(tagMap['usage.cache_read_tokens']) || 0);
|
|
74
|
+
const reasoningTokens = Math.max(0, Number(tagMap['usage.reasoning_tokens']) || 0);
|
|
75
|
+
|
|
76
|
+
if (inputTokens + outputTokens + cacheReadTokens + reasoningTokens === 0) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!tracesMap.has(line.traceID)) {
|
|
81
|
+
tracesMap.set(line.traceID, {
|
|
82
|
+
model,
|
|
83
|
+
inputTokens,
|
|
84
|
+
outputTokens,
|
|
85
|
+
cacheReadTokens,
|
|
86
|
+
reasoningTokens,
|
|
87
|
+
startTime: Number(line.startTime) || 0,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
// Merge spans under the same traceID by selecting the max values
|
|
91
|
+
const existing = tracesMap.get(line.traceID);
|
|
92
|
+
if (model) {
|
|
93
|
+
existing.model = model;
|
|
94
|
+
}
|
|
95
|
+
existing.inputTokens = Math.max(existing.inputTokens, inputTokens);
|
|
96
|
+
existing.outputTokens = Math.max(existing.outputTokens, outputTokens);
|
|
97
|
+
existing.cacheReadTokens = Math.max(existing.cacheReadTokens, cacheReadTokens);
|
|
98
|
+
existing.reasoningTokens = Math.max(existing.reasoningTokens, reasoningTokens);
|
|
99
|
+
if (line.startTime) {
|
|
100
|
+
existing.startTime = existing.startTime ? Math.min(existing.startTime, Number(line.startTime)) : Number(line.startTime);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convert trace map to vibe-usage entries
|
|
106
|
+
for (const trace of tracesMap.values()) {
|
|
107
|
+
// Convert microsecond startTime to milliseconds for Date constructor
|
|
108
|
+
const startTime = Number(trace.startTime);
|
|
109
|
+
if (!Number.isFinite(startTime) || startTime <= 0) continue;
|
|
110
|
+
const timestamp = new Date(startTime / 1000);
|
|
111
|
+
entries.push({
|
|
112
|
+
source: 'trae-cli',
|
|
113
|
+
model: trace.model || fallbackModel,
|
|
114
|
+
project,
|
|
115
|
+
timestamp,
|
|
116
|
+
inputTokens: trace.inputTokens,
|
|
117
|
+
outputTokens: trace.outputTokens,
|
|
118
|
+
cachedInputTokens: trace.cacheReadTokens,
|
|
119
|
+
reasoningOutputTokens: trace.reasoningTokens,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2. Parse events.jsonl for user and assistant timings
|
|
124
|
+
const eventLines = parseJsonlSafe(join(sessionPath, 'events.jsonl'));
|
|
125
|
+
for (const line of eventLines) {
|
|
126
|
+
if (!line.created_at) continue;
|
|
127
|
+
const timestamp = new Date(line.created_at);
|
|
128
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
129
|
+
|
|
130
|
+
if (line.agent_start) {
|
|
131
|
+
events.push({
|
|
132
|
+
sessionId,
|
|
133
|
+
source: 'trae-cli',
|
|
134
|
+
project,
|
|
135
|
+
timestamp,
|
|
136
|
+
role: 'user',
|
|
137
|
+
});
|
|
138
|
+
} else if (line.agent_end || line.tool_call || (line.message && line.message.message?.role === 'assistant')) {
|
|
139
|
+
events.push({
|
|
140
|
+
sessionId,
|
|
141
|
+
source: 'trae-cli',
|
|
142
|
+
project,
|
|
143
|
+
timestamp,
|
|
144
|
+
role: 'assistant',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
buckets: aggregateToBuckets(entries),
|
|
153
|
+
sessions: extractSessions(events),
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -113,11 +113,41 @@ function findKimiCodeDataDirs() {
|
|
|
113
113
|
].filter(existsSync);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function findAntigravityDataDirs() {
|
|
117
|
+
return [
|
|
118
|
+
join(homedir(), '.gemini', 'antigravity'),
|
|
119
|
+
join(homedir(), '.gemini', 'antigravity-cli'),
|
|
120
|
+
].filter(existsSync);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function findTraeCliDataDirs() {
|
|
124
|
+
const envDir = process.env.VIBE_USAGE_TRAE_CLI_SESSIONS?.trim();
|
|
125
|
+
if (envDir) {
|
|
126
|
+
return [envDir].filter(existsSync);
|
|
127
|
+
}
|
|
128
|
+
if (process.platform === 'darwin') {
|
|
129
|
+
return [join(homedir(), 'Library', 'Caches', 'trae-cli', 'sessions')].filter(existsSync);
|
|
130
|
+
}
|
|
131
|
+
if (process.platform === 'win32') {
|
|
132
|
+
const localAppData = process.env.LOCALAPPDATA?.trim() || join(homedir(), 'AppData', 'Local');
|
|
133
|
+
return [join(localAppData, 'trae-cli', 'cache', 'sessions')].filter(existsSync);
|
|
134
|
+
}
|
|
135
|
+
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim() || join(homedir(), '.cache');
|
|
136
|
+
return [join(xdgCacheHome, 'trae-cli', 'sessions')].filter(existsSync);
|
|
137
|
+
}
|
|
138
|
+
|
|
116
139
|
export const TOOLS = [
|
|
140
|
+
{
|
|
141
|
+
name: 'Trae CLI',
|
|
142
|
+
id: 'trae-cli',
|
|
143
|
+
dataDir: join(homedir(), 'Library', 'Caches', 'trae-cli', 'sessions'),
|
|
144
|
+
detectDataDirs: findTraeCliDataDirs,
|
|
145
|
+
},
|
|
117
146
|
{
|
|
118
147
|
name: 'Antigravity',
|
|
119
148
|
id: 'antigravity',
|
|
120
149
|
dataDir: join(homedir(), '.gemini', 'antigravity'),
|
|
150
|
+
detectDataDirs: findAntigravityDataDirs,
|
|
121
151
|
},
|
|
122
152
|
{
|
|
123
153
|
name: 'Claude Code',
|