@vibe-cafe/vibe-usage 0.9.13 → 0.9.14
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 +1 -0
- package/package.json +2 -1
- package/src/parsers/grok.js +325 -0
- package/src/parsers/index.js +6 -4
- package/src/skill.js +1 -1
- package/src/tools.js +46 -18
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
52
52
|
|------|---------------|
|
|
53
53
|
| Claude Code | `~/.claude/projects/` (tokens + sessions), `~/.claude/transcripts/` (sessions only); also scans `$CLAUDE_CONFIG_DIR` when set (deduped), so relocated configs and GUI/CLI env mismatches are both covered |
|
|
54
54
|
| Codex CLI | `$CODEX_HOME/sessions/` and `$CODEX_HOME/archived_sessions/` (default `~/.codex`); forked and sub-agent rollouts replay parent metadata, tasks, and `token_count` records at spawn time — full or last-N-turn replay blocks are matched as a fingerprinted parent suffix plus the child's own task boundary, duplicate cumulative emissions count once, and live/archive copies of the same session are deduplicated |
|
|
55
|
+
| 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` |
|
|
55
56
|
| GitHub Copilot CLI | `~/.copilot/session-state/*/events.jsonl` |
|
|
56
57
|
| 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 |
|
|
57
58
|
| Gemini CLI | `~/.gemini/tmp/<project_hash>/chats/session-*.jsonl` (current line-delimited format) and legacy `session-*.json`; recurses into nested subagent sessions |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-cafe/vibe-usage",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"description": "Track your AI coding tool token usage and sync to vibecafe.ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"tokens",
|
|
24
24
|
"claude",
|
|
25
25
|
"codex",
|
|
26
|
+
"grok",
|
|
26
27
|
"gemini"
|
|
27
28
|
],
|
|
28
29
|
"dependencies": {},
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { createReadStream, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { findGrokDataDirs, getGrokSessionsDir } from '../tools.js';
|
|
5
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
6
|
+
|
|
7
|
+
const SOURCE = 'grok';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Grok (Grok Build TUI / CLI) parser.
|
|
11
|
+
*
|
|
12
|
+
* Layout (see ~/.grok/docs/user-guide/17-sessions.md):
|
|
13
|
+
* $GROK_HOME/sessions/<url-encoded-cwd>/<session-id>/
|
|
14
|
+
* summary.json — cwd, model, timestamps
|
|
15
|
+
* updates.jsonl — ACP session updates; turn_completed carries exact usage
|
|
16
|
+
* events.jsonl — turn_started / turn_ended timing
|
|
17
|
+
*
|
|
18
|
+
* GROK_HOME defaults to ~/.grok. Override with GROK_HOME or
|
|
19
|
+
* VIBE_USAGE_GROK_SESSIONS (tests / relocated session trees).
|
|
20
|
+
*
|
|
21
|
+
* Token usage comes from updates.jsonl `turn_completed.usage` (and per-model
|
|
22
|
+
* `modelUsage` when present). inputTokens is non-cached prompt (total − cache
|
|
23
|
+
* reads), matching Codex/Copilot so totalTokens does not double-count cache.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
function readJsonSafe(path) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function projectFromPath(absPath) {
|
|
35
|
+
if (!absPath || typeof absPath !== 'string') return 'unknown';
|
|
36
|
+
const trimmed = absPath.replace(/[\\/]+$/, '');
|
|
37
|
+
const name = basename(trimmed);
|
|
38
|
+
return name || 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Decode a sessions group dirname; fall back to basename after decode. */
|
|
42
|
+
function projectFromGroupDir(groupName, groupPath) {
|
|
43
|
+
const cwdFile = join(groupPath, '.cwd');
|
|
44
|
+
if (existsSync(cwdFile)) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(cwdFile, 'utf-8').trim();
|
|
47
|
+
if (raw) return projectFromPath(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const decoded = decodeURIComponent(groupName);
|
|
54
|
+
if (decoded.includes('/') || decoded.includes('\\')) {
|
|
55
|
+
return projectFromPath(decoded);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// not URI-encoded
|
|
59
|
+
}
|
|
60
|
+
return groupName || 'unknown';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toDate(value) {
|
|
64
|
+
if (value == null) return null;
|
|
65
|
+
if (value instanceof Date) {
|
|
66
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
69
|
+
// Unix seconds (Grok updates.jsonl) vs milliseconds
|
|
70
|
+
const ms = value < 1e12 ? value * 1000 : value;
|
|
71
|
+
const d = new Date(ms);
|
|
72
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
73
|
+
}
|
|
74
|
+
if (typeof value === 'string' && value.trim()) {
|
|
75
|
+
const d = new Date(value);
|
|
76
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pushUsageEntry(entries, { model, project, timestamp, usage }) {
|
|
82
|
+
if (!usage || typeof usage !== 'object') return;
|
|
83
|
+
if (!timestamp) return;
|
|
84
|
+
|
|
85
|
+
const totalInput = Math.max(0, Number(usage.inputTokens) || 0);
|
|
86
|
+
const cached = Math.max(0, Number(usage.cachedReadTokens) || 0);
|
|
87
|
+
const output = Math.max(0, Number(usage.outputTokens) || 0);
|
|
88
|
+
const reasoning = Math.max(0, Number(usage.reasoningTokens) || 0);
|
|
89
|
+
|
|
90
|
+
// Prefer exclusive fields when both are present (Codex-style).
|
|
91
|
+
const inputTokens = Math.max(0, totalInput - cached);
|
|
92
|
+
const outputTokens = Math.max(0, output - reasoning);
|
|
93
|
+
|
|
94
|
+
if (inputTokens + outputTokens + cached + reasoning === 0) return;
|
|
95
|
+
|
|
96
|
+
entries.push({
|
|
97
|
+
source: SOURCE,
|
|
98
|
+
model: model || 'unknown',
|
|
99
|
+
project,
|
|
100
|
+
timestamp,
|
|
101
|
+
inputTokens,
|
|
102
|
+
outputTokens,
|
|
103
|
+
cachedInputTokens: cached,
|
|
104
|
+
reasoningOutputTokens: reasoning,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function emitTurnUsage(entries, { usage, project, timestamp, fallbackModel }) {
|
|
109
|
+
if (!usage || typeof usage !== 'object') return;
|
|
110
|
+
|
|
111
|
+
const modelUsage = usage.modelUsage;
|
|
112
|
+
if (modelUsage && typeof modelUsage === 'object' && Object.keys(modelUsage).length > 0) {
|
|
113
|
+
for (const [model, mUsage] of Object.entries(modelUsage)) {
|
|
114
|
+
pushUsageEntry(entries, {
|
|
115
|
+
model,
|
|
116
|
+
project,
|
|
117
|
+
timestamp,
|
|
118
|
+
usage: mUsage && typeof mUsage === 'object' ? mUsage : usage,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pushUsageEntry(entries, {
|
|
125
|
+
model: fallbackModel,
|
|
126
|
+
project,
|
|
127
|
+
timestamp,
|
|
128
|
+
usage,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function forEachJsonlLine(filePath, onLine) {
|
|
133
|
+
if (!existsSync(filePath)) return;
|
|
134
|
+
let stream;
|
|
135
|
+
try {
|
|
136
|
+
stream = createReadStream(filePath, { encoding: 'utf-8' });
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
142
|
+
try {
|
|
143
|
+
for await (const line of rl) {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed) continue;
|
|
146
|
+
let obj;
|
|
147
|
+
try {
|
|
148
|
+
obj = JSON.parse(trimmed);
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
onLine(obj);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// unreadable / truncated mid-write — keep what we have
|
|
156
|
+
} finally {
|
|
157
|
+
rl.close();
|
|
158
|
+
stream.destroy();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function listSessionDirs(sessionsDir) {
|
|
163
|
+
const results = [];
|
|
164
|
+
if (!existsSync(sessionsDir)) return results;
|
|
165
|
+
|
|
166
|
+
let groups;
|
|
167
|
+
try {
|
|
168
|
+
groups = readdirSync(sessionsDir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const group of groups) {
|
|
174
|
+
if (!group.isDirectory()) continue;
|
|
175
|
+
// Skip non-project group dirs (e.g. future index folders).
|
|
176
|
+
const groupPath = join(sessionsDir, group.name);
|
|
177
|
+
let children;
|
|
178
|
+
try {
|
|
179
|
+
children = readdirSync(groupPath, { withFileTypes: true });
|
|
180
|
+
} catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const projectFallback = projectFromGroupDir(group.name, groupPath);
|
|
185
|
+
|
|
186
|
+
for (const child of children) {
|
|
187
|
+
if (!child.isDirectory()) continue;
|
|
188
|
+
const sessionPath = join(groupPath, child.name);
|
|
189
|
+
// A real session always has summary.json (or at least updates/chat history).
|
|
190
|
+
if (
|
|
191
|
+
!existsSync(join(sessionPath, 'summary.json')) &&
|
|
192
|
+
!existsSync(join(sessionPath, 'updates.jsonl'))
|
|
193
|
+
) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
results.push({
|
|
197
|
+
sessionId: child.name,
|
|
198
|
+
sessionPath,
|
|
199
|
+
projectFallback,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse all Grok sessions under the configured sessions root(s).
|
|
209
|
+
* @returns {Promise<{ buckets: object[], sessions: object[] }>}
|
|
210
|
+
*/
|
|
211
|
+
export async function parse() {
|
|
212
|
+
const sessionRoots = findGrokDataDirs();
|
|
213
|
+
// findGrokDataDirs returns sessions dirs; also allow empty → try default once
|
|
214
|
+
const roots = sessionRoots.length > 0 ? sessionRoots : [getGrokSessionsDir()].filter(existsSync);
|
|
215
|
+
if (roots.length === 0) return { buckets: [], sessions: [] };
|
|
216
|
+
|
|
217
|
+
const entries = [];
|
|
218
|
+
const sessionEvents = [];
|
|
219
|
+
|
|
220
|
+
for (const sessionsDir of roots) {
|
|
221
|
+
for (const { sessionId, sessionPath, projectFallback } of listSessionDirs(sessionsDir)) {
|
|
222
|
+
const summary = readJsonSafe(join(sessionPath, 'summary.json')) || {};
|
|
223
|
+
const cwd = summary.info?.cwd || summary.git_root_dir || null;
|
|
224
|
+
const project = cwd ? projectFromPath(cwd) : projectFallback;
|
|
225
|
+
const fallbackModel = summary.current_model_id || 'unknown';
|
|
226
|
+
|
|
227
|
+
// Prefer updates.jsonl turn_completed for exact usage + message timings.
|
|
228
|
+
let sawUserOrAssistant = false;
|
|
229
|
+
await forEachJsonlLine(join(sessionPath, 'updates.jsonl'), (obj) => {
|
|
230
|
+
const update = obj?.params?.update;
|
|
231
|
+
if (!update || typeof update !== 'object') return;
|
|
232
|
+
|
|
233
|
+
const kind = update.sessionUpdate;
|
|
234
|
+
const timestamp = toDate(obj.timestamp);
|
|
235
|
+
|
|
236
|
+
if (kind === 'turn_completed' && timestamp) {
|
|
237
|
+
emitTurnUsage(entries, {
|
|
238
|
+
usage: update.usage,
|
|
239
|
+
project,
|
|
240
|
+
timestamp,
|
|
241
|
+
fallbackModel,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!timestamp) return;
|
|
246
|
+
|
|
247
|
+
if (kind === 'user_message_chunk') {
|
|
248
|
+
sawUserOrAssistant = true;
|
|
249
|
+
sessionEvents.push({
|
|
250
|
+
sessionId,
|
|
251
|
+
source: SOURCE,
|
|
252
|
+
project,
|
|
253
|
+
timestamp,
|
|
254
|
+
role: 'user',
|
|
255
|
+
});
|
|
256
|
+
} else if (kind === 'agent_message_chunk' || kind === 'turn_completed') {
|
|
257
|
+
sawUserOrAssistant = true;
|
|
258
|
+
sessionEvents.push({
|
|
259
|
+
sessionId,
|
|
260
|
+
source: SOURCE,
|
|
261
|
+
project,
|
|
262
|
+
timestamp,
|
|
263
|
+
role: 'assistant',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Fallback timing from events.jsonl when updates lack message chunks
|
|
269
|
+
// (short/aborted sessions, older builds).
|
|
270
|
+
if (!sawUserOrAssistant) {
|
|
271
|
+
await forEachJsonlLine(join(sessionPath, 'events.jsonl'), (obj) => {
|
|
272
|
+
const timestamp = toDate(obj.ts || obj.timestamp);
|
|
273
|
+
if (!timestamp) return;
|
|
274
|
+
if (obj.type === 'turn_started') {
|
|
275
|
+
sessionEvents.push({
|
|
276
|
+
sessionId,
|
|
277
|
+
source: SOURCE,
|
|
278
|
+
project,
|
|
279
|
+
timestamp,
|
|
280
|
+
role: 'user',
|
|
281
|
+
});
|
|
282
|
+
} else if (obj.type === 'turn_ended' || obj.type === 'first_token') {
|
|
283
|
+
sessionEvents.push({
|
|
284
|
+
sessionId,
|
|
285
|
+
source: SOURCE,
|
|
286
|
+
project,
|
|
287
|
+
timestamp,
|
|
288
|
+
role: 'assistant',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Last-resort session envelope from summary timestamps so a session with
|
|
295
|
+
// no parseable turns still appears once usage lands later.
|
|
296
|
+
if (sessionEvents.every((e) => e.sessionId !== sessionId)) {
|
|
297
|
+
const created = toDate(summary.created_at || summary.info?.created_at);
|
|
298
|
+
const updated = toDate(summary.updated_at || summary.last_active_at);
|
|
299
|
+
if (created) {
|
|
300
|
+
sessionEvents.push({
|
|
301
|
+
sessionId,
|
|
302
|
+
source: SOURCE,
|
|
303
|
+
project,
|
|
304
|
+
timestamp: created,
|
|
305
|
+
role: 'user',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (updated && (!created || updated.getTime() !== created.getTime())) {
|
|
309
|
+
sessionEvents.push({
|
|
310
|
+
sessionId,
|
|
311
|
+
source: SOURCE,
|
|
312
|
+
project,
|
|
313
|
+
timestamp: updated,
|
|
314
|
+
role: 'assistant',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
buckets: aggregateToBuckets(entries),
|
|
323
|
+
sessions: extractSessions(sessionEvents),
|
|
324
|
+
};
|
|
325
|
+
}
|
package/src/parsers/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parse as parseCopilotCli } from './copilot-cli.js';
|
|
|
6
6
|
import { parse as parseCursor } from './cursor.js';
|
|
7
7
|
import { parse as parseRooCode } from './roo-code.js';
|
|
8
8
|
import { parse as parseGeminiCli } from './gemini-cli.js';
|
|
9
|
+
import { parse as parseGrok } from './grok.js';
|
|
9
10
|
import { parse as parseOpencode } from './opencode.js';
|
|
10
11
|
import { parse as parseOpenclaw } from './openclaw.js';
|
|
11
12
|
import { parse as parseQwenCode } from './qwen-code.js';
|
|
@@ -21,24 +22,25 @@ import { parse as parseTraeCli } from './trae-cli.js';
|
|
|
21
22
|
|
|
22
23
|
export const parsers = {
|
|
23
24
|
'claude-code': parseClaudeCode,
|
|
24
|
-
'cline': parseCline,
|
|
25
25
|
'codex': parseCodex,
|
|
26
|
+
'grok': parseGrok,
|
|
26
27
|
'copilot-cli': parseCopilotCli,
|
|
27
28
|
'cursor': parseCursor,
|
|
28
|
-
'roo-code': parseRooCode,
|
|
29
29
|
'gemini-cli': parseGeminiCli,
|
|
30
30
|
'opencode': parseOpencode,
|
|
31
31
|
'openclaw': parseOpenclaw,
|
|
32
|
+
'pi-coding-agent': parsePiCodingAgent,
|
|
32
33
|
'qwen-code': parseQwenCode,
|
|
33
34
|
'kimi-code': parseKimiCode,
|
|
34
35
|
'amp': parseAmp,
|
|
35
36
|
'droid': parseDroid,
|
|
36
37
|
'antigravity': parseAntigravity,
|
|
38
|
+
'trae-cli': parseTraeCli,
|
|
37
39
|
'hermes': parseHermes,
|
|
38
40
|
'kiro': parseKiro,
|
|
39
|
-
'
|
|
41
|
+
'cline': parseCline,
|
|
42
|
+
'roo-code': parseRooCode,
|
|
40
43
|
'zcode': parseZcode,
|
|
41
|
-
'trae-cli': parseTraeCli,
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
|
package/src/skill.js
CHANGED
|
@@ -68,7 +68,7 @@ Track and answer questions about the user's AI coding token spend via [Vibe Usag
|
|
|
68
68
|
- \`summary\` 读 \`~/.vibe-usage/config.json\` 里已有的 API key,不需要用户额外输入
|
|
69
69
|
- summary 输出已是 markdown,**原样展示,不要复述**
|
|
70
70
|
- 用户没装过 vibe-usage?提示运行 \`npx @vibe-cafe/vibe-usage\` 先用浏览器登录链接账号
|
|
71
|
-
- 支持的工具:Claude Code, Codex
|
|
71
|
+
- 支持的工具:Claude Code, Codex, Grok 等
|
|
72
72
|
`;
|
|
73
73
|
|
|
74
74
|
export async function runSkill(args = []) {
|
package/src/tools.js
CHANGED
|
@@ -136,37 +136,47 @@ export function findTraeCliDataDirs() {
|
|
|
136
136
|
return [join(xdgCacheHome, 'trae-cli', 'sessions')].filter(existsSync);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** Grok home: GROK_HOME env (same as the Grok CLI) or ~/.grok. */
|
|
140
|
+
export function getGrokHome() {
|
|
141
|
+
const envHome = process.env.GROK_HOME?.trim();
|
|
142
|
+
if (envHome) {
|
|
143
|
+
return envHome.startsWith('~') ? join(homedir(), envHome.slice(1)) : envHome;
|
|
144
|
+
}
|
|
145
|
+
return join(homedir(), '.grok');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getGrokSessionsDir() {
|
|
149
|
+
const testDir = process.env.VIBE_USAGE_GROK_SESSIONS?.trim();
|
|
150
|
+
if (testDir) return testDir;
|
|
151
|
+
return join(getGrokHome(), 'sessions');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Detect Grok when sessions/ exists under GROK_HOME (or the test override).
|
|
155
|
+
export function findGrokDataDirs() {
|
|
156
|
+
const testDir = process.env.VIBE_USAGE_GROK_SESSIONS?.trim();
|
|
157
|
+
if (testDir) return [testDir].filter(existsSync);
|
|
158
|
+
return [join(getGrokHome(), 'sessions')].filter(existsSync);
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
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
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: 'Antigravity',
|
|
148
|
-
id: 'antigravity',
|
|
149
|
-
dataDir: join(homedir(), '.gemini', 'antigravity'),
|
|
150
|
-
detectDataDirs: findAntigravityDataDirs,
|
|
151
|
-
},
|
|
152
162
|
{
|
|
153
163
|
name: 'Claude Code',
|
|
154
164
|
id: 'claude-code',
|
|
155
165
|
dataDir: join(homedir(), '.claude', 'projects'),
|
|
156
166
|
detectDataDirs: findClaudeCodeDataDirs,
|
|
157
167
|
},
|
|
158
|
-
{
|
|
159
|
-
name: 'Cline',
|
|
160
|
-
id: 'cline',
|
|
161
|
-
dataDir: join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
|
|
162
|
-
detectDataDirs: findClineDataDirs,
|
|
163
|
-
},
|
|
164
168
|
{
|
|
165
169
|
name: 'Codex CLI',
|
|
166
170
|
id: 'codex',
|
|
167
171
|
dataDir: join(homedir(), '.codex', 'sessions'),
|
|
168
172
|
detectDataDirs: findCodexDataDirs,
|
|
169
173
|
},
|
|
174
|
+
{
|
|
175
|
+
name: 'Grok',
|
|
176
|
+
id: 'grok',
|
|
177
|
+
dataDir: join(homedir(), '.grok', 'sessions'),
|
|
178
|
+
detectDataDirs: findGrokDataDirs,
|
|
179
|
+
},
|
|
170
180
|
{
|
|
171
181
|
name: 'GitHub Copilot CLI',
|
|
172
182
|
id: 'copilot-cli',
|
|
@@ -221,6 +231,18 @@ export const TOOLS = [
|
|
|
221
231
|
id: 'droid',
|
|
222
232
|
dataDir: join(homedir(), '.factory', 'sessions'),
|
|
223
233
|
},
|
|
234
|
+
{
|
|
235
|
+
name: 'Antigravity',
|
|
236
|
+
id: 'antigravity',
|
|
237
|
+
dataDir: join(homedir(), '.gemini', 'antigravity'),
|
|
238
|
+
detectDataDirs: findAntigravityDataDirs,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'Trae CLI',
|
|
242
|
+
id: 'trae-cli',
|
|
243
|
+
dataDir: join(homedir(), 'Library', 'Caches', 'trae-cli', 'sessions'),
|
|
244
|
+
detectDataDirs: findTraeCliDataDirs,
|
|
245
|
+
},
|
|
224
246
|
{
|
|
225
247
|
name: 'Hermes',
|
|
226
248
|
id: 'hermes',
|
|
@@ -231,6 +253,12 @@ export const TOOLS = [
|
|
|
231
253
|
id: 'kiro',
|
|
232
254
|
dataDir: getKiroAgentPath(),
|
|
233
255
|
},
|
|
256
|
+
{
|
|
257
|
+
name: 'Cline',
|
|
258
|
+
id: 'cline',
|
|
259
|
+
dataDir: join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
|
|
260
|
+
detectDataDirs: findClineDataDirs,
|
|
261
|
+
},
|
|
234
262
|
{
|
|
235
263
|
name: 'Roo Code',
|
|
236
264
|
id: 'roo-code',
|