@vibe-cafe/vibe-usage 0.9.2 → 0.9.4
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 +1 -1
- package/src/parsers/index.js +2 -0
- package/src/parsers/kimi-code.js +206 -36
- package/src/parsers/zcode.js +104 -0
- package/src/tools.js +18 -1
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
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
|
| Antigravity | `~/.gemini/antigravity/conversations/*.pb` to discover cascades, then reads token usage + sessions from the running language server via Connect RPC (process discovered with `ps`/`lsof` on macOS/Linux, PowerShell CIM with a `wmic` fallback on Windows) |
|
|
70
|
+
| ZCode | `~/.zcode/cli/db/db.sqlite` (SQLite; reads the `message` table for per-message tokens, model, and project `cwd`/`root`, joined to `session.directory`) |
|
|
70
71
|
|
|
71
72
|
## How It Works
|
|
72
73
|
|
package/package.json
CHANGED
package/src/parsers/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { parse as parseAntigravity } from './antigravity.js';
|
|
|
16
16
|
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
|
+
import { parse as parseZcode } from './zcode.js';
|
|
19
20
|
|
|
20
21
|
export const parsers = {
|
|
21
22
|
'claude-code': parseClaudeCode,
|
|
@@ -35,6 +36,7 @@ export const parsers = {
|
|
|
35
36
|
'hermes': parseHermes,
|
|
36
37
|
'kiro': parseKiro,
|
|
37
38
|
'pi-coding-agent': parsePiCodingAgent,
|
|
39
|
+
'zcode': parseZcode,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
|
package/src/parsers/kimi-code.js
CHANGED
|
@@ -1,33 +1,209 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Kimi CLI parser (a.k.a. "Kimi Code").
|
|
8
|
+
* Kimi Code CLI parser. MoonshotAI/kimi-cli (a.k.a. "Kimi Code").
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* - First line is a metadata header: {"type":"metadata","protocol_version":"1.9"}
|
|
12
|
-
* - Each subsequent line (1.9): {"timestamp": <float seconds>, "message": {"type", "payload"}}
|
|
13
|
-
* - Legacy 1.1 line: {"type", "payload"} (no message wrapper, ts may live in payload)
|
|
10
|
+
* Two on-disk layouts are supported:
|
|
14
11
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* 1. Current ("~/.kimi-code", protocol >= 1.4). Sessions live at
|
|
13
|
+
* ~/.kimi-code/sessions/wd_<slug>_<hash>/session_<id>/agents/<agentId>/wire.jsonl
|
|
14
|
+
* Each line is a self-describing event with a top-level integer-ms `time`:
|
|
15
|
+
* {"type":"usage.record","model":"kimi-code/kimi-for-coding",
|
|
16
|
+
* "usage":{"inputOther","output","inputCacheRead","inputCacheCreation"},
|
|
17
|
+
* "usageScope":"turn","time":<ms>}
|
|
18
|
+
* `usage.record` events are per-step deltas (one per assistant step), so they
|
|
19
|
+
* sum to the session total. The model name rides on each record — no config
|
|
20
|
+
* lookup needed. User turns are `turn.prompt` with origin.kind === "user".
|
|
21
|
+
* The real working directory per session is recorded in
|
|
22
|
+
* ~/.kimi-code/session_index.jsonl -> {sessionId, sessionDir, workDir}
|
|
23
|
+
* which gives an accurate project name (last path component of workDir).
|
|
17
24
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* 2. Legacy ("~/.kimi", protocol 1.1 / 1.9). Sessions live at
|
|
26
|
+
* ~/.kimi/sessions/<md5(workdir)>/<session-id>/wire.jsonl
|
|
27
|
+
* with a different envelope (StatusUpdate.payload.token_usage, float-second
|
|
28
|
+
* `timestamp`) and the model in ~/.kimi/config.toml. Kept for users who have
|
|
29
|
+
* not migrated; see parseLegacyKimi() below.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Current format: ~/.kimi-code
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const KIMI_CODE_DIR = join(homedir(), '.kimi-code');
|
|
37
|
+
const KIMI_CODE_SESSIONS_DIR = join(KIMI_CODE_DIR, 'sessions');
|
|
38
|
+
const KIMI_CODE_SESSION_INDEX = join(KIMI_CODE_DIR, 'session_index.jsonl');
|
|
39
|
+
|
|
40
|
+
function projectNameFromPath(path) {
|
|
41
|
+
if (typeof path !== 'string' || !path) return null;
|
|
42
|
+
return basename(path.replace(/[/\\]+$/, '')) || path;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Map each session directory (absolute path) to a project name, read from
|
|
47
|
+
* ~/.kimi-code/session_index.jsonl. Falls back gracefully if the file is
|
|
48
|
+
* missing or malformed — callers default to the wd_ bucket name.
|
|
23
49
|
*/
|
|
50
|
+
function loadSessionIndex() {
|
|
51
|
+
const map = new Map();
|
|
52
|
+
if (!existsSync(KIMI_CODE_SESSION_INDEX)) return map;
|
|
53
|
+
|
|
54
|
+
let content;
|
|
55
|
+
try {
|
|
56
|
+
content = readFileSync(KIMI_CODE_SESSION_INDEX, 'utf-8');
|
|
57
|
+
} catch {
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const line of content.split('\n')) {
|
|
62
|
+
if (!line.trim()) continue;
|
|
63
|
+
let entry;
|
|
64
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
65
|
+
const dir = entry?.sessionDir;
|
|
66
|
+
const project = projectNameFromPath(entry?.workDir);
|
|
67
|
+
if (typeof dir === 'string' && dir && project) map.set(dir, project);
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Strip the trailing _<hash> from a "wd_<slug>_<hash>" bucket name so the slug
|
|
73
|
+
// can serve as a last-resort project label when session_index has no entry.
|
|
74
|
+
function projectFromBucketName(name) {
|
|
75
|
+
const m = /^wd_(.+)_[0-9a-f]+$/.exec(name);
|
|
76
|
+
return m ? m[1] : name;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Collect every agents/<id>/wire.jsonl under sessions/wd_<...>/session_<...>/.
|
|
80
|
+
function findKimiCodeWireFiles(baseDir) {
|
|
81
|
+
const results = [];
|
|
82
|
+
if (!existsSync(baseDir)) return results;
|
|
83
|
+
|
|
84
|
+
let workDirs;
|
|
85
|
+
try {
|
|
86
|
+
workDirs = readdirSync(baseDir, { withFileTypes: true });
|
|
87
|
+
} catch {
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const workDir of workDirs) {
|
|
92
|
+
if (!workDir.isDirectory()) continue;
|
|
93
|
+
const workDirPath = join(baseDir, workDir.name);
|
|
94
|
+
const bucketProject = projectFromBucketName(workDir.name);
|
|
95
|
+
|
|
96
|
+
let sessions;
|
|
97
|
+
try {
|
|
98
|
+
sessions = readdirSync(workDirPath, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const session of sessions) {
|
|
104
|
+
if (!session.isDirectory()) continue;
|
|
105
|
+
const sessionDir = join(workDirPath, session.name);
|
|
106
|
+
const agentsDir = join(sessionDir, 'agents');
|
|
107
|
+
|
|
108
|
+
let agents;
|
|
109
|
+
try {
|
|
110
|
+
agents = readdirSync(agentsDir, { withFileTypes: true });
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const agent of agents) {
|
|
116
|
+
if (!agent.isDirectory()) continue;
|
|
117
|
+
const wireFile = join(agentsDir, agent.name, 'wire.jsonl');
|
|
118
|
+
if (existsSync(wireFile)) {
|
|
119
|
+
results.push({ wireFile, sessionDir, bucketProject });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseKimiCode() {
|
|
128
|
+
const wireFiles = findKimiCodeWireFiles(KIMI_CODE_SESSIONS_DIR);
|
|
129
|
+
if (wireFiles.length === 0) return null;
|
|
130
|
+
|
|
131
|
+
const sessionIndex = loadSessionIndex();
|
|
132
|
+
const entries = [];
|
|
133
|
+
const sessionEvents = [];
|
|
134
|
+
|
|
135
|
+
for (const { wireFile, sessionDir, bucketProject } of wireFiles) {
|
|
136
|
+
let content;
|
|
137
|
+
try {
|
|
138
|
+
content = readFileSync(wireFile, 'utf-8');
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const project = sessionIndex.get(sessionDir) || bucketProject || 'unknown';
|
|
144
|
+
|
|
145
|
+
for (const line of content.split('\n')) {
|
|
146
|
+
if (!line.trim()) continue;
|
|
147
|
+
let evt;
|
|
148
|
+
try { evt = JSON.parse(line); } catch { continue; }
|
|
149
|
+
|
|
150
|
+
const type = evt.type;
|
|
151
|
+
// Top-level `time` is integer milliseconds since epoch.
|
|
152
|
+
const time = typeof evt.time === 'number' ? evt.time : null;
|
|
153
|
+
|
|
154
|
+
// Session timing: a user turn vs. anything the model emits.
|
|
155
|
+
if (type === 'turn.prompt' && evt.origin?.kind === 'user' && time) {
|
|
156
|
+
const ts = new Date(time);
|
|
157
|
+
if (!isNaN(ts.getTime())) {
|
|
158
|
+
sessionEvents.push({ sessionId: wireFile, source: 'kimi-code', project, timestamp: ts, role: 'user' });
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (type !== 'usage.record') continue;
|
|
164
|
+
|
|
165
|
+
const usage = evt.usage;
|
|
166
|
+
if (!usage) continue;
|
|
167
|
+
|
|
168
|
+
const inputTokens = usage.inputOther || 0;
|
|
169
|
+
const outputTokens = usage.output || 0;
|
|
170
|
+
const cachedInputTokens = usage.inputCacheRead || 0;
|
|
171
|
+
if (!inputTokens && !outputTokens && !cachedInputTokens) continue;
|
|
172
|
+
|
|
173
|
+
const ts = time ? new Date(time) : new Date();
|
|
174
|
+
|
|
175
|
+
entries.push({
|
|
176
|
+
source: 'kimi-code',
|
|
177
|
+
model: evt.model || 'unknown',
|
|
178
|
+
project,
|
|
179
|
+
timestamp: ts,
|
|
180
|
+
inputTokens,
|
|
181
|
+
outputTokens,
|
|
182
|
+
cachedInputTokens,
|
|
183
|
+
reasoningOutputTokens: 0,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Each usage.record marks an assistant step completing — use it as an
|
|
187
|
+
// assistant timing event so active-time math has both sides of a turn.
|
|
188
|
+
if (time && !isNaN(ts.getTime())) {
|
|
189
|
+
sessionEvents.push({ sessionId: wireFile, source: 'kimi-code', project, timestamp: ts, role: 'assistant' });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Legacy format: ~/.kimi (kept for users who haven't migrated to ~/.kimi-code)
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
24
200
|
|
|
25
201
|
const KIMI_DIR = join(homedir(), '.kimi');
|
|
26
202
|
const KIMI_SESSIONS_DIR = join(KIMI_DIR, 'sessions');
|
|
27
203
|
const KIMI_WORKDIRS_JSON = join(KIMI_DIR, 'kimi.json');
|
|
28
204
|
const KIMI_CONFIG_TOML = join(KIMI_DIR, 'config.toml');
|
|
29
205
|
|
|
30
|
-
function
|
|
206
|
+
function findLegacyWireFiles(baseDir) {
|
|
31
207
|
const results = [];
|
|
32
208
|
if (!existsSync(baseDir)) return results;
|
|
33
209
|
|
|
@@ -54,12 +230,7 @@ function findWireFiles(baseDir) {
|
|
|
54
230
|
return results;
|
|
55
231
|
}
|
|
56
232
|
|
|
57
|
-
function
|
|
58
|
-
const parts = path.split('/').filter(Boolean);
|
|
59
|
-
return parts[parts.length - 1] || path;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function loadProjectMap() {
|
|
233
|
+
function loadLegacyProjectMap() {
|
|
63
234
|
const map = new Map();
|
|
64
235
|
if (!existsSync(KIMI_WORKDIRS_JSON)) return map;
|
|
65
236
|
|
|
@@ -70,7 +241,6 @@ function loadProjectMap() {
|
|
|
70
241
|
return map;
|
|
71
242
|
}
|
|
72
243
|
|
|
73
|
-
// 1.9 schema: { work_dirs: [{ path, kaos, last_session_id }] }
|
|
74
244
|
if (Array.isArray(config.work_dirs)) {
|
|
75
245
|
for (const entry of config.work_dirs) {
|
|
76
246
|
const path = entry?.path;
|
|
@@ -80,7 +250,6 @@ function loadProjectMap() {
|
|
|
80
250
|
}
|
|
81
251
|
}
|
|
82
252
|
|
|
83
|
-
// Legacy schemas keyed by hash
|
|
84
253
|
for (const key of ['workspaces', 'projects']) {
|
|
85
254
|
const obj = config[key];
|
|
86
255
|
if (!obj || typeof obj !== 'object') continue;
|
|
@@ -93,13 +262,10 @@ function loadProjectMap() {
|
|
|
93
262
|
return map;
|
|
94
263
|
}
|
|
95
264
|
|
|
96
|
-
// Matches both bare-key `[models.kimi-for-coding]` and quoted
|
|
97
|
-
// `[models."kimi-code/kimi-for-coding"]` forms (TOML bare keys can't
|
|
98
|
-
// contain `/`, so quoting is mandatory for hierarchical names).
|
|
99
265
|
const TOML_MODEL_SECTION_RE = /^\s*\[models\.(?:"([^"]+)"|([A-Za-z0-9_-]+))\]/m;
|
|
100
266
|
const TOML_DEFAULT_MODEL_RE = /^\s*default_model\s*=\s*["']([^"']+)["']/m;
|
|
101
267
|
|
|
102
|
-
function
|
|
268
|
+
function loadLegacyModelFromConfig() {
|
|
103
269
|
if (!existsSync(KIMI_CONFIG_TOML)) return 'unknown';
|
|
104
270
|
|
|
105
271
|
let content;
|
|
@@ -118,14 +284,14 @@ function loadModelFromConfig() {
|
|
|
118
284
|
return 'unknown';
|
|
119
285
|
}
|
|
120
286
|
|
|
121
|
-
const
|
|
287
|
+
const LEGACY_USER_EVENT_TYPES = new Set(['TurnBegin', 'UserMessage', 'user_message', 'Input']);
|
|
122
288
|
|
|
123
|
-
|
|
124
|
-
const wireFiles =
|
|
289
|
+
function parseLegacyKimi() {
|
|
290
|
+
const wireFiles = findLegacyWireFiles(KIMI_SESSIONS_DIR);
|
|
125
291
|
if (wireFiles.length === 0) return { buckets: [], sessions: [] };
|
|
126
292
|
|
|
127
|
-
const projectMap =
|
|
128
|
-
const defaultModel =
|
|
293
|
+
const projectMap = loadLegacyProjectMap();
|
|
294
|
+
const defaultModel = loadLegacyModelFromConfig();
|
|
129
295
|
const entries = [];
|
|
130
296
|
const sessionEvents = [];
|
|
131
297
|
const seenMessageIds = new Set();
|
|
@@ -147,15 +313,11 @@ export async function parse() {
|
|
|
147
313
|
let raw;
|
|
148
314
|
try { raw = JSON.parse(line); } catch { continue; }
|
|
149
315
|
|
|
150
|
-
// Unwrap 1.9 envelope; fall through to top-level for legacy 1.1.
|
|
151
|
-
// Metadata header line has no payload and is filtered by the next check.
|
|
152
316
|
const envelope = raw.message || raw;
|
|
153
317
|
const type = envelope.type || raw.type;
|
|
154
318
|
const payload = envelope.payload || raw.payload;
|
|
155
319
|
if (!payload) continue;
|
|
156
320
|
|
|
157
|
-
// 1.9 puts timestamp at the outer level (Unix seconds, float).
|
|
158
|
-
// Legacy 1.1 sometimes puts it inside payload.
|
|
159
321
|
if (typeof raw.timestamp === 'number') {
|
|
160
322
|
lastTimestamp = raw.timestamp * 1000;
|
|
161
323
|
} else if (typeof payload.timestamp === 'number') {
|
|
@@ -171,7 +333,7 @@ export async function parse() {
|
|
|
171
333
|
source: 'kimi-code',
|
|
172
334
|
project,
|
|
173
335
|
timestamp: evTs,
|
|
174
|
-
role:
|
|
336
|
+
role: LEGACY_USER_EVENT_TYPES.has(type) ? 'user' : 'assistant',
|
|
175
337
|
});
|
|
176
338
|
}
|
|
177
339
|
}
|
|
@@ -205,3 +367,11 @@ export async function parse() {
|
|
|
205
367
|
|
|
206
368
|
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
207
369
|
}
|
|
370
|
+
|
|
371
|
+
export async function parse() {
|
|
372
|
+
// Prefer the current ~/.kimi-code layout; fall back to legacy ~/.kimi only
|
|
373
|
+
// when no kimi-code sessions exist, so migrated users aren't double-counted.
|
|
374
|
+
const current = parseKimiCode();
|
|
375
|
+
if (current) return current;
|
|
376
|
+
return parseLegacyKimi();
|
|
377
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
import { queryDbJson } from './sqlite.js';
|
|
6
|
+
|
|
7
|
+
// ZCode (z.ai / Zhipu's coding agent) stores everything in a SQLite database
|
|
8
|
+
// at ~/.zcode/cli/db/db.sqlite. The `message` table is the canonical source:
|
|
9
|
+
// each row is one user or assistant message, with an assistant message carrying
|
|
10
|
+
// per-request token usage and the working directory. We read it directly rather
|
|
11
|
+
// than the parallel `model_usage` ledger because `message` gives us BOTH session
|
|
12
|
+
// timing (user + assistant rows) and token usage in one pass, with the project
|
|
13
|
+
// path attached to each message.
|
|
14
|
+
const DB_PATH = join(homedir(), '.zcode', 'cli', 'db', 'db.sqlite');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Project name from a ZCode message's path. ZCode records both `cwd` and `root`;
|
|
18
|
+
* prefer `root` (the workspace root) and fall back to `cwd`, then to the
|
|
19
|
+
* session's `directory` column (joined in via the query) — taking the last path
|
|
20
|
+
* component, matching how every other parser names projects.
|
|
21
|
+
*/
|
|
22
|
+
function projectName(root, cwd, sessionDir) {
|
|
23
|
+
const p = root || cwd || sessionDir;
|
|
24
|
+
if (!p) return 'unknown';
|
|
25
|
+
return basename(String(p).replace(/[/\\]+$/, '')) || 'unknown';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function parse() {
|
|
29
|
+
if (!existsSync(DB_PATH)) return { buckets: [], sessions: [] };
|
|
30
|
+
|
|
31
|
+
// Join each message to its session so we can fall back to the session's
|
|
32
|
+
// directory when an individual message has no path (older rows, lite agents).
|
|
33
|
+
const query = `SELECT
|
|
34
|
+
m.session_id AS sessionId,
|
|
35
|
+
m.time_created AS created,
|
|
36
|
+
json_extract(m.data, '$.role') AS role,
|
|
37
|
+
json_extract(m.data, '$.modelID') AS modelID,
|
|
38
|
+
json_extract(m.data, '$.tokens') AS tokens,
|
|
39
|
+
json_extract(m.data, '$.path.root') AS pathRoot,
|
|
40
|
+
json_extract(m.data, '$.path.cwd') AS pathCwd,
|
|
41
|
+
s.directory AS sessionDir
|
|
42
|
+
FROM message m
|
|
43
|
+
LEFT JOIN session s ON s.id = m.session_id`;
|
|
44
|
+
|
|
45
|
+
let rows;
|
|
46
|
+
try {
|
|
47
|
+
rows = queryDbJson(DB_PATH, query);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.status === 127 || (err.message && err.message.includes('ENOENT'))) {
|
|
50
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync ZCode data.');
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
if (!rows.length) return { buckets: [], sessions: [] };
|
|
55
|
+
|
|
56
|
+
const entries = [];
|
|
57
|
+
const sessionEvents = [];
|
|
58
|
+
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
const timestamp = new Date(row.created);
|
|
61
|
+
if (isNaN(timestamp.getTime())) continue;
|
|
62
|
+
|
|
63
|
+
const project = projectName(row.pathRoot, row.pathCwd, row.sessionDir);
|
|
64
|
+
const sessionId = row.sessionId || 'unknown';
|
|
65
|
+
|
|
66
|
+
sessionEvents.push({
|
|
67
|
+
sessionId,
|
|
68
|
+
source: 'zcode',
|
|
69
|
+
project,
|
|
70
|
+
timestamp,
|
|
71
|
+
role: row.role === 'user' ? 'user' : 'assistant',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (row.role !== 'assistant') continue;
|
|
75
|
+
|
|
76
|
+
let tokens;
|
|
77
|
+
try {
|
|
78
|
+
tokens = typeof row.tokens === 'string' ? JSON.parse(row.tokens) : row.tokens;
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!tokens || (!tokens.input && !tokens.output)) continue;
|
|
83
|
+
|
|
84
|
+
// ZCode follows Anthropic-style usage where `input` INCLUDES the cache-read
|
|
85
|
+
// tokens and `output` INCLUDES reasoning (verified: input + output == total).
|
|
86
|
+
// Normalize to this codebase's non-overlapping fields so cached/reasoning
|
|
87
|
+
// tokens aren't double-counted inside input/output.
|
|
88
|
+
const cachedInput = tokens.cache?.read || 0;
|
|
89
|
+
const reasoning = tokens.reasoning || 0;
|
|
90
|
+
|
|
91
|
+
entries.push({
|
|
92
|
+
source: 'zcode',
|
|
93
|
+
model: row.modelID || 'unknown',
|
|
94
|
+
project,
|
|
95
|
+
timestamp,
|
|
96
|
+
inputTokens: (tokens.input || 0) - cachedInput,
|
|
97
|
+
outputTokens: (tokens.output || 0) - reasoning,
|
|
98
|
+
cachedInputTokens: cachedInput,
|
|
99
|
+
reasoningOutputTokens: reasoning,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
104
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -104,6 +104,15 @@ function findCodexDataDirs() {
|
|
|
104
104
|
].filter(existsSync);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Kimi Code moved its store from ~/.kimi to ~/.kimi-code; recognize either so
|
|
108
|
+
// users on either version are detected. The parser prefers ~/.kimi-code.
|
|
109
|
+
function findKimiCodeDataDirs() {
|
|
110
|
+
return [
|
|
111
|
+
join(homedir(), '.kimi-code', 'sessions'),
|
|
112
|
+
join(homedir(), '.kimi', 'sessions'),
|
|
113
|
+
].filter(existsSync);
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
export const TOOLS = [
|
|
108
117
|
{
|
|
109
118
|
name: 'Antigravity',
|
|
@@ -167,7 +176,10 @@ export const TOOLS = [
|
|
|
167
176
|
{
|
|
168
177
|
name: 'Kimi Code',
|
|
169
178
|
id: 'kimi-code',
|
|
170
|
-
|
|
179
|
+
// Current layout is ~/.kimi-code/sessions; ~/.kimi/sessions is the legacy
|
|
180
|
+
// path. The parser reads whichever exists (preferring ~/.kimi-code).
|
|
181
|
+
dataDir: join(homedir(), '.kimi-code', 'sessions'),
|
|
182
|
+
detectDataDirs: findKimiCodeDataDirs,
|
|
171
183
|
},
|
|
172
184
|
{
|
|
173
185
|
name: 'Amp',
|
|
@@ -195,6 +207,11 @@ export const TOOLS = [
|
|
|
195
207
|
dataDir: join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline'),
|
|
196
208
|
detectDataDirs: findRooCodeDataDirs,
|
|
197
209
|
},
|
|
210
|
+
{
|
|
211
|
+
name: 'ZCode',
|
|
212
|
+
id: 'zcode',
|
|
213
|
+
dataDir: join(homedir(), '.zcode', 'cli', 'db', 'db.sqlite'),
|
|
214
|
+
},
|
|
198
215
|
];
|
|
199
216
|
|
|
200
217
|
export function detectInstalledTools() {
|