@vibe-cafe/vibe-usage 0.9.3 → 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/package.json +1 -1
- package/src/parsers/kimi-code.js +206 -36
- package/src/tools.js +13 -1
package/package.json
CHANGED
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
|
+
}
|
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',
|