@vibe-cafe/vibe-usage 0.7.13 → 0.7.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 -1
- package/package.json +1 -1
- package/src/parsers/kimi-code.js +130 -63
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
55
55
|
| OpenClaw | `~/.openclaw/agents/`, `~/.openclaw-<profile>/agents/` (profile deployments) |
|
|
56
56
|
| pi | `~/.pi/agent/sessions/` |
|
|
57
57
|
| Qwen Code | `~/.qwen/tmp/` |
|
|
58
|
-
| Kimi Code | `~/.kimi/sessions
|
|
58
|
+
| Kimi Code | `~/.kimi/sessions/<md5(workdir)>/<session-id>/wire.jsonl` (wire protocol 1.9, model from `~/.kimi/config.toml`, project from `~/.kimi/kimi.json`) |
|
|
59
59
|
| Amp | `~/.local/share/amp/threads/` |
|
|
60
60
|
| Droid | `~/.factory/sessions/` |
|
|
61
61
|
| Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
|
package/package.json
CHANGED
package/src/parsers/kimi-code.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Kimi
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Kimi CLI parser (a.k.a. "Kimi Code"). MoonshotAI/kimi-cli.
|
|
9
|
+
*
|
|
10
|
+
* Wire protocol JSONL at ~/.kimi/sessions/<md5(workdir)>/<session-id>/wire.jsonl
|
|
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)
|
|
14
|
+
*
|
|
15
|
+
* Token data: StatusUpdate.payload.token_usage
|
|
16
|
+
* = {input_other, output, input_cache_read, input_cache_creation}
|
|
17
|
+
*
|
|
18
|
+
* Model name is NOT present on StatusUpdate events; we read it from
|
|
19
|
+
* ~/.kimi/config.toml (default_model, falling back to first [models.X] table).
|
|
20
|
+
*
|
|
21
|
+
* Project name comes from ~/.kimi/kimi.json -> work_dirs[].path; the dir name
|
|
22
|
+
* under sessions/ is md5(path).
|
|
11
23
|
*/
|
|
12
24
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
25
|
+
const KIMI_DIR = join(homedir(), '.kimi');
|
|
26
|
+
const KIMI_SESSIONS_DIR = join(KIMI_DIR, 'sessions');
|
|
27
|
+
const KIMI_WORKDIRS_JSON = join(KIMI_DIR, 'kimi.json');
|
|
28
|
+
const KIMI_CONFIG_TOML = join(KIMI_DIR, 'config.toml');
|
|
15
29
|
|
|
16
30
|
function findWireFiles(baseDir) {
|
|
17
31
|
const results = [];
|
|
@@ -40,31 +54,78 @@ function findWireFiles(baseDir) {
|
|
|
40
54
|
return results;
|
|
41
55
|
}
|
|
42
56
|
|
|
57
|
+
function projectNameFromPath(path) {
|
|
58
|
+
const parts = path.split('/').filter(Boolean);
|
|
59
|
+
return parts[parts.length - 1] || path;
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
function loadProjectMap() {
|
|
44
63
|
const map = new Map();
|
|
45
|
-
if (!existsSync(
|
|
64
|
+
if (!existsSync(KIMI_WORKDIRS_JSON)) return map;
|
|
46
65
|
|
|
66
|
+
let config;
|
|
47
67
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
config = JSON.parse(readFileSync(KIMI_WORKDIRS_JSON, 'utf-8'));
|
|
69
|
+
} catch {
|
|
70
|
+
return map;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 1.9 schema: { work_dirs: [{ path, kaos, last_session_id }] }
|
|
74
|
+
if (Array.isArray(config.work_dirs)) {
|
|
75
|
+
for (const entry of config.work_dirs) {
|
|
76
|
+
const path = entry?.path;
|
|
77
|
+
if (typeof path !== 'string' || !path) continue;
|
|
78
|
+
const hash = createHash('md5').update(path).digest('hex');
|
|
79
|
+
map.set(hash, projectNameFromPath(path));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Legacy schemas keyed by hash
|
|
84
|
+
for (const key of ['workspaces', 'projects']) {
|
|
85
|
+
const obj = config[key];
|
|
86
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
87
|
+
for (const [hash, info] of Object.entries(obj)) {
|
|
51
88
|
const path = typeof info === 'string' ? info : (info?.path || info?.dir);
|
|
52
|
-
if (path)
|
|
53
|
-
const parts = path.split('/').filter(Boolean);
|
|
54
|
-
map.set(hash, parts[parts.length - 1] || hash);
|
|
55
|
-
}
|
|
89
|
+
if (typeof path === 'string' && path) map.set(hash, projectNameFromPath(path));
|
|
56
90
|
}
|
|
57
|
-
} catch {
|
|
58
|
-
// config unreadable
|
|
59
91
|
}
|
|
92
|
+
|
|
60
93
|
return map;
|
|
61
94
|
}
|
|
62
95
|
|
|
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
|
+
const TOML_MODEL_SECTION_RE = /^\s*\[models\.(?:"([^"]+)"|([A-Za-z0-9_-]+))\]/m;
|
|
100
|
+
const TOML_DEFAULT_MODEL_RE = /^\s*default_model\s*=\s*["']([^"']+)["']/m;
|
|
101
|
+
|
|
102
|
+
function loadModelFromConfig() {
|
|
103
|
+
if (!existsSync(KIMI_CONFIG_TOML)) return 'unknown';
|
|
104
|
+
|
|
105
|
+
let content;
|
|
106
|
+
try {
|
|
107
|
+
content = readFileSync(KIMI_CONFIG_TOML, 'utf-8');
|
|
108
|
+
} catch {
|
|
109
|
+
return 'unknown';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const defaultMatch = content.match(TOML_DEFAULT_MODEL_RE);
|
|
113
|
+
if (defaultMatch) return defaultMatch[1];
|
|
114
|
+
|
|
115
|
+
const sectionMatch = content.match(TOML_MODEL_SECTION_RE);
|
|
116
|
+
if (sectionMatch) return sectionMatch[1] || sectionMatch[2];
|
|
117
|
+
|
|
118
|
+
return 'unknown';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const USER_EVENT_TYPES = new Set(['TurnBegin', 'UserMessage', 'user_message', 'Input']);
|
|
122
|
+
|
|
63
123
|
export async function parse() {
|
|
64
124
|
const wireFiles = findWireFiles(KIMI_SESSIONS_DIR);
|
|
65
125
|
if (wireFiles.length === 0) return { buckets: [], sessions: [] };
|
|
66
126
|
|
|
67
127
|
const projectMap = loadProjectMap();
|
|
128
|
+
const defaultModel = loadModelFromConfig();
|
|
68
129
|
const entries = [];
|
|
69
130
|
const sessionEvents = [];
|
|
70
131
|
const seenMessageIds = new Set();
|
|
@@ -78,61 +139,67 @@ export async function parse() {
|
|
|
78
139
|
}
|
|
79
140
|
|
|
80
141
|
const project = projectMap.get(workDirHash) || workDirHash;
|
|
81
|
-
let currentModel =
|
|
142
|
+
let currentModel = defaultModel;
|
|
82
143
|
let lastTimestamp = null;
|
|
83
144
|
|
|
84
145
|
for (const line of content.split('\n')) {
|
|
85
146
|
if (!line.trim()) continue;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
147
|
+
let raw;
|
|
148
|
+
try { raw = JSON.parse(line); } catch { continue; }
|
|
149
|
+
|
|
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
|
+
const envelope = raw.message || raw;
|
|
153
|
+
const type = envelope.type || raw.type;
|
|
154
|
+
const payload = envelope.payload || raw.payload;
|
|
155
|
+
if (!payload) continue;
|
|
156
|
+
|
|
157
|
+
// 1.9 puts timestamp at the outer level (Unix seconds, float).
|
|
158
|
+
// Legacy 1.1 sometimes puts it inside payload.
|
|
159
|
+
if (typeof raw.timestamp === 'number') {
|
|
160
|
+
lastTimestamp = raw.timestamp * 1000;
|
|
161
|
+
} else if (typeof payload.timestamp === 'number') {
|
|
162
|
+
lastTimestamp = payload.timestamp * 1000;
|
|
163
|
+
}
|
|
164
|
+
if (payload.model) currentModel = payload.model;
|
|
165
|
+
|
|
166
|
+
if (lastTimestamp) {
|
|
167
|
+
const evTs = new Date(lastTimestamp);
|
|
168
|
+
if (!isNaN(evTs.getTime())) {
|
|
169
|
+
sessionEvents.push({
|
|
170
|
+
sessionId: filePath,
|
|
171
|
+
source: 'kimi-code',
|
|
172
|
+
project,
|
|
173
|
+
timestamp: evTs,
|
|
174
|
+
role: USER_EVENT_TYPES.has(type) ? 'user' : 'assistant',
|
|
175
|
+
});
|
|
107
176
|
}
|
|
177
|
+
}
|
|
108
178
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const tokenUsage = payload.token_usage;
|
|
112
|
-
if (!tokenUsage) continue;
|
|
113
|
-
if (!tokenUsage.input_other && !tokenUsage.output) continue;
|
|
179
|
+
if (type !== 'StatusUpdate') continue;
|
|
114
180
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
seenMessageIds.add(messageId);
|
|
119
|
-
}
|
|
181
|
+
const tokenUsage = payload.token_usage;
|
|
182
|
+
if (!tokenUsage) continue;
|
|
183
|
+
if (!tokenUsage.input_other && !tokenUsage.output) continue;
|
|
120
184
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
model: currentModel,
|
|
126
|
-
project,
|
|
127
|
-
timestamp: ts,
|
|
128
|
-
inputTokens: tokenUsage.input_other || 0,
|
|
129
|
-
outputTokens: tokenUsage.output || 0,
|
|
130
|
-
cachedInputTokens: tokenUsage.input_cache_read || 0,
|
|
131
|
-
reasoningOutputTokens: 0,
|
|
132
|
-
});
|
|
133
|
-
} catch {
|
|
134
|
-
continue;
|
|
185
|
+
const messageId = payload.message_id;
|
|
186
|
+
if (messageId) {
|
|
187
|
+
if (seenMessageIds.has(messageId)) continue;
|
|
188
|
+
seenMessageIds.add(messageId);
|
|
135
189
|
}
|
|
190
|
+
|
|
191
|
+
const ts = lastTimestamp ? new Date(lastTimestamp) : new Date();
|
|
192
|
+
|
|
193
|
+
entries.push({
|
|
194
|
+
source: 'kimi-code',
|
|
195
|
+
model: currentModel,
|
|
196
|
+
project,
|
|
197
|
+
timestamp: ts,
|
|
198
|
+
inputTokens: tokenUsage.input_other || 0,
|
|
199
|
+
outputTokens: tokenUsage.output || 0,
|
|
200
|
+
cachedInputTokens: tokenUsage.input_cache_read || 0,
|
|
201
|
+
reasoningOutputTokens: 0,
|
|
202
|
+
});
|
|
136
203
|
}
|
|
137
204
|
}
|
|
138
205
|
|