vibeusage 0.3.5 → 0.4.0
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/README.zh-CN.md +1 -1
- package/package.json +9 -1
- package/src/commands/init.js +2 -2
- package/src/commands/sync.js +32 -18
- package/src/lib/integrations/context.js +12 -0
- package/src/lib/integrations/index.js +2 -0
- package/src/lib/integrations/kimi.js +105 -0
- package/src/lib/kimi-config.js +221 -0
- package/src/lib/opencode-usage-audit.js +2 -3
- package/src/lib/rollout.js +167 -50
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ This is useful when you want to copy an install command from the dashboard or le
|
|
|
86
86
|
| **Every Code** | Auto-detected | `notify` hook | `~/.code/sessions/**/rollout-*.jsonl` |
|
|
87
87
|
| **Claude Code** | Auto-detected | `Stop` + `SessionEnd` hooks | local hook output |
|
|
88
88
|
| **Gemini CLI** | Auto-detected | `SessionEnd` hook | `~/.gemini/tmp/**/chats/session-*.json` |
|
|
89
|
-
| **OpenCode** | Auto-detected | plugin + local parsing | `~/.local/share/opencode/opencode.db` (
|
|
89
|
+
| **OpenCode** | Auto-detected | plugin + local parsing | `~/.local/share/opencode/opencode.db` (SQLite is the only supported local accounting source) |
|
|
90
90
|
| **Hermes** | Auto-detected when installed | plugin + local parsing | `~/.vibeusage/tracker/hermes.usage.jsonl` |
|
|
91
91
|
| **OpenClaw** | Auto-detected when installed | session plugin | local sanitized usage ledger |
|
|
92
92
|
|
|
@@ -273,7 +273,7 @@ npx vibeusage status
|
|
|
273
273
|
npx vibeusage doctor
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
VibeUsage reads OpenCode usage only from `opencode.db`, so the most common issues are missing `sqlite3` on `PATH`, a missing database file, or a local SQLite query failure.
|
|
277
277
|
|
|
278
278
|
### My OpenClaw usage is not showing up. What should I check?
|
|
279
279
|
|
package/README.zh-CN.md
CHANGED
|
@@ -86,7 +86,7 @@ npx --yes vibeusage init --link-code <code>
|
|
|
86
86
|
| **Every Code** | 自动检测 | `notify` hook | `~/.code/sessions/**/rollout-*.jsonl` |
|
|
87
87
|
| **Claude Code** | 自动检测 | `Stop` + `SessionEnd` hooks | 本地 hook 输出 |
|
|
88
88
|
| **Gemini CLI** | 自动检测 | `SessionEnd` hook | `~/.gemini/tmp/**/chats/session-*.json` |
|
|
89
|
-
| **OpenCode** | 自动检测 | plugin + 本地解析 | `~/.local/share/opencode/opencode.db
|
|
89
|
+
| **OpenCode** | 自动检测 | plugin + 本地解析 | `~/.local/share/opencode/opencode.db`(SQLite 是唯一受支持的本地 accounting source) |
|
|
90
90
|
| **OpenClaw** | 安装后自动检测 | session plugin | 本地 sanitized usage ledger |
|
|
91
91
|
|
|
92
92
|
### OpenClaw 说明
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeusage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/victorGPT/vibeusage.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/victorGPT/vibeusage/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/victorGPT/vibeusage#readme",
|
|
6
14
|
"bin": {
|
|
7
15
|
"tracker": "bin/tracker.js",
|
|
8
16
|
"vibeusage": "bin/tracker.js",
|
package/src/commands/init.js
CHANGED
|
@@ -190,7 +190,7 @@ function renderWelcome() {
|
|
|
190
190
|
DIVIDER,
|
|
191
191
|
"",
|
|
192
192
|
"This tool will:",
|
|
193
|
-
" - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, Hermes, OpenClaw)",
|
|
193
|
+
" - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Kimi, Opencode, Hermes, OpenClaw)",
|
|
194
194
|
" - Set up lightweight hooks to track your flow state",
|
|
195
195
|
" - Link your device to your VibeScore account",
|
|
196
196
|
"",
|
|
@@ -486,7 +486,7 @@ try {
|
|
|
486
486
|
const originalPath =
|
|
487
487
|
source === 'every-code'
|
|
488
488
|
? codeOriginalPath
|
|
489
|
-
: source === 'claude' || source === 'opencode' || source === 'gemini'
|
|
489
|
+
: source === 'claude' || source === 'opencode' || source === 'gemini' || source === 'kimi'
|
|
490
490
|
? null
|
|
491
491
|
: codexOriginalPath;
|
|
492
492
|
if (originalPath) {
|
package/src/commands/sync.js
CHANGED
|
@@ -8,10 +8,11 @@ const {
|
|
|
8
8
|
listRolloutFiles,
|
|
9
9
|
listClaudeProjectFiles,
|
|
10
10
|
listGeminiSessionFiles,
|
|
11
|
-
|
|
11
|
+
listKimiSessionFiles,
|
|
12
12
|
parseRolloutIncremental,
|
|
13
13
|
parseClaudeIncremental,
|
|
14
14
|
parseGeminiIncremental,
|
|
15
|
+
parseKimiIncremental,
|
|
15
16
|
parseOpencodeIncremental,
|
|
16
17
|
normalizeHourlyState,
|
|
17
18
|
getHourlyBucket,
|
|
@@ -72,9 +73,10 @@ async function cmdSync(argv) {
|
|
|
72
73
|
const claudeProjectsDir = path.join(home, ".claude", "projects");
|
|
73
74
|
const geminiHome = process.env.GEMINI_HOME || path.join(home, ".gemini");
|
|
74
75
|
const geminiTmpDir = path.join(geminiHome, "tmp");
|
|
76
|
+
const kimiHome = process.env.KIMI_HOME || path.join(home, ".kimi");
|
|
77
|
+
const kimiSessionsDir = path.join(kimiHome, "sessions");
|
|
75
78
|
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
76
79
|
const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
|
|
77
|
-
const opencodeStorageDir = path.join(opencodeHome, "storage");
|
|
78
80
|
const opencodeDbPath = path.join(opencodeHome, "opencode.db");
|
|
79
81
|
|
|
80
82
|
const sources = [
|
|
@@ -181,28 +183,38 @@ async function cmdSync(argv) {
|
|
|
181
183
|
});
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
const
|
|
185
|
-
let
|
|
186
|
-
if (
|
|
187
|
-
progress
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const kimiFiles = await listKimiSessionFiles(kimiSessionsDir);
|
|
187
|
+
let kimiResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
188
|
+
if (kimiFiles.length > 0) {
|
|
189
|
+
if (progress?.enabled) {
|
|
190
|
+
progress.start(
|
|
191
|
+
`Parsing Kimi ${renderBar(0)} 0/${formatNumber(kimiFiles.length)} files | buckets 0`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
kimiResult = await parseKimiIncremental({
|
|
195
|
+
sessionFiles: kimiFiles,
|
|
196
|
+
cursors,
|
|
197
|
+
queuePath,
|
|
198
|
+
projectQueuePath,
|
|
199
|
+
onProgress: (p) => {
|
|
200
|
+
if (!progress?.enabled) return;
|
|
201
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
202
|
+
progress.update(
|
|
203
|
+
`Parsing Kimi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
|
|
204
|
+
p.bucketsQueued,
|
|
205
|
+
)}`,
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
source: "kimi",
|
|
209
|
+
});
|
|
190
210
|
}
|
|
211
|
+
|
|
212
|
+
let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
191
213
|
opencodeResult = await parseOpencodeIncremental({
|
|
192
|
-
messageFiles: opencodeFiles,
|
|
193
214
|
opencodeDbPath,
|
|
194
215
|
cursors,
|
|
195
216
|
queuePath,
|
|
196
217
|
projectQueuePath,
|
|
197
|
-
onProgress: (p) => {
|
|
198
|
-
if (!progress?.enabled) return;
|
|
199
|
-
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
200
|
-
progress.update(
|
|
201
|
-
`Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
|
|
202
|
-
p.total,
|
|
203
|
-
)} files | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
204
|
-
);
|
|
205
|
-
},
|
|
206
218
|
source: "opencode",
|
|
207
219
|
});
|
|
208
220
|
|
|
@@ -380,6 +392,7 @@ async function cmdSync(argv) {
|
|
|
380
392
|
openclawResult.filesProcessed +
|
|
381
393
|
claudeResult.filesProcessed +
|
|
382
394
|
geminiResult.filesProcessed +
|
|
395
|
+
kimiResult.filesProcessed +
|
|
383
396
|
opencodeResult.filesProcessed;
|
|
384
397
|
const totalBuckets =
|
|
385
398
|
parseResult.bucketsQueued +
|
|
@@ -387,6 +400,7 @@ async function cmdSync(argv) {
|
|
|
387
400
|
openclawResult.bucketsQueued +
|
|
388
401
|
claudeResult.bucketsQueued +
|
|
389
402
|
geminiResult.bucketsQueued +
|
|
403
|
+
kimiResult.bucketsQueued +
|
|
390
404
|
opencodeResult.bucketsQueued;
|
|
391
405
|
process.stdout.write(
|
|
392
406
|
[
|
|
@@ -7,6 +7,11 @@ const {
|
|
|
7
7
|
resolveGeminiSettingsPath,
|
|
8
8
|
buildGeminiHookCommand,
|
|
9
9
|
} = require("../gemini-config");
|
|
10
|
+
const {
|
|
11
|
+
resolveKimiConfigDir,
|
|
12
|
+
resolveKimiConfigPath,
|
|
13
|
+
buildKimiHookCommand,
|
|
14
|
+
} = require("../kimi-config");
|
|
10
15
|
const { resolveOpencodeConfigDir } = require("../opencode-config");
|
|
11
16
|
const { resolveOpenclawSessionPluginPaths } = require("../openclaw-session-plugin");
|
|
12
17
|
const { resolveHermesPluginPaths } = require("../hermes-config");
|
|
@@ -25,6 +30,7 @@ async function createIntegrationContext({
|
|
|
25
30
|
const codeHome = env.CODE_HOME || path.join(home, ".code");
|
|
26
31
|
const claudeDir = path.join(home, ".claude");
|
|
27
32
|
const geminiConfigDir = resolveGeminiConfigDir({ home, env });
|
|
33
|
+
const kimiConfigDir = resolveKimiConfigDir({ home, env });
|
|
28
34
|
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env });
|
|
29
35
|
|
|
30
36
|
return {
|
|
@@ -55,6 +61,12 @@ async function createIntegrationContext({
|
|
|
55
61
|
settingsPath: resolveGeminiSettingsPath({ configDir: geminiConfigDir }),
|
|
56
62
|
hookCommand: buildGeminiHookCommand(resolvedNotifyPath),
|
|
57
63
|
},
|
|
64
|
+
kimi: {
|
|
65
|
+
configDir: kimiConfigDir,
|
|
66
|
+
configPath: resolveKimiConfigPath({ configDir: kimiConfigDir }),
|
|
67
|
+
hookCommand: buildKimiHookCommand(resolvedNotifyPath),
|
|
68
|
+
sessionsDir: path.join(kimiConfigDir, "sessions"),
|
|
69
|
+
},
|
|
58
70
|
opencode: {
|
|
59
71
|
configDir: opencodeConfigDir,
|
|
60
72
|
},
|
|
@@ -3,6 +3,7 @@ const codex = require("./codex");
|
|
|
3
3
|
const everyCode = require("./every-code");
|
|
4
4
|
const claude = require("./claude");
|
|
5
5
|
const gemini = require("./gemini");
|
|
6
|
+
const kimi = require("./kimi");
|
|
6
7
|
const opencode = require("./opencode");
|
|
7
8
|
const hermes = require("./hermes");
|
|
8
9
|
const openclawSession = require("./openclaw-session");
|
|
@@ -12,6 +13,7 @@ const INTEGRATIONS = [
|
|
|
12
13
|
everyCode,
|
|
13
14
|
claude,
|
|
14
15
|
gemini,
|
|
16
|
+
kimi,
|
|
15
17
|
opencode,
|
|
16
18
|
hermes,
|
|
17
19
|
openclawSession,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isKimiHookConfigured,
|
|
3
|
+
upsertKimiHook,
|
|
4
|
+
removeKimiHook,
|
|
5
|
+
probeKimiHook,
|
|
6
|
+
} = require("../kimi-config");
|
|
7
|
+
const { isDir, isFile } = require("./utils");
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
name: "kimi",
|
|
11
|
+
summaryLabel: "Kimi",
|
|
12
|
+
statusLabel: "Kimi hooks",
|
|
13
|
+
async probe(ctx) {
|
|
14
|
+
const hasConfigDir = await isDir(ctx.kimi.configDir);
|
|
15
|
+
if (!hasConfigDir) {
|
|
16
|
+
return baseProbe(this, { status: "not_installed", detail: "Config not found" });
|
|
17
|
+
}
|
|
18
|
+
const hasConfigFile = await isFile(ctx.kimi.configPath);
|
|
19
|
+
if (!hasConfigFile) {
|
|
20
|
+
return baseProbe(this, {
|
|
21
|
+
status: "not_installed",
|
|
22
|
+
detail: "config.toml not found",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const state = await probeKimiHook({
|
|
26
|
+
configPath: ctx.kimi.configPath,
|
|
27
|
+
hookCommand: ctx.kimi.hookCommand,
|
|
28
|
+
});
|
|
29
|
+
if (state.configured) {
|
|
30
|
+
return baseProbe(this, {
|
|
31
|
+
status: "ready",
|
|
32
|
+
detail: "Hooks installed",
|
|
33
|
+
configured: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return baseProbe(this, {
|
|
37
|
+
status: state.drifted ? "drifted" : "not_installed",
|
|
38
|
+
detail: state.drifted
|
|
39
|
+
? "Run vibeusage init to reconcile hooks"
|
|
40
|
+
: "Run vibeusage init to install hooks",
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
async install(ctx) {
|
|
44
|
+
if (!(await isDir(ctx.kimi.configDir))) {
|
|
45
|
+
return action(this, "skipped", false, "Config not found");
|
|
46
|
+
}
|
|
47
|
+
if (!(await isFile(ctx.kimi.configPath))) {
|
|
48
|
+
return action(this, "skipped", false, "config.toml not found");
|
|
49
|
+
}
|
|
50
|
+
const result = await upsertKimiHook({
|
|
51
|
+
configPath: ctx.kimi.configPath,
|
|
52
|
+
hookCommand: ctx.kimi.hookCommand,
|
|
53
|
+
});
|
|
54
|
+
return action(
|
|
55
|
+
this,
|
|
56
|
+
result.changed ? "installed" : "set",
|
|
57
|
+
Boolean(result.changed),
|
|
58
|
+
result.changed ? "Hooks installed" : "Hooks already installed",
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
async uninstall(ctx) {
|
|
62
|
+
if (!(await isDir(ctx.kimi.configDir))) {
|
|
63
|
+
return action(this, "skipped", false, "config dir not found");
|
|
64
|
+
}
|
|
65
|
+
const result = await removeKimiHook({ configPath: ctx.kimi.configPath });
|
|
66
|
+
if (result.removed) {
|
|
67
|
+
return action(this, "removed", true, ctx.kimi.configPath);
|
|
68
|
+
}
|
|
69
|
+
if (result.skippedReason === "hook-missing") {
|
|
70
|
+
return action(this, "unchanged", false, "no change", {
|
|
71
|
+
skippedReason: result.skippedReason,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return action(this, "skipped", false, "config.toml not found");
|
|
75
|
+
},
|
|
76
|
+
renderStatusValue(probe) {
|
|
77
|
+
if (probe.status === "ready") return "set";
|
|
78
|
+
if (probe.status === "not_installed") return "unset";
|
|
79
|
+
return probe.status;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function baseProbe(descriptor, values) {
|
|
84
|
+
return {
|
|
85
|
+
name: descriptor.name,
|
|
86
|
+
summaryLabel: descriptor.summaryLabel,
|
|
87
|
+
statusLabel: descriptor.statusLabel,
|
|
88
|
+
configured: false,
|
|
89
|
+
...values,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function action(descriptor, status, changed, detail, extras = {}) {
|
|
94
|
+
return {
|
|
95
|
+
name: descriptor.name,
|
|
96
|
+
label: descriptor.summaryLabel,
|
|
97
|
+
status,
|
|
98
|
+
changed,
|
|
99
|
+
detail,
|
|
100
|
+
...extras,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Expose for tests / diagnostics that want the probe-only check.
|
|
105
|
+
module.exports.isKimiHookConfigured = isKimiHookConfigured;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
|
|
5
|
+
const { ensureDir } = require("./fs");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EVENTS = ["SessionEnd", "Stop"];
|
|
8
|
+
const DEFAULT_TIMEOUT = 30;
|
|
9
|
+
const MANAGED_START = "# --- vibeusage Kimi hooks START (managed, do not edit) ---";
|
|
10
|
+
const MANAGED_END = "# --- vibeusage Kimi hooks END ---";
|
|
11
|
+
|
|
12
|
+
function resolveKimiConfigDir({ home = os.homedir(), env = process.env } = {}) {
|
|
13
|
+
const explicit = typeof env.KIMI_HOME === "string" ? env.KIMI_HOME.trim() : "";
|
|
14
|
+
if (explicit) return path.resolve(explicit);
|
|
15
|
+
return path.join(home, ".kimi");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveKimiConfigPath({ configDir }) {
|
|
19
|
+
return path.join(configDir, "config.toml");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildKimiHookCommand(notifyPath) {
|
|
23
|
+
const cmd = typeof notifyPath === "string" ? notifyPath : "";
|
|
24
|
+
return `/usr/bin/env node ${quoteArg(cmd)} --source=kimi`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function upsertKimiHook({
|
|
28
|
+
configPath,
|
|
29
|
+
hookCommand,
|
|
30
|
+
events = DEFAULT_EVENTS,
|
|
31
|
+
timeout = DEFAULT_TIMEOUT,
|
|
32
|
+
}) {
|
|
33
|
+
const existing = await readFileOrEmpty(configPath);
|
|
34
|
+
const normalizedEvents = normalizeEvents(events);
|
|
35
|
+
const nextBlock = buildManagedBlock({
|
|
36
|
+
hookCommand,
|
|
37
|
+
events: normalizedEvents,
|
|
38
|
+
timeout: normalizeTimeout(timeout),
|
|
39
|
+
});
|
|
40
|
+
const { content, changed } = replaceManagedBlock(existing, nextBlock);
|
|
41
|
+
if (!changed) return { changed: false, backupPath: null };
|
|
42
|
+
const backupPath = await writeWithBackup({ configPath, content });
|
|
43
|
+
return { changed: true, backupPath };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function removeKimiHook({ configPath }) {
|
|
47
|
+
const existing = await readFileOrEmpty(configPath);
|
|
48
|
+
if (!existing) return { removed: false, skippedReason: "config-missing", backupPath: null };
|
|
49
|
+
const { content, changed } = stripManagedBlock(existing);
|
|
50
|
+
if (!changed) return { removed: false, skippedReason: "hook-missing", backupPath: null };
|
|
51
|
+
const backupPath = await writeWithBackup({ configPath, content });
|
|
52
|
+
return { removed: true, skippedReason: null, backupPath };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function isKimiHookConfigured({
|
|
56
|
+
configPath,
|
|
57
|
+
hookCommand,
|
|
58
|
+
events = DEFAULT_EVENTS,
|
|
59
|
+
timeout = DEFAULT_TIMEOUT,
|
|
60
|
+
}) {
|
|
61
|
+
const probe = await probeKimiHook({ configPath, hookCommand, events, timeout });
|
|
62
|
+
return probe.configured;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function probeKimiHook({
|
|
66
|
+
configPath,
|
|
67
|
+
hookCommand,
|
|
68
|
+
events = DEFAULT_EVENTS,
|
|
69
|
+
timeout = DEFAULT_TIMEOUT,
|
|
70
|
+
}) {
|
|
71
|
+
const existing = await readFileOrEmpty(configPath);
|
|
72
|
+
if (!existing) {
|
|
73
|
+
return { configured: false, anyPresent: false, drifted: false };
|
|
74
|
+
}
|
|
75
|
+
const block = extractManagedBlock(existing);
|
|
76
|
+
if (!block) {
|
|
77
|
+
return { configured: false, anyPresent: false, drifted: false };
|
|
78
|
+
}
|
|
79
|
+
const expected = buildManagedBlock({
|
|
80
|
+
hookCommand,
|
|
81
|
+
events: normalizeEvents(events),
|
|
82
|
+
timeout: normalizeTimeout(timeout),
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
configured: block === expected,
|
|
86
|
+
anyPresent: true,
|
|
87
|
+
drifted: block !== expected,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildManagedBlock({ hookCommand, events, timeout }) {
|
|
92
|
+
const lines = [MANAGED_START];
|
|
93
|
+
for (const event of events) {
|
|
94
|
+
lines.push(
|
|
95
|
+
"[[hooks]]",
|
|
96
|
+
`event = ${tomlString(event)}`,
|
|
97
|
+
`command = ${tomlString(hookCommand)}`,
|
|
98
|
+
`timeout = ${timeout}`,
|
|
99
|
+
);
|
|
100
|
+
lines.push("");
|
|
101
|
+
}
|
|
102
|
+
lines.push(MANAGED_END);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function replaceManagedBlock(existing, nextBlock) {
|
|
107
|
+
const startIdx = existing.indexOf(MANAGED_START);
|
|
108
|
+
const endIdx = existing.indexOf(MANAGED_END);
|
|
109
|
+
|
|
110
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
111
|
+
const blockEnd = endIdx + MANAGED_END.length;
|
|
112
|
+
const current = existing.slice(startIdx, blockEnd);
|
|
113
|
+
if (current === nextBlock) return { content: existing, changed: false };
|
|
114
|
+
const before = existing.slice(0, startIdx);
|
|
115
|
+
let after = existing.slice(blockEnd);
|
|
116
|
+
if (after.startsWith("\n")) after = after.slice(1);
|
|
117
|
+
const beforeTrimmed = before.replace(/\n+$/, "");
|
|
118
|
+
const prefix = beforeTrimmed.length > 0 ? `${beforeTrimmed}\n\n` : "";
|
|
119
|
+
const suffix = after.length > 0 ? `\n\n${after.replace(/^\n+/, "")}` : "\n";
|
|
120
|
+
return { content: `${prefix}${nextBlock}${suffix}`, changed: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const base = existing.replace(/\n+$/, "");
|
|
124
|
+
const prefix = base.length > 0 ? `${base}\n\n` : "";
|
|
125
|
+
return { content: `${prefix}${nextBlock}\n`, changed: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stripManagedBlock(existing) {
|
|
129
|
+
const startIdx = existing.indexOf(MANAGED_START);
|
|
130
|
+
const endIdx = existing.indexOf(MANAGED_END);
|
|
131
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
132
|
+
return { content: existing, changed: false };
|
|
133
|
+
}
|
|
134
|
+
const blockEnd = endIdx + MANAGED_END.length;
|
|
135
|
+
const before = existing.slice(0, startIdx).replace(/\n+$/, "");
|
|
136
|
+
let after = existing.slice(blockEnd);
|
|
137
|
+
after = after.replace(/^\n+/, "");
|
|
138
|
+
if (before.length === 0 && after.length === 0) {
|
|
139
|
+
return { content: "", changed: true };
|
|
140
|
+
}
|
|
141
|
+
if (before.length === 0) return { content: `${after.endsWith("\n") ? after : `${after}\n`}`, changed: true };
|
|
142
|
+
if (after.length === 0) return { content: `${before}\n`, changed: true };
|
|
143
|
+
return { content: `${before}\n\n${after.endsWith("\n") ? after : `${after}\n`}`, changed: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractManagedBlock(existing) {
|
|
147
|
+
const startIdx = existing.indexOf(MANAGED_START);
|
|
148
|
+
const endIdx = existing.indexOf(MANAGED_END);
|
|
149
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null;
|
|
150
|
+
const blockEnd = endIdx + MANAGED_END.length;
|
|
151
|
+
return existing.slice(startIdx, blockEnd);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeEvents(raw) {
|
|
155
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const value of values) {
|
|
158
|
+
if (typeof value !== "string") continue;
|
|
159
|
+
const normalized = value.trim();
|
|
160
|
+
if (!normalized || out.includes(normalized)) continue;
|
|
161
|
+
out.push(normalized);
|
|
162
|
+
}
|
|
163
|
+
return out.length > 0 ? out : DEFAULT_EVENTS.slice();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeTimeout(value) {
|
|
167
|
+
const n = Number(value);
|
|
168
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_TIMEOUT;
|
|
169
|
+
return Math.floor(n);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function tomlString(value) {
|
|
173
|
+
const v = typeof value === "string" ? value : String(value ?? "");
|
|
174
|
+
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function quoteArg(value) {
|
|
178
|
+
const v = typeof value === "string" ? value : "";
|
|
179
|
+
if (!v) return '""';
|
|
180
|
+
if (/^[A-Za-z0-9_\-./:@]+$/.test(v)) return v;
|
|
181
|
+
return `"${v.replace(/"/g, '\\"')}"`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function readFileOrEmpty(filePath) {
|
|
185
|
+
try {
|
|
186
|
+
return await fs.readFile(filePath, "utf8");
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err && err.code === "ENOENT") return "";
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function writeWithBackup({ configPath, content }) {
|
|
194
|
+
await ensureDir(path.dirname(configPath));
|
|
195
|
+
let backupPath = null;
|
|
196
|
+
try {
|
|
197
|
+
const st = await fs.stat(configPath);
|
|
198
|
+
if (st && st.isFile()) {
|
|
199
|
+
backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
200
|
+
await fs.copyFile(configPath, backupPath);
|
|
201
|
+
}
|
|
202
|
+
} catch (_e) {
|
|
203
|
+
// no existing file
|
|
204
|
+
}
|
|
205
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
206
|
+
return backupPath;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
DEFAULT_EVENTS,
|
|
211
|
+
DEFAULT_TIMEOUT,
|
|
212
|
+
MANAGED_START,
|
|
213
|
+
MANAGED_END,
|
|
214
|
+
resolveKimiConfigDir,
|
|
215
|
+
resolveKimiConfigPath,
|
|
216
|
+
buildKimiHookCommand,
|
|
217
|
+
upsertKimiHook,
|
|
218
|
+
removeKimiHook,
|
|
219
|
+
isKimiHookConfigured,
|
|
220
|
+
probeKimiHook,
|
|
221
|
+
};
|
|
@@ -2,7 +2,7 @@ const fs = require("node:fs/promises");
|
|
|
2
2
|
const os = require("node:os");
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { parseOpencodeIncremental } = require("./rollout");
|
|
6
6
|
|
|
7
7
|
const BUCKET_SEPARATOR = "|";
|
|
8
8
|
const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -31,7 +31,6 @@ function addTotals(target, delta) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async function buildLocalHourlyTotals({ storageDir, source = "opencode" }) {
|
|
34
|
-
const messageFiles = await listOpencodeMessageFiles(storageDir);
|
|
35
34
|
const opencodeDbPath = path.resolve(storageDir, "..", "opencode.db");
|
|
36
35
|
const queuePath = path.join(
|
|
37
36
|
os.tmpdir(),
|
|
@@ -39,7 +38,7 @@ async function buildLocalHourlyTotals({ storageDir, source = "opencode" }) {
|
|
|
39
38
|
);
|
|
40
39
|
const cursors = { version: 1, files: {}, hourly: null, opencode: null, opencodeSqlite: null };
|
|
41
40
|
|
|
42
|
-
await parseOpencodeIncremental({
|
|
41
|
+
await parseOpencodeIncremental({ opencodeDbPath, cursors, queuePath, source });
|
|
43
42
|
await fs.rm(queuePath, { force: true }).catch(() => {});
|
|
44
43
|
|
|
45
44
|
const byHour = new Map();
|
package/src/lib/rollout.js
CHANGED
|
@@ -63,6 +63,25 @@ async function listGeminiSessionFiles(tmpDir) {
|
|
|
63
63
|
return out;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
async function listKimiSessionFiles(sessionsDir) {
|
|
67
|
+
const out = [];
|
|
68
|
+
const projects = await safeReadDir(sessionsDir);
|
|
69
|
+
for (const project of projects) {
|
|
70
|
+
if (!project.isDirectory()) continue;
|
|
71
|
+
const projectDir = path.join(sessionsDir, project.name);
|
|
72
|
+
const sessions = await safeReadDir(projectDir);
|
|
73
|
+
for (const session of sessions) {
|
|
74
|
+
if (!session.isDirectory()) continue;
|
|
75
|
+
const wirePath = path.join(projectDir, session.name, "wire.jsonl");
|
|
76
|
+
const st = await fs.stat(wirePath).catch(() => null);
|
|
77
|
+
if (!st || !st.isFile()) continue;
|
|
78
|
+
out.push(wirePath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
async function listOpencodeMessageFiles(storageDir) {
|
|
67
86
|
const out = [];
|
|
68
87
|
const messageDir = path.join(storageDir, "message");
|
|
@@ -397,23 +416,21 @@ async function parseGeminiIncremental({
|
|
|
397
416
|
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
398
417
|
}
|
|
399
418
|
|
|
400
|
-
async function
|
|
401
|
-
|
|
402
|
-
opencodeDbPath,
|
|
419
|
+
async function parseKimiIncremental({
|
|
420
|
+
sessionFiles,
|
|
403
421
|
cursors,
|
|
404
422
|
queuePath,
|
|
405
423
|
projectQueuePath,
|
|
406
424
|
onProgress,
|
|
407
425
|
source,
|
|
408
426
|
publicRepoResolver,
|
|
409
|
-
readSqliteRows,
|
|
410
427
|
}) {
|
|
411
428
|
await ensureDir(path.dirname(queuePath));
|
|
412
429
|
let filesProcessed = 0;
|
|
413
430
|
let eventsAggregated = 0;
|
|
414
431
|
|
|
415
432
|
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
416
|
-
const files = Array.isArray(
|
|
433
|
+
const files = Array.isArray(sessionFiles) ? sessionFiles : [];
|
|
417
434
|
const totalFiles = files.length;
|
|
418
435
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
419
436
|
const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
|
|
@@ -421,14 +438,8 @@ async function parseOpencodeIncremental({
|
|
|
421
438
|
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
422
439
|
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
423
440
|
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
424
|
-
const opencodeState = normalizeOpencodeState(cursors?.opencode);
|
|
425
|
-
const opencodeSqliteState = normalizeOpencodeSqliteState(cursors?.opencodeSqlite);
|
|
426
|
-
const messageIndex = opencodeState.messages;
|
|
427
441
|
const touchedBuckets = new Set();
|
|
428
|
-
const defaultSource = normalizeSourceInput(source) || "
|
|
429
|
-
let sqliteStatus = opencodeSqliteState.lastStatus || "never_checked";
|
|
430
|
-
let sqliteCheckedAt = opencodeSqliteState.lastCheckedAt || null;
|
|
431
|
-
let sqliteErrorCode = opencodeSqliteState.lastErrorCode || null;
|
|
442
|
+
const defaultSource = normalizeSourceInput(source) || "kimi";
|
|
432
443
|
|
|
433
444
|
if (!cursors.files || typeof cursors.files !== "object") {
|
|
434
445
|
cursors.files = {};
|
|
@@ -448,30 +459,8 @@ async function parseOpencodeIncremental({
|
|
|
448
459
|
const key = filePath;
|
|
449
460
|
const prev = cursors.files[key] || null;
|
|
450
461
|
const inode = st.ino || 0;
|
|
451
|
-
const
|
|
452
|
-
const mtimeMs = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
|
|
453
|
-
const unchanged =
|
|
454
|
-
prev && prev.inode === inode && prev.size === size && prev.mtimeMs === mtimeMs;
|
|
455
|
-
if (unchanged) {
|
|
456
|
-
filesProcessed += 1;
|
|
457
|
-
if (cb) {
|
|
458
|
-
cb({
|
|
459
|
-
index: idx + 1,
|
|
460
|
-
total: totalFiles,
|
|
461
|
-
filePath,
|
|
462
|
-
filesProcessed,
|
|
463
|
-
eventsAggregated,
|
|
464
|
-
bucketsQueued: touchedBuckets.size,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
462
|
+
const startOffset = prev && prev.inode === inode ? prev.offset || 0 : 0;
|
|
469
463
|
|
|
470
|
-
const fallbackTotals = prev && typeof prev.lastTotals === "object" ? prev.lastTotals : null;
|
|
471
|
-
const fallbackMessageKey =
|
|
472
|
-
prev && typeof prev.messageKey === "string" && prev.messageKey.trim()
|
|
473
|
-
? prev.messageKey.trim()
|
|
474
|
-
: null;
|
|
475
464
|
const projectContext = projectEnabled
|
|
476
465
|
? await resolveProjectContextForFile({
|
|
477
466
|
filePath,
|
|
@@ -484,11 +473,9 @@ async function parseOpencodeIncremental({
|
|
|
484
473
|
const projectRef = projectContext?.projectRef || null;
|
|
485
474
|
const projectKey = projectContext?.projectKey || null;
|
|
486
475
|
|
|
487
|
-
const result = await
|
|
476
|
+
const result = await parseKimiFile({
|
|
488
477
|
filePath,
|
|
489
|
-
|
|
490
|
-
fallbackTotals,
|
|
491
|
-
fallbackMessageKey,
|
|
478
|
+
startOffset,
|
|
492
479
|
hourlyState,
|
|
493
480
|
touchedBuckets,
|
|
494
481
|
source: fileSource,
|
|
@@ -500,23 +487,13 @@ async function parseOpencodeIncremental({
|
|
|
500
487
|
|
|
501
488
|
cursors.files[key] = {
|
|
502
489
|
inode,
|
|
503
|
-
|
|
504
|
-
mtimeMs,
|
|
505
|
-
lastTotals: result.lastTotals,
|
|
506
|
-
messageKey: result.messageKey || null,
|
|
490
|
+
offset: result.endOffset,
|
|
507
491
|
updatedAt: new Date().toISOString(),
|
|
508
492
|
};
|
|
509
493
|
|
|
510
494
|
filesProcessed += 1;
|
|
511
495
|
eventsAggregated += result.eventsAggregated;
|
|
512
496
|
|
|
513
|
-
if (result.messageKey && result.shouldUpdate) {
|
|
514
|
-
messageIndex[result.messageKey] = {
|
|
515
|
-
lastTotals: result.lastTotals,
|
|
516
|
-
updatedAt: new Date().toISOString(),
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
497
|
if (cb) {
|
|
521
498
|
cb({
|
|
522
499
|
index: idx + 1,
|
|
@@ -529,6 +506,54 @@ async function parseOpencodeIncremental({
|
|
|
529
506
|
}
|
|
530
507
|
}
|
|
531
508
|
|
|
509
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
510
|
+
const projectBucketsQueued = projectEnabled
|
|
511
|
+
? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
|
|
512
|
+
: 0;
|
|
513
|
+
hourlyState.updatedAt = new Date().toISOString();
|
|
514
|
+
cursors.hourly = hourlyState;
|
|
515
|
+
if (projectState) {
|
|
516
|
+
projectState.updatedAt = new Date().toISOString();
|
|
517
|
+
cursors.projectHourly = projectState;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function parseOpencodeIncremental({
|
|
524
|
+
opencodeDbPath,
|
|
525
|
+
cursors,
|
|
526
|
+
queuePath,
|
|
527
|
+
projectQueuePath,
|
|
528
|
+
onProgress,
|
|
529
|
+
source = "opencode",
|
|
530
|
+
publicRepoResolver = null,
|
|
531
|
+
readSqliteRows,
|
|
532
|
+
}) {
|
|
533
|
+
await ensureDir(path.dirname(queuePath));
|
|
534
|
+
let filesProcessed = 0;
|
|
535
|
+
let eventsAggregated = 0;
|
|
536
|
+
|
|
537
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
538
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
539
|
+
const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
|
|
540
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
541
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
542
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
543
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
544
|
+
const opencodeState = normalizeOpencodeState(cursors?.opencode);
|
|
545
|
+
const opencodeSqliteState = normalizeOpencodeSqliteState(cursors?.opencodeSqlite);
|
|
546
|
+
const messageIndex = opencodeState.messages;
|
|
547
|
+
const touchedBuckets = new Set();
|
|
548
|
+
const defaultSource = normalizeSourceInput(source) || "opencode";
|
|
549
|
+
let sqliteStatus = opencodeSqliteState.lastStatus || "never_checked";
|
|
550
|
+
let sqliteCheckedAt = opencodeSqliteState.lastCheckedAt || null;
|
|
551
|
+
let sqliteErrorCode = opencodeSqliteState.lastErrorCode || null;
|
|
552
|
+
|
|
553
|
+
if (!cursors.files || typeof cursors.files !== "object") {
|
|
554
|
+
cursors.files = {};
|
|
555
|
+
}
|
|
556
|
+
|
|
532
557
|
if (typeof opencodeDbPath === "string" && opencodeDbPath.length > 0) {
|
|
533
558
|
const readRows =
|
|
534
559
|
typeof readSqliteRows === "function" ? readSqliteRows : readOpencodeSqliteRows;
|
|
@@ -818,6 +843,70 @@ async function parseClaudeFile({
|
|
|
818
843
|
return { endOffset, eventsAggregated };
|
|
819
844
|
}
|
|
820
845
|
|
|
846
|
+
async function parseKimiFile({
|
|
847
|
+
filePath,
|
|
848
|
+
startOffset,
|
|
849
|
+
hourlyState,
|
|
850
|
+
touchedBuckets,
|
|
851
|
+
source,
|
|
852
|
+
projectState,
|
|
853
|
+
projectTouchedBuckets,
|
|
854
|
+
projectRef,
|
|
855
|
+
projectKey,
|
|
856
|
+
}) {
|
|
857
|
+
const st = await fs.stat(filePath).catch(() => null);
|
|
858
|
+
if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
|
|
859
|
+
|
|
860
|
+
const endOffset = st.size;
|
|
861
|
+
if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
|
|
862
|
+
|
|
863
|
+
const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
|
|
864
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
865
|
+
|
|
866
|
+
let eventsAggregated = 0;
|
|
867
|
+
for await (const line of rl) {
|
|
868
|
+
if (!line || !line.includes("StatusUpdate")) continue;
|
|
869
|
+
let obj;
|
|
870
|
+
try {
|
|
871
|
+
obj = JSON.parse(line);
|
|
872
|
+
} catch (_e) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (!obj || !obj.message || obj.message.type !== "StatusUpdate") continue;
|
|
876
|
+
|
|
877
|
+
const payload = obj.message.payload;
|
|
878
|
+
const delta = normalizeKimiUsage(payload?.token_usage);
|
|
879
|
+
if (!delta || isAllZeroUsage(delta)) continue;
|
|
880
|
+
|
|
881
|
+
const tsIso = kimiTimestampToIso(obj.timestamp);
|
|
882
|
+
if (!tsIso) continue;
|
|
883
|
+
|
|
884
|
+
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
885
|
+
if (!bucketStart) continue;
|
|
886
|
+
|
|
887
|
+
const model = normalizeModelInput(payload?.model) || DEFAULT_MODEL;
|
|
888
|
+
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
889
|
+
addTotals(bucket.totals, delta);
|
|
890
|
+
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
891
|
+
if (projectKey && projectState && projectTouchedBuckets) {
|
|
892
|
+
const projectBucket = getProjectBucket(
|
|
893
|
+
projectState,
|
|
894
|
+
projectKey,
|
|
895
|
+
source,
|
|
896
|
+
bucketStart,
|
|
897
|
+
projectRef,
|
|
898
|
+
);
|
|
899
|
+
addTotals(projectBucket.totals, delta);
|
|
900
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
901
|
+
}
|
|
902
|
+
eventsAggregated += 1;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
rl.close();
|
|
906
|
+
stream.close?.();
|
|
907
|
+
return { endOffset, eventsAggregated };
|
|
908
|
+
}
|
|
909
|
+
|
|
821
910
|
async function parseGeminiFile({
|
|
822
911
|
filePath,
|
|
823
912
|
startIndex,
|
|
@@ -2058,6 +2147,32 @@ function normalizeGeminiTokens(tokens) {
|
|
|
2058
2147
|
};
|
|
2059
2148
|
}
|
|
2060
2149
|
|
|
2150
|
+
function normalizeKimiUsage(usage) {
|
|
2151
|
+
if (!usage || typeof usage !== "object") return null;
|
|
2152
|
+
const inputOther = toNonNegativeInt(usage.input_other);
|
|
2153
|
+
const cacheCreation = toNonNegativeInt(usage.input_cache_creation);
|
|
2154
|
+
const cacheRead = toNonNegativeInt(usage.input_cache_read);
|
|
2155
|
+
const output = toNonNegativeInt(usage.output);
|
|
2156
|
+
const inputTokens = inputOther + cacheCreation;
|
|
2157
|
+
const total = inputTokens + cacheRead + output;
|
|
2158
|
+
return {
|
|
2159
|
+
input_tokens: inputTokens,
|
|
2160
|
+
cached_input_tokens: cacheRead,
|
|
2161
|
+
output_tokens: output,
|
|
2162
|
+
reasoning_output_tokens: 0,
|
|
2163
|
+
total_tokens: total,
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function kimiTimestampToIso(value) {
|
|
2168
|
+
const n = Number(value);
|
|
2169
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
2170
|
+
const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);
|
|
2171
|
+
const date = new Date(ms);
|
|
2172
|
+
const iso = date.toISOString();
|
|
2173
|
+
return iso;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2061
2176
|
function normalizeOpencodeTokens(tokens) {
|
|
2062
2177
|
if (!tokens || typeof tokens !== "object") return null;
|
|
2063
2178
|
const input = toNonNegativeInt(tokens.input);
|
|
@@ -2293,10 +2408,12 @@ module.exports = {
|
|
|
2293
2408
|
listRolloutFiles,
|
|
2294
2409
|
listClaudeProjectFiles,
|
|
2295
2410
|
listGeminiSessionFiles,
|
|
2411
|
+
listKimiSessionFiles,
|
|
2296
2412
|
listOpencodeMessageFiles,
|
|
2297
2413
|
parseRolloutIncremental,
|
|
2298
2414
|
parseClaudeIncremental,
|
|
2299
2415
|
parseGeminiIncremental,
|
|
2416
|
+
parseKimiIncremental,
|
|
2300
2417
|
parseOpencodeIncremental,
|
|
2301
2418
|
normalizeHourlyState,
|
|
2302
2419
|
getHourlyBucket,
|