@vibe-cafe/vibe-usage 0.8.1 → 0.8.3
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 -2
- package/package.json +1 -1
- package/src/parsers/claude-code.js +106 -34
- package/src/parsers/gemini-cli.js +147 -57
- package/src/tools.js +15 -0
package/README.md
CHANGED
|
@@ -46,11 +46,11 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
46
46
|
|
|
47
47
|
| Tool | Data Location |
|
|
48
48
|
|------|---------------|
|
|
49
|
-
| Claude Code | `~/.claude/projects/` (tokens + sessions), `~/.claude/transcripts/` (sessions only) |
|
|
49
|
+
| 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 |
|
|
50
50
|
| Codex CLI | `~/.codex/sessions/` and `~/.codex/archived_sessions/` |
|
|
51
51
|
| GitHub Copilot CLI | `~/.copilot/session-state/*/events.jsonl` |
|
|
52
52
|
| 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 |
|
|
53
|
-
| Gemini CLI | `~/.gemini/tmp
|
|
53
|
+
| Gemini CLI | `~/.gemini/tmp/<project_hash>/chats/session-*.jsonl` (current line-delimited format) and legacy `session-*.json`; recurses into nested subagent sessions |
|
|
54
54
|
| OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
|
|
55
55
|
| OpenClaw | `~/.openclaw/agents/`, `~/.openclaw-<profile>/agents/` (profile deployments) |
|
|
56
56
|
| pi | `~/.pi/agent/sessions/` |
|
package/package.json
CHANGED
|
@@ -1,18 +1,54 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
1
|
+
import { readdirSync, readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { join, basename, sep } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Stateless Claude Code parser.
|
|
8
|
-
* Reads ALL *.jsonl files under
|
|
8
|
+
* Reads ALL *.jsonl files under <root>/projects/ and extracts per-message
|
|
9
9
|
* token usage from assistant messages. No state file needed — every sync
|
|
10
10
|
* computes the full bucket totals from raw data, making server-side
|
|
11
11
|
* ON CONFLICT ... DO UPDATE SET idempotent.
|
|
12
|
+
*
|
|
13
|
+
* Roots: always ~/.claude, plus $CLAUDE_CONFIG_DIR when set to a different
|
|
14
|
+
* path. Claude Code itself relocates its whole tree (incl. projects/) to
|
|
15
|
+
* $CLAUDE_CONFIG_DIR and uses only that dir — but a GUI launched from the
|
|
16
|
+
* Dock may not inherit the shell's env, so usage can be split across both
|
|
17
|
+
* roots. We scan both and dedup so neither source is missed or double-counted.
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the set of Claude config roots to scan.
|
|
22
|
+
* Always includes ~/.claude; adds $CLAUDE_CONFIG_DIR when set and it resolves
|
|
23
|
+
* to a different real path. Deduped by canonical path.
|
|
24
|
+
*/
|
|
25
|
+
function getClaudeRoots() {
|
|
26
|
+
const roots = [join(homedir(), '.claude')];
|
|
27
|
+
|
|
28
|
+
const cfg = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
29
|
+
if (cfg) {
|
|
30
|
+
let custom = cfg;
|
|
31
|
+
if (custom.startsWith('~')) custom = join(homedir(), custom.slice(1));
|
|
32
|
+
custom = custom.replace(/[/\\]+$/, '') || custom;
|
|
33
|
+
roots.push(custom);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Dedup by canonical path (realpath when the dir exists, else the raw string).
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const unique = [];
|
|
39
|
+
for (const r of roots) {
|
|
40
|
+
let key = r;
|
|
41
|
+
try {
|
|
42
|
+
key = realpathSync(r);
|
|
43
|
+
} catch {
|
|
44
|
+
// dir may not exist yet — fall back to the literal path
|
|
45
|
+
}
|
|
46
|
+
if (seen.has(key)) continue;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
unique.push(r);
|
|
49
|
+
}
|
|
50
|
+
return unique;
|
|
51
|
+
}
|
|
16
52
|
|
|
17
53
|
/**
|
|
18
54
|
* Recursively find all .jsonl files under a directory.
|
|
@@ -39,15 +75,22 @@ function findJsonlFiles(dir) {
|
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
78
|
+
* Path of a project file relative to its root's projects/ dir, e.g.
|
|
79
|
+
* "<root>/projects/-Users-foo-app/abc.jsonl" → "-Users-foo-app/abc.jsonl".
|
|
80
|
+
* Used both for project-name extraction and cross-root dedup.
|
|
81
|
+
*/
|
|
82
|
+
function projectRelativePath(filePath, projectsDir) {
|
|
83
|
+
const prefix = projectsDir + sep;
|
|
84
|
+
return filePath.startsWith(prefix) ? filePath.slice(prefix.length) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract project name from a projects-relative path.
|
|
89
|
+
* The first segment is the dash-encoded project path (e.g. -Users-foo-myproject);
|
|
90
|
+
* we take its last component as the project name.
|
|
46
91
|
*/
|
|
47
|
-
function extractProject(
|
|
48
|
-
|
|
49
|
-
if (!filePath.startsWith(projectsPrefix)) return 'unknown';
|
|
50
|
-
const relative = filePath.slice(projectsPrefix.length);
|
|
92
|
+
function extractProject(relative) {
|
|
93
|
+
if (!relative) return 'unknown';
|
|
51
94
|
const firstSeg = relative.split(sep)[0];
|
|
52
95
|
if (!firstSeg) return 'unknown';
|
|
53
96
|
const parts = firstSeg.split('-').filter(Boolean);
|
|
@@ -58,16 +101,21 @@ function extractSessionId(filePath) {
|
|
|
58
101
|
return basename(filePath, '.jsonl');
|
|
59
102
|
}
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Scan one root's projects/ dir → token entries + session events (mutates ctx).
|
|
106
|
+
*/
|
|
107
|
+
function scanProjectsRoot(root, ctx) {
|
|
108
|
+
const projectsDir = join(root, 'projects');
|
|
109
|
+
|
|
110
|
+
for (const filePath of findJsonlFiles(projectsDir)) {
|
|
111
|
+
const relative = projectRelativePath(filePath, projectsDir);
|
|
112
|
+
// Same session present under two roots (e.g. data copied between them):
|
|
113
|
+
// process it once so session message counts aren't inflated.
|
|
114
|
+
if (relative !== null) {
|
|
115
|
+
if (ctx.seenProjectFiles.has(relative)) continue;
|
|
116
|
+
ctx.seenProjectFiles.add(relative);
|
|
117
|
+
}
|
|
69
118
|
|
|
70
|
-
for (const filePath of projectFiles) {
|
|
71
119
|
let content;
|
|
72
120
|
try {
|
|
73
121
|
content = readFileSync(filePath, 'utf-8');
|
|
@@ -75,9 +123,9 @@ export async function parse() {
|
|
|
75
123
|
continue;
|
|
76
124
|
}
|
|
77
125
|
|
|
78
|
-
const project = extractProject(
|
|
126
|
+
const project = extractProject(relative);
|
|
79
127
|
const sessionId = extractSessionId(filePath);
|
|
80
|
-
seenSessionIds.add(sessionId);
|
|
128
|
+
ctx.seenSessionIds.add(sessionId);
|
|
81
129
|
|
|
82
130
|
for (const line of content.split('\n')) {
|
|
83
131
|
if (!line.trim()) continue;
|
|
@@ -90,7 +138,7 @@ export async function parse() {
|
|
|
90
138
|
if (isNaN(ts.getTime())) continue;
|
|
91
139
|
|
|
92
140
|
if (obj.type === 'user' || obj.type === 'assistant' || obj.type === 'tool_use' || obj.type === 'tool_result') {
|
|
93
|
-
sessionEvents.push({
|
|
141
|
+
ctx.sessionEvents.push({
|
|
94
142
|
sessionId,
|
|
95
143
|
source: 'claude-code',
|
|
96
144
|
project,
|
|
@@ -108,11 +156,11 @@ export async function parse() {
|
|
|
108
156
|
|
|
109
157
|
const uuid = obj.uuid;
|
|
110
158
|
if (uuid) {
|
|
111
|
-
if (seenUuids.has(uuid)) continue;
|
|
112
|
-
seenUuids.add(uuid);
|
|
159
|
+
if (ctx.seenUuids.has(uuid)) continue;
|
|
160
|
+
ctx.seenUuids.add(uuid);
|
|
113
161
|
}
|
|
114
162
|
|
|
115
|
-
entries.push({
|
|
163
|
+
ctx.entries.push({
|
|
116
164
|
source: 'claude-code',
|
|
117
165
|
model: msg.model || 'unknown',
|
|
118
166
|
project,
|
|
@@ -127,13 +175,17 @@ export async function parse() {
|
|
|
127
175
|
}
|
|
128
176
|
}
|
|
129
177
|
}
|
|
178
|
+
}
|
|
130
179
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Scan one root's transcripts/ dir → session events only (no token data).
|
|
182
|
+
* Skips sessions already covered by a projects/ or transcripts/ scan.
|
|
183
|
+
*/
|
|
184
|
+
function scanTranscriptsRoot(root, ctx) {
|
|
185
|
+
for (const filePath of findJsonlFiles(join(root, 'transcripts'))) {
|
|
135
186
|
const sessionId = extractSessionId(filePath);
|
|
136
|
-
if (seenSessionIds.has(sessionId)) continue;
|
|
187
|
+
if (ctx.seenSessionIds.has(sessionId)) continue;
|
|
188
|
+
ctx.seenSessionIds.add(sessionId);
|
|
137
189
|
|
|
138
190
|
let content;
|
|
139
191
|
try {
|
|
@@ -153,7 +205,7 @@ export async function parse() {
|
|
|
153
205
|
if (isNaN(ts.getTime())) continue;
|
|
154
206
|
|
|
155
207
|
if (obj.type === 'user' || obj.type === 'assistant' || obj.type === 'tool_use' || obj.type === 'tool_result') {
|
|
156
|
-
sessionEvents.push({
|
|
208
|
+
ctx.sessionEvents.push({
|
|
157
209
|
sessionId,
|
|
158
210
|
source: 'claude-code',
|
|
159
211
|
project: 'unknown',
|
|
@@ -166,6 +218,26 @@ export async function parse() {
|
|
|
166
218
|
}
|
|
167
219
|
}
|
|
168
220
|
}
|
|
221
|
+
}
|
|
169
222
|
|
|
170
|
-
|
|
223
|
+
export async function parse() {
|
|
224
|
+
const ctx = {
|
|
225
|
+
entries: [],
|
|
226
|
+
sessionEvents: [],
|
|
227
|
+
seenUuids: new Set(),
|
|
228
|
+
seenSessionIds: new Set(),
|
|
229
|
+
seenProjectFiles: new Set(), // projects-relative path → dedup same session across roots
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const roots = getClaudeRoots();
|
|
233
|
+
|
|
234
|
+
// projects/ yields BOTH token buckets and session events.
|
|
235
|
+
for (const root of roots) scanProjectsRoot(root, ctx);
|
|
236
|
+
// transcripts/ yields session events only, for sessions not already covered.
|
|
237
|
+
for (const root of roots) scanTranscriptsRoot(root, ctx);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
buckets: aggregateToBuckets(ctx.entries),
|
|
241
|
+
sessions: extractSessions(ctx.sessionEvents),
|
|
242
|
+
};
|
|
171
243
|
}
|
|
@@ -1,33 +1,145 @@
|
|
|
1
|
-
import { readdirSync, readFileSync,
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
5
|
|
|
6
6
|
const TMP_DIR = join(homedir(), '.gemini', 'tmp');
|
|
7
7
|
|
|
8
|
+
// Gemini CLI session storage:
|
|
9
|
+
// ~/.gemini/tmp/<project_hash>/chats/session-<ts>-<id>.jsonl (current, v0.39+)
|
|
10
|
+
// ~/.gemini/tmp/<project_hash>/chats/session-<ts>-<id>.json (legacy, single JSON object)
|
|
11
|
+
// ~/.gemini/tmp/<project_hash>/chats/<parent_id>/<sub_id>.jsonl (subagent sessions, nested)
|
|
12
|
+
// The .jsonl migration (PR #23749, ~v0.39.0) made the old .json-only glob miss every new
|
|
13
|
+
// session — collect both extensions, and recurse one level for nested subagent files.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Walk each project's chats/ directory and collect every session file
|
|
17
|
+
* (both .json and .jsonl), descending into subagent subdirectories.
|
|
18
|
+
*/
|
|
8
19
|
function findSessionFiles(baseDir) {
|
|
9
20
|
const results = [];
|
|
10
21
|
if (!existsSync(baseDir)) return results;
|
|
11
22
|
|
|
23
|
+
let projectDirs;
|
|
24
|
+
try {
|
|
25
|
+
projectDirs = readdirSync(baseDir, { withFileTypes: true });
|
|
26
|
+
} catch {
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const entry of projectDirs) {
|
|
31
|
+
if (!entry.isDirectory()) continue;
|
|
32
|
+
collectChatFiles(join(baseDir, entry.name, 'chats'), results, 0);
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectChatFiles(dir, out, depth) {
|
|
38
|
+
if (depth > 2) return; // chats/ + nested subagent dirs is as deep as it goes
|
|
39
|
+
let entries;
|
|
12
40
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
41
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const e of entries) {
|
|
46
|
+
const full = join(dir, e.name);
|
|
47
|
+
if (e.isDirectory()) {
|
|
48
|
+
collectChatFiles(full, out, depth + 1);
|
|
49
|
+
} else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
50
|
+
out.push(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read a session file into a uniform { messages, directories } shape.
|
|
57
|
+
* .jsonl: line 1 is session metadata, each following line is one record.
|
|
58
|
+
* .json: a single ConversationRecord object with a messages[] array.
|
|
59
|
+
*/
|
|
60
|
+
function readRecords(filePath) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (filePath.endsWith('.jsonl')) {
|
|
69
|
+
const messages = [];
|
|
70
|
+
let directories = null;
|
|
71
|
+
for (const line of raw.split('\n')) {
|
|
72
|
+
const trimmed = line.trim();
|
|
73
|
+
if (!trimmed) continue;
|
|
74
|
+
let obj;
|
|
17
75
|
try {
|
|
18
|
-
|
|
19
|
-
if (f.startsWith('session-') && f.endsWith('.json')) {
|
|
20
|
-
results.push(join(chatsDir, f));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
76
|
+
obj = JSON.parse(trimmed);
|
|
23
77
|
} catch {
|
|
24
78
|
continue;
|
|
25
79
|
}
|
|
80
|
+
// The metadata line carries directories; message lines carry a `type`.
|
|
81
|
+
if (!directories && Array.isArray(obj.directories)) directories = obj.directories;
|
|
82
|
+
if (typeof obj.type === 'string' || typeof obj.role === 'string') messages.push(obj);
|
|
26
83
|
}
|
|
84
|
+
return { messages, directories };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let data;
|
|
88
|
+
try {
|
|
89
|
+
data = JSON.parse(raw);
|
|
27
90
|
} catch {
|
|
28
|
-
return
|
|
91
|
+
return null;
|
|
29
92
|
}
|
|
30
|
-
return
|
|
93
|
+
return {
|
|
94
|
+
messages: data.messages || data.history || [],
|
|
95
|
+
directories: Array.isArray(data.directories) ? data.directories : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Model/assistant messages are recorded as type 'gemini'; user turns as 'user'.
|
|
100
|
+
// info/error/warning are system noise and skipped. `role` is accepted as a
|
|
101
|
+
// fallback for any older format that used it.
|
|
102
|
+
function classifyRole(msg) {
|
|
103
|
+
const t = msg.type ?? msg.role;
|
|
104
|
+
if (t === 'user') return 'user';
|
|
105
|
+
if (t === 'gemini' || t === 'model' || t === 'assistant') return 'assistant';
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Tokens live in msg.tokens.{input,output,cached,thoughts} (TokensSummary, where
|
|
110
|
+
// `input` already includes cached). Fall back to the raw Gemini API usageMetadata
|
|
111
|
+
// shape for any legacy record that stored it.
|
|
112
|
+
function extractTokens(msg) {
|
|
113
|
+
const t = msg.tokens;
|
|
114
|
+
if (t) {
|
|
115
|
+
const cached = t.cached || 0;
|
|
116
|
+
const thoughts = t.thoughts || 0;
|
|
117
|
+
return {
|
|
118
|
+
inputTokens: (t.input || 0) - cached,
|
|
119
|
+
outputTokens: (t.output || 0) - thoughts,
|
|
120
|
+
cachedInputTokens: cached,
|
|
121
|
+
reasoningOutputTokens: thoughts,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const u = msg.usageMetadata || msg.usage;
|
|
125
|
+
if (u) {
|
|
126
|
+
const cached = u.cachedContentTokenCount || 0;
|
|
127
|
+
const thoughts = u.thoughtsTokenCount || 0;
|
|
128
|
+
return {
|
|
129
|
+
inputTokens: (u.promptTokenCount || u.input_tokens || 0) - cached,
|
|
130
|
+
outputTokens: (u.candidatesTokenCount || u.output_tokens || 0) - thoughts,
|
|
131
|
+
cachedInputTokens: cached,
|
|
132
|
+
reasoningOutputTokens: thoughts,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function projectFromDirectories(directories) {
|
|
139
|
+
if (!directories || directories.length === 0) return 'unknown';
|
|
140
|
+
const first = directories[0];
|
|
141
|
+
if (!first) return 'unknown';
|
|
142
|
+
return basename(String(first).replace(/[\\/]+$/, '')) || 'unknown';
|
|
31
143
|
}
|
|
32
144
|
|
|
33
145
|
export async function parse() {
|
|
@@ -38,61 +150,39 @@ export async function parse() {
|
|
|
38
150
|
const sessionEvents = [];
|
|
39
151
|
|
|
40
152
|
for (const filePath of sessionFiles) {
|
|
153
|
+
const record = readRecords(filePath);
|
|
154
|
+
if (!record) continue;
|
|
41
155
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
156
|
+
const project = projectFromDirectories(record.directories);
|
|
157
|
+
|
|
158
|
+
for (const msg of record.messages) {
|
|
159
|
+
const role = classifyRole(msg);
|
|
160
|
+
if (!role) continue;
|
|
48
161
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
if (!timestamp) continue;
|
|
53
|
-
const ts = new Date(timestamp);
|
|
162
|
+
const stamp = msg.timestamp || msg.createTime;
|
|
163
|
+
if (!stamp) continue;
|
|
164
|
+
const ts = new Date(stamp);
|
|
54
165
|
if (isNaN(ts.getTime())) continue;
|
|
55
166
|
|
|
56
|
-
const role = (msg.role === 'user') ? 'user' : 'assistant';
|
|
57
167
|
sessionEvents.push({
|
|
58
168
|
sessionId: filePath,
|
|
59
169
|
source: 'gemini-cli',
|
|
60
|
-
project
|
|
170
|
+
project,
|
|
61
171
|
timestamp: ts,
|
|
62
172
|
role,
|
|
63
173
|
});
|
|
64
174
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
if (!tokens
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
timestamp: ts,
|
|
77
|
-
inputTokens: (tokens.input || 0) - cached,
|
|
78
|
-
outputTokens: (tokens.output || 0) - thoughts,
|
|
79
|
-
cachedInputTokens: cached,
|
|
80
|
-
reasoningOutputTokens: thoughts,
|
|
81
|
-
});
|
|
82
|
-
} else {
|
|
83
|
-
const cached = usage.cachedContentTokenCount || 0;
|
|
84
|
-
const thoughts = usage.thoughtsTokenCount || 0;
|
|
85
|
-
entries.push({
|
|
86
|
-
source: 'gemini-cli',
|
|
87
|
-
model: msg.model || data.model || 'unknown',
|
|
88
|
-
project: 'unknown',
|
|
89
|
-
timestamp: ts,
|
|
90
|
-
inputTokens: (usage.promptTokenCount || usage.input_tokens || 0) - cached,
|
|
91
|
-
outputTokens: (usage.candidatesTokenCount || usage.output_tokens || 0) - thoughts,
|
|
92
|
-
cachedInputTokens: cached,
|
|
93
|
-
reasoningOutputTokens: thoughts,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
175
|
+
if (role !== 'assistant') continue;
|
|
176
|
+
const tokens = extractTokens(msg);
|
|
177
|
+
if (!tokens) continue;
|
|
178
|
+
|
|
179
|
+
entries.push({
|
|
180
|
+
source: 'gemini-cli',
|
|
181
|
+
model: msg.model || 'unknown',
|
|
182
|
+
project,
|
|
183
|
+
timestamp: ts,
|
|
184
|
+
...tokens,
|
|
185
|
+
});
|
|
96
186
|
}
|
|
97
187
|
}
|
|
98
188
|
|
package/src/tools.js
CHANGED
|
@@ -80,6 +80,20 @@ function findOpenclawDataDirs() {
|
|
|
80
80
|
return dirs;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Claude Code lives in ~/.claude/projects, but $CLAUDE_CONFIG_DIR relocates its
|
|
84
|
+
// whole tree. Detect either so a user who only set CLAUDE_CONFIG_DIR is still
|
|
85
|
+
// recognized (the parser scans both roots; see parsers/claude-code.js).
|
|
86
|
+
function findClaudeCodeDataDirs() {
|
|
87
|
+
const dirs = [join(homedir(), '.claude', 'projects')];
|
|
88
|
+
const cfg = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
89
|
+
if (cfg) {
|
|
90
|
+
let custom = cfg.startsWith('~') ? join(homedir(), cfg.slice(1)) : cfg;
|
|
91
|
+
custom = custom.replace(/[/\\]+$/, '') || custom;
|
|
92
|
+
dirs.push(join(custom, 'projects'));
|
|
93
|
+
}
|
|
94
|
+
return dirs.filter(existsSync);
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
// Codex keeps live sessions in ~/.codex/sessions and moves completed ones to
|
|
84
98
|
// ~/.codex/archived_sessions. Detect Codex if either dir exists, so a user
|
|
85
99
|
// whose sessions have all been archived is still recognized.
|
|
@@ -100,6 +114,7 @@ export const TOOLS = [
|
|
|
100
114
|
name: 'Claude Code',
|
|
101
115
|
id: 'claude-code',
|
|
102
116
|
dataDir: join(homedir(), '.claude', 'projects'),
|
|
117
|
+
detectDataDirs: findClaudeCodeDataDirs,
|
|
103
118
|
},
|
|
104
119
|
{
|
|
105
120
|
name: 'Cline',
|