feat: add registry-driven Kimi provider

This commit is contained in:
tommy0103
2026-07-20 22:43:23 +08:00
parent 21c3a1b9fc
commit 3ee44de4e5
39 changed files with 2240 additions and 732 deletions
+139 -7
View File
@@ -7,6 +7,8 @@
// (started_at/ended_at/message_count); persist merges them with any existing row.
import { createRequire } from 'node:module';
import { homedir } from 'node:os';
import { dirname, isAbsolute, join, normalize, relative } from 'node:path';
const require = createRequire(import.meta.url);
const fs = require('node:fs');
@@ -15,7 +17,15 @@ import {
filePath, trunc, truncJson, readLines, discoverJsonlFiles,
} from '../parsing.ts';
import type { Cursor, DiscoverContext, IndexRecord, IndexUnit, Provider } from './types.ts';
import type {
Cursor,
DiscoverContext,
IndexRecord,
IndexUnit,
ProviderAdapter,
RawLookup,
RawRecord,
} from './types.ts';
// Claude cursor encodes the file mtime and the number of lines already indexed:
// "<mtimeMs>:<linesProcessed>". mtime lets discovery detect change; lines lets
@@ -46,8 +56,32 @@ function totalInputTokens(usage: Record<string, unknown>): number | null {
return seen ? total : null;
}
export function discover(_ctx: DiscoverContext): IndexUnit[] {
return discoverJsonlFiles().map((f: any) => ({
function discoverAt(rootDir: string, ctx: DiscoverContext): IndexUnit[] {
const projectsDir = join(rootDir, 'projects');
const changedTranscriptPaths = new Set<string>();
const forcedPaths = new Set<string>();
for (const changedPath of ctx.changedPaths ?? []) {
const absolute = isAbsolute(changedPath)
? normalize(changedPath)
: normalize(join(projectsDir, changedPath));
const inside = relative(projectsDir, absolute);
if (!inside || inside.startsWith('..') || isAbsolute(inside)) continue;
if (absolute.toLowerCase().endsWith('.meta.json')) {
const transcript = absolute.slice(0, -'.meta.json'.length) + '.jsonl';
changedTranscriptPaths.add(transcript);
forcedPaths.add(transcript);
} else if (absolute.toLowerCase().endsWith('.jsonl')) {
changedTranscriptPaths.add(absolute);
}
}
return discoverJsonlFiles(projectsDir).filter((file) => {
const normalizedPath = normalize(file.path);
if (ctx.changedPaths !== undefined && !changedTranscriptPaths.has(normalizedPath)) return false;
const cursor = ctx.lastCursor(file.path);
return forcedPaths.has(normalizedPath)
|| cursor === null
|| Number(cursor.split(':')[0]) < fs.statSync(file.path).mtimeMs;
}).map((f: any) => ({
key: f.path,
sessionId: f.sessionId,
project: f.project,
@@ -57,6 +91,10 @@ export function discover(_ctx: DiscoverContext): IndexUnit[] {
}));
}
export function discover(ctx: DiscoverContext): IndexUnit[] {
return discoverAt(join(homedir(), '.claude'), ctx);
}
export function* parse(unit: IndexUnit, cursor: Cursor): Generator<IndexRecord, Cursor> {
const skip = cursorToSkip(cursor);
const mtime = fs.statSync(unit.key).mtimeMs;
@@ -70,15 +108,28 @@ export function* parse(unit: IndexUnit, cursor: Cursor): Generator<IndexRecord,
title: null as string | null,
n: 0,
};
const subagentStats = {
startedAt: null as string | null,
endedAt: null as string | null,
totalTokens: 0,
};
let lineNum = 0;
readLines(unit.key, (line: string) => {
lineNum++;
if (lineNum <= skip) return;
let obj: any;
try { obj = JSON.parse(line); } catch { return; }
const sid = unit.sessionId;
const ts = obj.timestamp || null;
const msg = obj.message || {};
const usage = msg.usage || {};
if (isSubagent && (obj.type === 'user' || obj.type === 'assistant')) {
if (ts && (!subagentStats.startedAt || ts < subagentStats.startedAt)) subagentStats.startedAt = ts;
if (ts && (!subagentStats.endedAt || ts > subagentStats.endedAt)) subagentStats.endedAt = ts;
subagentStats.totalTokens += (totalInputTokens(usage) ?? 0) + (usage.output_tokens ?? 0);
}
if (lineNum <= skip) return;
if (obj.type === 'ai-title' && obj.aiTitle) { sm.title = obj.aiTitle; return; }
if (obj.type === 'system' && obj.subtype === 'away_summary' && obj.content) {
@@ -97,11 +148,9 @@ export function* parse(unit: IndexUnit, cursor: Cursor): Generator<IndexRecord,
if (obj.version) sm.version = obj.version;
sm.n++;
const msg = obj.message || {};
const text = extractText(msg.content);
const contentType = extractContentType(msg.content);
const isMeta = extractMessageIsMeta(obj, text);
const usage = msg.usage || {};
const aid = isSubagent ? (unit.agentId ?? null) : (obj.agentId || null);
if (obj.uuid) {
@@ -132,6 +181,39 @@ export function* parse(unit: IndexUnit, cursor: Cursor): Generator<IndexRecord,
}
});
if (isSubagent && unit.agentId) {
const metaPath = unit.key.replace(/\.jsonl$/, '.meta.json');
if (fs.existsSync(metaPath)) {
try {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
const workflowRunId = (unit.meta as { workflowRunId?: string } | undefined)?.workflowRunId;
if (workflowRunId) {
records.push({
kind: 'workflow_agent',
agent_id: unit.agentId,
run_id: workflowRunId,
session_id: unit.sessionId,
agent_type: meta.agentType || null,
description: meta.description || null,
});
} else {
const started = subagentStats.startedAt ? new Date(subagentStats.startedAt).getTime() : null;
const ended = subagentStats.endedAt ? new Date(subagentStats.endedAt).getTime() : null;
records.push({
kind: 'subagent',
agent_id: unit.agentId,
session_id: unit.sessionId,
parent_tool_use_id: meta.toolUseId || null,
agent_type: meta.agentType || null,
description: meta.description || null,
duration_ms: started !== null && ended !== null ? ended - started : null,
total_tokens: subagentStats.totalTokens,
});
}
} catch { /* malformed optional subagent metadata */ }
}
}
// Subagent transcripts do not own a session row (matches indexJsonl).
if (!isSubagent) {
records.push({
@@ -146,4 +228,54 @@ export function* parse(unit: IndexUnit, cursor: Cursor): Generator<IndexRecord,
return `${mtime}:${lineNum}`;
}
export const claudeProvider: Provider = { name, discover, parse };
function rawClaude(input: RawLookup): RawRecord | null {
const mainPath = typeof input.session?.jsonl_path === 'string' ? input.session.jsonl_path : null;
if (mainPath === null) return null;
let sourcePath = mainPath;
if (input.agentId !== null) {
const runId = input.workflowAgent?.['run_id'];
sourcePath = typeof runId === 'string'
? join(dirname(mainPath), String(input.session?.id ?? ''), 'subagents', 'workflows', runId, `${input.agentId}.jsonl`)
: join(dirname(mainPath), String(input.session?.id ?? ''), 'subagents', `${input.agentId}.jsonl`);
}
if (!fs.existsSync(sourcePath)) return null;
let found: string | null = null;
readLines(sourcePath, (line: string) => {
if (!line.includes(input.messageUuid)) return;
try {
if (JSON.parse(line)?.uuid === input.messageUuid) {
found = line;
return false;
}
} catch { /* malformed source line */ }
});
const raw = found as string | null;
let messageText: string | null = null;
if (raw !== null) {
try {
const content = JSON.parse(raw)?.message?.content;
if (typeof content === 'string') messageText = content;
else if (Array.isArray(content)) {
const parts = content.map((part) => part?.text ?? part?.thinking).filter((part) => typeof part === 'string');
messageText = parts.length > 0 ? parts.join('\n') : null;
}
} catch { /* malformed source line */ }
}
return raw === null
? null
: { text: raw, totalLength: raw.length, offset: 0, limit: raw.length, hasMore: false, messageText };
}
export function createClaudeProvider({ rootDir = join(homedir(), '.claude') }: { rootDir?: string } = {}): ProviderAdapter {
return {
name,
descriptor: { id: name, name: 'Claude Code', vendor: 'Anthropic', defaultRoot: rootDir, color: '#d97757' },
indexVersionMarker: CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER,
watchRoots: (configuredRoot) => [join(configuredRoot, 'projects')],
discover: (ctx) => discoverAt(rootDir, ctx),
parse,
raw: rawClaude,
};
}
export const claudeProvider = createClaudeProvider();