mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-31 03:44:59 +00:00
kimicode: discover desktop-runtime sessions and fix menubar visibility
- Resolve k3/k3-agent/k2d6-agent model aliases to canonical Kimi names - Discover sessions across all Kimi Code homes (CLI + desktop runtime) - Accept conv-*/ctitle-* session directory naming, not just session_* - Add Kimi Code provider tab with brand color to the menubar - Show short model names (Kimi K3, Kimi K2.6) in the menubar payload
This commit is contained in:
parent
2f8e5bddcd
commit
be5c0c00aa
10 changed files with 129 additions and 11 deletions
|
|
@ -8,14 +8,23 @@ MoonshotAI Kimi Code local session usage and tool activity.
|
|||
|
||||
## Where it reads from
|
||||
|
||||
The provider reads `~/.kimi-code` by default and honors the Kimi Code CLI's `KIMI_CODE_HOME` environment variable. It scans:
|
||||
By default the provider scans every known Kimi Code runtime store:
|
||||
|
||||
```text
|
||||
$KIMI_CODE_HOME/sessions/wd_*/session_*/
|
||||
~/.kimi-code
|
||||
~/Library/Application Support/kimi-desktop/daimon-share/daimon/runtime/kimi-code/home (Kimi desktop / IDE embedded runtime)
|
||||
```
|
||||
|
||||
Setting `KIMI_CODE_HOME` (or passing a home override) narrows the scan to that single home. Inside each home it scans:
|
||||
|
||||
```text
|
||||
$HOME/sessions/wd_*/<session-dir>/
|
||||
├── state.json
|
||||
└── agents/<agent-id>/wire.jsonl
|
||||
```
|
||||
|
||||
Session directory naming depends on the host product: the CLI uses `session_*`, embedded runtimes use `conv-*` / `ctitle-*`. Any directory is accepted; the `agents/*/wire.jsonl` probe gates real sessions.
|
||||
|
||||
Every agent wire is a cache source. Main-agent and subagent calls share the session ID from the `session_*` directory. `state.json.workDir` supplies the project name and path. `probeRoots()` reports the resolved Kimi Code home for `codeburn doctor` even when there are no sessions.
|
||||
|
||||
## Storage format
|
||||
|
|
|
|||
|
|
@ -494,6 +494,7 @@ extension ProviderFilter {
|
|||
case .kiloCode: return Color(red: 0x00/255.0, green: 0x96/255.0, blue: 0x88/255.0)
|
||||
case .kiro: return Color(red: 0x4A/255.0, green: 0x9E/255.0, blue: 0xC4/255.0)
|
||||
case .kimi: return Color(red: 0xA4/255.0, green: 0xC6/255.0, blue: 0x39/255.0)
|
||||
case .kimiCode: return Color(red: 0xA3/255.0, green: 0xE6/255.0, blue: 0x35/255.0)
|
||||
case .lingtaiTui: return Color(red: 0x22/255.0, green: 0xA7/255.0, blue: 0xA0/255.0)
|
||||
case .openclaw: return Color(red: 0xDA/255.0, green: 0x70/255.0, blue: 0x56/255.0)
|
||||
case .opencode: return Color(red: 0x5B/255.0, green: 0x83/255.0, blue: 0x5B/255.0)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const PROVIDER_COLORS: Record<string, string> = {
|
|||
opencode: '#A78BFA',
|
||||
pi: '#F472B6',
|
||||
kimi: '#B6E34A',
|
||||
kimicode: '#A3E635',
|
||||
all: '#FF8C42',
|
||||
}
|
||||
|
||||
|
|
@ -670,6 +671,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
|||
opencode: 'OpenCode',
|
||||
pi: 'Pi',
|
||||
kimi: 'Kimi',
|
||||
kimicode: 'Kimi Code',
|
||||
}
|
||||
function getProviderDisplayName(name: string): string { return PROVIDER_DISPLAY_NAMES[name] ?? name }
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export type ProviderCost = {
|
|||
import type { OptimizeResult } from './optimize.js'
|
||||
import { getCurrency } from './currency.js'
|
||||
import type { GranularHistory } from './granular-history.js'
|
||||
import { getShortModelName } from './models.js'
|
||||
import type { ReworkedFile } from './workflow-insights.js'
|
||||
import type { PrRow, BranchRow } from './sessions-report.js'
|
||||
|
||||
|
|
@ -369,10 +370,24 @@ function buildTopActivities(categories: PeriodData['categories']): MenubarPayloa
|
|||
}
|
||||
|
||||
function buildTopModels(models: PeriodData['models']): MenubarPayload['current']['topModels'] {
|
||||
return models
|
||||
.filter(m => m.name !== SYNTHETIC_MODEL_NAME)
|
||||
// Day entries key models by the raw provider id (day-aggregator), so resolve
|
||||
// display names here — the menubar shows "Kimi K3" rather than "k3". Ids that
|
||||
// collapse to one display name (e.g. k3 and kimi-k3) merge into a single row.
|
||||
const merged = new Map<string, { cost: number; calls: number; savingsUSD: number; estimatedCostUSD: number }>()
|
||||
for (const m of models) {
|
||||
if (m.name === SYNTHETIC_MODEL_NAME) continue
|
||||
const name = getShortModelName(m.name)
|
||||
const acc = merged.get(name) ?? { cost: 0, calls: 0, savingsUSD: 0, estimatedCostUSD: 0 }
|
||||
acc.cost += m.cost
|
||||
acc.calls += m.calls
|
||||
acc.savingsUSD += m.savingsUSD ?? 0
|
||||
acc.estimatedCostUSD += m.estimatedCostUSD ?? 0
|
||||
merged.set(name, acc)
|
||||
}
|
||||
return [...merged.entries()]
|
||||
.sort(([, a], [, b]) => b.cost - a.cost)
|
||||
.slice(0, TOP_MODELS_LIMIT)
|
||||
.map(m => ({ name: m.name, cost: m.cost, calls: m.calls, savingsUSD: m.savingsUSD, savingsBaselineModel: '', estimatedCostUSD: m.estimatedCostUSD ?? 0 }))
|
||||
.map(([name, d]) => ({ name, cost: d.cost, calls: d.calls, savingsUSD: d.savingsUSD, savingsBaselineModel: '', estimatedCostUSD: d.estimatedCostUSD }))
|
||||
}
|
||||
|
||||
function buildOptimize(optimize: OptimizeResult | null): MenubarPayload['optimize'] {
|
||||
|
|
|
|||
|
|
@ -290,6 +290,12 @@ const BUILTIN_ALIASES: Record<string, string> = {
|
|||
'kimi-auto': 'kimi-k2-thinking',
|
||||
'kimi-code': 'kimi-k2-thinking',
|
||||
'kimi-for-coding': 'kimi-k2-thinking',
|
||||
// Kimi Code wires report the bare `k3` id in llm.request.model; without an
|
||||
// alias those calls priced at $0 and the provider looked absent in the UI.
|
||||
'k3': 'kimi-k3',
|
||||
// Kimi desktop/IDE embedded runtime serves `k3-agent` / `k2d6-agent`.
|
||||
'k3-agent': 'kimi-k3',
|
||||
'k2d6-agent': 'kimi-k2p6',
|
||||
'mimo-v2-flash': 'xiaomi/mimo-v2-flash',
|
||||
'kat-coder-pro-v1': 'kwaipilot/kat-coder-pro',
|
||||
// Cursor emits dot-version tier-last names plus tier/reasoning suffixes
|
||||
|
|
@ -879,6 +885,8 @@ const SHORT_NAMES: Record<string, string> = {
|
|||
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
||||
'kimi-k2-thinking-turbo': 'Kimi K2 Thinking Turbo',
|
||||
'kimi-k2-thinking': 'Kimi K2 Thinking',
|
||||
'kimi-k3': 'Kimi K3',
|
||||
'kimi-k2p6': 'Kimi K2.6',
|
||||
'kimi-thinking-preview': 'Kimi Thinking',
|
||||
'kimi-k2.6': 'Kimi K2.6',
|
||||
'kimi-k2.5': 'Kimi K2.5',
|
||||
|
|
|
|||
|
|
@ -71,8 +71,18 @@ function timestampIso(value: unknown): string {
|
|||
return Number.isNaN(date.getTime()) ? '' : date.toISOString()
|
||||
}
|
||||
|
||||
function kimicodeHome(override?: string): string {
|
||||
return resolve(override || process.env['KIMI_CODE_HOME'] || join(homedir(), '.kimi-code'))
|
||||
function kimicodeHomes(override?: string): string[] {
|
||||
const explicit = override || process.env['KIMI_CODE_HOME']
|
||||
if (explicit) return [resolve(explicit)]
|
||||
// Default stores. Beyond the CLI's own ~/.kimi-code, embedded runtimes keep
|
||||
// the same wire layout under their own home (Kimi desktop app, Kimi Code
|
||||
// IDE); each home is scanned so embedded-agent usage is not invisible.
|
||||
const home = homedir()
|
||||
const homes = [
|
||||
join(home, '.kimi-code'),
|
||||
join(home, 'Library', 'Application Support', 'kimi-desktop', 'daimon-share', 'daimon', 'runtime', 'kimi-code', 'home'),
|
||||
]
|
||||
return [...new Set(homes.map(h => resolve(h)))]
|
||||
}
|
||||
|
||||
async function directoryEntries(path: string) {
|
||||
|
|
@ -120,7 +130,10 @@ async function discoverSources(root: string): Promise<SessionSource[]> {
|
|||
const workDirPath = join(sessionsDir, workDirEntry.name)
|
||||
|
||||
for (const sessionEntry of await directoryEntries(workDirPath)) {
|
||||
if (!sessionEntry.isDirectory() || !sessionEntry.name.startsWith('session_')) continue
|
||||
// Session dir naming differs by host product: the CLI uses session_*,
|
||||
// embedded runtimes (desktop app, IDE) use conv-*/ctitle-*. Any directory
|
||||
// is accepted; the agents/*/wire.jsonl probe below gates real sessions.
|
||||
if (!sessionEntry.isDirectory()) continue
|
||||
const sessionDir = join(workDirPath, sessionEntry.name)
|
||||
const state = await readState(sessionDir)
|
||||
const project = projectFromWorkDir(state.workDir ?? '', workDirEntry.name)
|
||||
|
|
@ -344,11 +357,15 @@ export function createKimicodeProvider(homeOverride?: string): Provider {
|
|||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: kimicodeHome(homeOverride), label: 'Kimi Code home' }]
|
||||
return kimicodeHomes(homeOverride).map(path => ({ path, label: 'Kimi Code home' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverSources(kimicodeHome(homeOverride))
|
||||
const all: SessionSource[] = []
|
||||
for (const home of kimicodeHomes(homeOverride)) {
|
||||
all.push(...await discoverSources(home))
|
||||
}
|
||||
return all.sort((a, b) => a.path.localeCompare(b.path))
|
||||
},
|
||||
|
||||
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,31 @@ describe('buildMenubarPayload', () => {
|
|||
expect(payload.current.topModels[0].name).toBe('Model0')
|
||||
})
|
||||
|
||||
it('resolves raw model ids to display names in topModels and merges rows that collapse', () => {
|
||||
// Day entries key models by the raw wire id (day-aggregator); the menubar
|
||||
// must show friendly names and merge ids that share one (k3 + kimi-k3).
|
||||
const period: PeriodData = {
|
||||
label: 'Today',
|
||||
cost: 0, calls: 0, sessions: 0,
|
||||
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
|
||||
categories: [],
|
||||
models: [
|
||||
{ name: 'k3', cost: 2.5, calls: 78 },
|
||||
{ name: 'kimi-k3', cost: 0.5, calls: 2 },
|
||||
{ name: 'kimi-for-coding', cost: 0.06, calls: 13 },
|
||||
{ name: 'k3-agent', cost: 1.2, calls: 40 },
|
||||
],
|
||||
}
|
||||
const payload = buildMenubarPayload(period, [], null)
|
||||
const kimiK3 = payload.current.topModels.find(m => m.name === 'Kimi K3')!
|
||||
expect(kimiK3.cost).toBeCloseTo(2.5 + 0.5 + 1.2)
|
||||
expect(kimiK3.calls).toBe(78 + 2 + 40)
|
||||
const k2 = payload.current.topModels.find(m => m.name === 'Kimi K2 Thinking')!
|
||||
expect(k2.cost).toBeCloseTo(0.06)
|
||||
expect(payload.current.topModels.find(m => m.name === 'k3')).toBeUndefined()
|
||||
expect(payload.current.topModels.find(m => m.name === 'k3-agent')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('caps topActivities at 20 so all task categories can surface', () => {
|
||||
const period: PeriodData = {
|
||||
label: 'Today',
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ describe('buildMenubarPayload: local-model savings', () => {
|
|||
const local = payload.current.topModels.find(m => m.name === 'Local Model')!
|
||||
expect(local.savingsUSD).toBe(5)
|
||||
expect(local.cost).toBe(0)
|
||||
const paid = payload.current.topModels.find(m => m.name === 'gpt-4o')!
|
||||
const paid = payload.current.topModels.find(m => m.name === 'GPT-4o')!
|
||||
expect(paid.savingsUSD).toBe(0)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -672,6 +672,9 @@ describe('observed provider model aliases', () => {
|
|||
const cases: Array<[string, string]> = [
|
||||
['MiMo-V2-Flash', 'xiaomi/mimo-v2-flash'],
|
||||
['KAT-Coder-Pro-V1', 'kwaipilot/kat-coder-pro'],
|
||||
// Kimi Code wires report bare `k3` in llm.request.model; it must price
|
||||
// through the kimi-k3 table entry, not fall through to $0.
|
||||
['k3', 'kimi-k3'],
|
||||
]
|
||||
|
||||
for (const [input, expectedModel] of cases) {
|
||||
|
|
@ -685,6 +688,10 @@ describe('observed provider model aliases', () => {
|
|||
})
|
||||
}
|
||||
|
||||
it('k3 shows the Kimi K3 display name', () => {
|
||||
expect(getShortModelName('k3')).toBe('Kimi K3')
|
||||
})
|
||||
|
||||
it('does not map dated Qwen3 Max to a reseller price without provider context', () => {
|
||||
expect(getModelCosts('qwen3-max-2026-01-23')).toBeNull()
|
||||
expect(calculateCost('qwen3-max-2026-01-23', 1_000_000, 1_000_000, 0, 0, 0)).toBe(0)
|
||||
|
|
|
|||
|
|
@ -167,6 +167,40 @@ describe('Kimi Code provider', () => {
|
|||
expect(sources.every(source => source.sourcePath === '/workspace/neutral-project')).toBe(true)
|
||||
})
|
||||
|
||||
it('discovers embedded-runtime conv-* and ctitle-* session directories', async () => {
|
||||
// Embedded runtimes (Kimi desktop app, Kimi Code IDE) name session dirs
|
||||
// conv-*/ctitle-* instead of session_*; their wires carry the same event
|
||||
// format and must be discovered and priced the same way.
|
||||
for (const dirName of ['conv-abc123def456', 'ctitle-019f8f78-db81']) {
|
||||
const agentDir = join(fixtureHome, 'sessions', 'wd_neutral-project_0123456789ab', dirName, 'agents', 'main')
|
||||
await mkdir(agentDir, { recursive: true })
|
||||
await writeFile(join(agentDir, '..', 'state.json'), JSON.stringify({
|
||||
createdAt: '2026-07-01T10:00:00.000Z',
|
||||
updatedAt: '2026-07-01T10:05:00.000Z',
|
||||
workDir: '/workspace/neutral-project',
|
||||
}))
|
||||
await writeFile(join(agentDir, 'wire.jsonl'), [
|
||||
JSON.stringify({ type: 'metadata', protocol_version: '1.4', created_at: 1782900000000 }),
|
||||
JSON.stringify(prompt('hello from an embedded runtime', 1782900000000)),
|
||||
JSON.stringify(request('0.1', 'k3-agent', 'k3-agent', 1782900001000)),
|
||||
JSON.stringify(usage('k3-agent', 1782900002000, { input: 100, output: 50 })),
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
const provider = createKimicodeProvider(fixtureHome)
|
||||
const sources = await provider.discoverSessions()
|
||||
expect(sources).toHaveLength(2)
|
||||
expect(sources.every(source => source.project === 'neutral-project')).toBe(true)
|
||||
|
||||
const seen = new Set<string>()
|
||||
const calls = (await Promise.all(sources.map(source => collect(provider, source, seen)))).flat()
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls.every(call => call.model === 'k3-agent')).toBe(true)
|
||||
// k3-agent prices through the kimi-k3 alias, never $0.
|
||||
expect(calls.every(call => call.costUSD > 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('parses single-turn usage with the real model id and estimated token pricing', async () => {
|
||||
const [wirePath] = await writeSession('single-turn', [{ id: 'main', lines: [
|
||||
prompt('Summarize the neutral module.', 1782900000000),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue