feat: add registry-driven Kimi provider
This commit is contained in:
@@ -0,0 +1,656 @@
|
||||
import {
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, isAbsolute, join, normalize, relative, sep } from 'node:path';
|
||||
|
||||
import { filePath, projectSlugFromPath, trunc, truncJson } from '../parsing.ts';
|
||||
import type {
|
||||
Cursor,
|
||||
DiscoverContext,
|
||||
IndexRecord,
|
||||
IndexUnit,
|
||||
MessageRecord,
|
||||
ProviderAdapter,
|
||||
RawLookup,
|
||||
RawRecord,
|
||||
SubagentRecord,
|
||||
SummaryRecord,
|
||||
ToolCallRecord,
|
||||
ToolResultRecord,
|
||||
} from './types.ts';
|
||||
|
||||
type JsonRecord = Record<string, any>;
|
||||
|
||||
interface KimiWireFile {
|
||||
readonly agentId: string;
|
||||
readonly main: boolean;
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
interface KimiSessionUnitMeta {
|
||||
readonly kind: 'session';
|
||||
readonly sessionDir: string;
|
||||
readonly statePath: string;
|
||||
readonly wireFiles: readonly KimiWireFile[];
|
||||
readonly currentCursor: Exclude<Cursor, null>;
|
||||
}
|
||||
|
||||
interface LineRecord {
|
||||
readonly line: number;
|
||||
readonly record: JsonRecord;
|
||||
}
|
||||
|
||||
interface ProjectedSession {
|
||||
readonly messages: MessageRecord[];
|
||||
readonly toolCalls: ToolCallRecord[];
|
||||
readonly toolResults: ToolResultRecord[];
|
||||
readonly summaries: SummaryRecord[];
|
||||
readonly subagents: SubagentRecord[];
|
||||
readonly durations: IndexRecord[];
|
||||
readonly mainMessageCount: number;
|
||||
readonly mainWirePath: string;
|
||||
}
|
||||
|
||||
const SOURCE = 'kimi';
|
||||
|
||||
function defaultKimiRoot(): string {
|
||||
return process.env['KIMI_CODE_HOME'] ?? join(homedir(), '.kimi-code');
|
||||
}
|
||||
|
||||
function readState(path: string): JsonRecord {
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
||||
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? parsed as JsonRecord
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function readWire(path: string): LineRecord[] {
|
||||
const lines = readFileSync(path, 'utf8').split('\n');
|
||||
const records: LineRecord[] = [];
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index]!.endsWith('\r') ? lines[index]!.slice(0, -1) : lines[index]!;
|
||||
if (line.length === 0) continue;
|
||||
try {
|
||||
records.push({ line: index + 1, record: JSON.parse(line) as JsonRecord });
|
||||
} catch (error) {
|
||||
if (index === lines.length - 1) break;
|
||||
throw new Error(`wire.jsonl: corrupted line ${index + 1} in ${path}: ${String(error)}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
function listWireFiles(sessionDir: string): KimiWireFile[] {
|
||||
const agentsDir = join(sessionDir, 'agents');
|
||||
const files: KimiWireFile[] = [];
|
||||
if (existsSync(agentsDir)) {
|
||||
for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const path = join(agentsDir, entry.name, 'wire.jsonl');
|
||||
if (existsSync(path)) files.push({ agentId: entry.name, main: entry.name === 'main', path });
|
||||
}
|
||||
}
|
||||
if (!files.some((file) => file.main)) {
|
||||
const legacy = join(sessionDir, 'wire.jsonl');
|
||||
if (existsSync(legacy)) files.push({ agentId: 'main', main: true, path: legacy });
|
||||
}
|
||||
return files.sort((a, b) => Number(b.main) - Number(a.main) || a.agentId.localeCompare(b.agentId));
|
||||
}
|
||||
|
||||
function fileLineCount(path: string): number {
|
||||
const raw = readFileSync(path, 'utf8');
|
||||
if (raw.length === 0) return 0;
|
||||
const newlines = raw.match(/\n/g)?.length ?? 0;
|
||||
return newlines + (raw.endsWith('\n') ? 0 : 1);
|
||||
}
|
||||
|
||||
function cursorFor(statePath: string, wires: readonly KimiWireFile[]): Exclude<Cursor, null> {
|
||||
const paths = [statePath, ...wires.map((wire) => wire.path)].filter(existsSync);
|
||||
let maxMtime = 0;
|
||||
let totalLines = 0;
|
||||
for (const path of paths) {
|
||||
maxMtime = Math.max(maxMtime, statSync(path).mtimeMs);
|
||||
totalLines += fileLineCount(path);
|
||||
}
|
||||
return `${maxMtime}:${totalLines}`;
|
||||
}
|
||||
|
||||
function normalizeTime(value: unknown): string | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
||||
if (typeof value === 'string' && !Number.isNaN(Date.parse(value))) {
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function contentParts(content: unknown): JsonRecord[] {
|
||||
if (typeof content === 'string') return [{ type: 'text', text: content }];
|
||||
return Array.isArray(content)
|
||||
? content.filter((part): part is JsonRecord => part !== null && typeof part === 'object')
|
||||
: [];
|
||||
}
|
||||
|
||||
function partText(part: JsonRecord): string | null {
|
||||
if (part.type === 'text' && typeof part.text === 'string') return trunc(part.text);
|
||||
if (part.type === 'thinking' && typeof part.thinking === 'string') return trunc(part.thinking);
|
||||
return null;
|
||||
}
|
||||
|
||||
function messageText(content: unknown): string | null {
|
||||
const parts = contentParts(content);
|
||||
const text = parts.map(partText).filter((value): value is string => value !== null);
|
||||
return text.length > 0 ? trunc(text.join('\n')) : null;
|
||||
}
|
||||
|
||||
function messageContentType(content: unknown): string {
|
||||
const types = new Set(contentParts(content).map((part) => String(part.type ?? 'unknown')));
|
||||
return types.size === 1 ? [...types][0]! : 'unknown';
|
||||
}
|
||||
|
||||
function namespacedSessionId(nativeId: string): string {
|
||||
return `kimi:${nativeId}`;
|
||||
}
|
||||
|
||||
function namespacedAgentId(sessionId: string, agentId: string): string {
|
||||
return `${sessionId}:${agentId}`;
|
||||
}
|
||||
|
||||
function namespacedEventId(sessionId: string, agentId: string, nativeId: unknown, line: number): string {
|
||||
const suffix = typeof nativeId === 'string' && nativeId.length > 0 ? nativeId : `line-${line}`;
|
||||
return `${sessionId}:${agentId}:${suffix}`;
|
||||
}
|
||||
|
||||
function namespacedToolId(sessionId: string, agentId: string, nativeId: unknown): string {
|
||||
return `${sessionId}:${agentId}:${String(nativeId)}`;
|
||||
}
|
||||
|
||||
function numericField(record: JsonRecord, ...fields: string[]): number | null {
|
||||
const value = fields.map((field) => record[field]).find((candidate) => (
|
||||
typeof candidate === 'number' && Number.isFinite(candidate)
|
||||
));
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function inputUsage(usage: JsonRecord): number | null {
|
||||
const normalized = numericField(usage, 'input_tokens', 'inputTokens');
|
||||
if (normalized !== null) return normalized;
|
||||
const fields = ['inputOther', 'inputCacheRead', 'inputCacheCreation'];
|
||||
const values = fields.map((field) => numericField(usage, field));
|
||||
return values.some((value) => value !== null)
|
||||
? values.reduce<number>((sum, value) => sum + (value ?? 0), 0)
|
||||
: null;
|
||||
}
|
||||
|
||||
function outputUsage(usage: JsonRecord): number | null {
|
||||
return numericField(usage, 'output_tokens', 'outputTokens', 'output');
|
||||
}
|
||||
|
||||
function isRealUserMessage(message: JsonRecord): boolean {
|
||||
if (message.role !== 'user') return false;
|
||||
const origin = message.origin as JsonRecord | undefined;
|
||||
if (origin === undefined || origin.kind === 'user') return true;
|
||||
return (origin.kind === 'skill_activation' || origin.kind === 'plugin_command')
|
||||
&& origin.trigger === 'user-slash';
|
||||
}
|
||||
|
||||
function projectSession(meta: KimiSessionUnitMeta, sessionId: string, state: JsonRecord): ProjectedSession {
|
||||
const cwd = typeof state.cwd === 'string'
|
||||
? state.cwd
|
||||
: typeof state.workDir === 'string'
|
||||
? state.workDir
|
||||
: null;
|
||||
const messages: MessageRecord[] = [];
|
||||
const toolCalls: ToolCallRecord[] = [];
|
||||
const toolResults: ToolResultRecord[] = [];
|
||||
const summaries: SummaryRecord[] = [];
|
||||
const durations: IndexRecord[] = [];
|
||||
const childParentCalls = new Map<string, string>();
|
||||
let mainMessageCount = 0;
|
||||
|
||||
for (const wire of meta.wireFiles) {
|
||||
const wireMessageStart = messages.length;
|
||||
const records = readWire(wire.path);
|
||||
const agentDbId = wire.main ? null : namespacedAgentId(sessionId, wire.agentId);
|
||||
let previousUuid: string | null = null;
|
||||
let model: string | null = null;
|
||||
const stepStarts = new Map<string, number>();
|
||||
const stepMessages = new Map<string, MessageRecord[]>();
|
||||
const callMessageUuids = new Map<string, string>();
|
||||
const injectionMessageUuids = new Set<string>();
|
||||
const realUserMessageUuids = new Set<string>();
|
||||
let undoFloor = wireMessageStart;
|
||||
|
||||
const resetOpenState = (): void => {
|
||||
stepStarts.clear();
|
||||
stepMessages.clear();
|
||||
callMessageUuids.clear();
|
||||
};
|
||||
|
||||
const applyUndo = (count: number): void => {
|
||||
if (count <= 0) return;
|
||||
const removedMessageUuids = new Set<string>();
|
||||
let removedUserCount = 0;
|
||||
for (let index = messages.length - 1; index >= undoFloor; index--) {
|
||||
const message = messages[index]!;
|
||||
if (injectionMessageUuids.has(message.uuid)) continue;
|
||||
messages.splice(index, 1);
|
||||
removedMessageUuids.add(message.uuid);
|
||||
injectionMessageUuids.delete(message.uuid);
|
||||
if (wire.main) mainMessageCount--;
|
||||
if (realUserMessageUuids.delete(message.uuid)) {
|
||||
removedUserCount++;
|
||||
if (removedUserCount >= count) break;
|
||||
}
|
||||
}
|
||||
const removedToolIds = new Set(
|
||||
toolCalls.filter((call) => removedMessageUuids.has(call.message_uuid)).map((call) => call.id),
|
||||
);
|
||||
for (let index = toolCalls.length - 1; index >= 0; index--) {
|
||||
if (removedMessageUuids.has(toolCalls[index]!.message_uuid)) toolCalls.splice(index, 1);
|
||||
}
|
||||
for (let index = toolResults.length - 1; index >= 0; index--) {
|
||||
const result = toolResults[index]!;
|
||||
if (removedMessageUuids.has(result.message_uuid) || removedToolIds.has(result.tool_use_id)) {
|
||||
toolResults.splice(index, 1);
|
||||
}
|
||||
}
|
||||
for (let index = durations.length - 1; index >= 0; index--) {
|
||||
const duration = durations[index]!;
|
||||
if (duration.kind === 'message-turn-duration' && removedMessageUuids.has(duration.uuid)) {
|
||||
durations.splice(index, 1);
|
||||
}
|
||||
}
|
||||
previousUuid = messages.slice(wireMessageStart).at(-1)?.uuid ?? null;
|
||||
resetOpenState();
|
||||
};
|
||||
|
||||
const pushMessage = (message: MessageRecord, stepUuid?: string): void => {
|
||||
messages.push(message);
|
||||
if (wire.main) mainMessageCount++;
|
||||
previousUuid = message.uuid;
|
||||
if (stepUuid !== undefined) {
|
||||
const entries = stepMessages.get(stepUuid) ?? [];
|
||||
entries.push(message);
|
||||
stepMessages.set(stepUuid, entries);
|
||||
}
|
||||
};
|
||||
|
||||
for (const { line, record } of records) {
|
||||
const timestamp = normalizeTime(record.time);
|
||||
if (record.type === 'config.update') {
|
||||
model = typeof record.modelAlias === 'string' ? record.modelAlias : model;
|
||||
continue;
|
||||
}
|
||||
if (record.type === 'context.clear') {
|
||||
undoFloor = messages.length;
|
||||
resetOpenState();
|
||||
continue;
|
||||
}
|
||||
if (record.type === 'context.undo') {
|
||||
applyUndo(typeof record.count === 'number' ? record.count : 0);
|
||||
continue;
|
||||
}
|
||||
if (record.type === 'context.append_message') {
|
||||
const source = record.message as JsonRecord | undefined;
|
||||
if (source === undefined || typeof source.role !== 'string') continue;
|
||||
const uuid = namespacedEventId(sessionId, wire.agentId, source.id, line);
|
||||
const origin = source.origin as JsonRecord | undefined;
|
||||
const messageUuid = uuid;
|
||||
pushMessage({
|
||||
kind: 'message',
|
||||
uuid,
|
||||
session_id: sessionId,
|
||||
type: source.role,
|
||||
parent_uuid: previousUuid,
|
||||
timestamp,
|
||||
role: source.role,
|
||||
text: messageText(source.content),
|
||||
content_type: messageContentType(source.content),
|
||||
is_meta: origin !== undefined && origin.kind !== 'user' ? 1 : 0,
|
||||
model,
|
||||
is_sidechain: wire.main ? 0 : 1,
|
||||
agent_id: agentDbId,
|
||||
input_tokens: null,
|
||||
output_tokens: null,
|
||||
cwd,
|
||||
skill: null,
|
||||
source: SOURCE,
|
||||
});
|
||||
if (origin?.kind === 'injection') injectionMessageUuids.add(messageUuid);
|
||||
if (isRealUserMessage(source)) realUserMessageUuids.add(messageUuid);
|
||||
if (Array.isArray(source.toolCalls)) {
|
||||
for (const call of source.toolCalls) {
|
||||
if (call === null || typeof call !== 'object' || typeof call.id !== 'string') continue;
|
||||
const fn = call.function as JsonRecord | undefined;
|
||||
const name = typeof fn?.name === 'string' ? fn.name : 'tool';
|
||||
let args: unknown = fn?.arguments ?? {};
|
||||
if (typeof args === 'string') {
|
||||
try { args = JSON.parse(args); } catch { args = { raw: args }; }
|
||||
}
|
||||
const toolId = namespacedToolId(sessionId, wire.agentId, call.id);
|
||||
toolCalls.push({
|
||||
kind: 'tool_call',
|
||||
id: toolId,
|
||||
message_uuid: messageUuid,
|
||||
session_id: sessionId,
|
||||
name,
|
||||
input_json: truncJson(args) ?? '{}',
|
||||
file_path: filePath(name, args as JsonRecord | undefined),
|
||||
});
|
||||
callMessageUuids.set(call.id, messageUuid);
|
||||
}
|
||||
}
|
||||
if (source.role === 'tool' && typeof source.toolCallId === 'string') {
|
||||
const toolId = namespacedToolId(sessionId, wire.agentId, source.toolCallId);
|
||||
toolResults.push({
|
||||
kind: 'tool_result',
|
||||
tool_use_id: toolId,
|
||||
message_uuid: messageUuid,
|
||||
session_id: sessionId,
|
||||
content: messageText(source.content) ?? '',
|
||||
file_path: null,
|
||||
is_error: source.isError === true ? 1 : 0,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (record.type === 'context.apply_compaction') {
|
||||
const content = typeof record.contextSummary === 'string'
|
||||
? record.contextSummary
|
||||
: typeof record.summary === 'string'
|
||||
? record.summary
|
||||
: messageText((record.summary as JsonRecord | undefined)?.content);
|
||||
if (content !== null) {
|
||||
summaries.push({
|
||||
kind: 'summary',
|
||||
id: namespacedEventId(sessionId, wire.agentId, undefined, line),
|
||||
session_id: sessionId,
|
||||
timestamp,
|
||||
source: 'compaction',
|
||||
content,
|
||||
});
|
||||
}
|
||||
undoFloor = messages.length;
|
||||
resetOpenState();
|
||||
continue;
|
||||
}
|
||||
if (record.type !== 'context.append_loop_event') continue;
|
||||
const event = record.event as JsonRecord | undefined;
|
||||
if (event === undefined || typeof event.type !== 'string') continue;
|
||||
if (event.type === 'step.begin' && typeof event.uuid === 'string') {
|
||||
stepStarts.set(event.uuid, typeof record.time === 'number' ? record.time : 0);
|
||||
continue;
|
||||
}
|
||||
if (event.type === 'content.part' && typeof event.stepUuid === 'string') {
|
||||
const part = event.part as JsonRecord | undefined;
|
||||
if (part === undefined) continue;
|
||||
pushMessage({
|
||||
kind: 'message',
|
||||
uuid: namespacedEventId(sessionId, wire.agentId, event.uuid, line),
|
||||
session_id: sessionId,
|
||||
type: 'assistant',
|
||||
parent_uuid: previousUuid,
|
||||
timestamp,
|
||||
role: 'assistant',
|
||||
text: partText(part),
|
||||
content_type: typeof part.type === 'string' ? part.type : 'unknown',
|
||||
is_meta: 0,
|
||||
model,
|
||||
is_sidechain: wire.main ? 0 : 1,
|
||||
agent_id: agentDbId,
|
||||
input_tokens: null,
|
||||
output_tokens: null,
|
||||
cwd,
|
||||
skill: null,
|
||||
source: SOURCE,
|
||||
}, event.stepUuid);
|
||||
continue;
|
||||
}
|
||||
if (event.type === 'tool.call' && typeof event.stepUuid === 'string' && event.toolCallId !== undefined) {
|
||||
const uuid = namespacedEventId(sessionId, wire.agentId, event.uuid, line);
|
||||
const toolId = namespacedToolId(sessionId, wire.agentId, event.toolCallId);
|
||||
pushMessage({
|
||||
kind: 'message', uuid, session_id: sessionId, type: 'assistant', parent_uuid: previousUuid,
|
||||
timestamp, role: 'assistant', text: null, content_type: 'tool_use', is_meta: 0, model,
|
||||
is_sidechain: wire.main ? 0 : 1, agent_id: agentDbId, input_tokens: null,
|
||||
output_tokens: null, cwd, skill: null, source: SOURCE,
|
||||
}, event.stepUuid);
|
||||
toolCalls.push({
|
||||
kind: 'tool_call',
|
||||
id: toolId,
|
||||
message_uuid: uuid,
|
||||
session_id: sessionId,
|
||||
name: String(event.name ?? 'tool'),
|
||||
input_json: truncJson(event.args ?? {}) ?? '{}',
|
||||
file_path: filePath(String(event.name ?? 'tool'), event.args as JsonRecord | undefined),
|
||||
});
|
||||
callMessageUuids.set(String(event.toolCallId), uuid);
|
||||
continue;
|
||||
}
|
||||
if (event.type === 'tool.result' && event.toolCallId !== undefined) {
|
||||
const nativeToolId = String(event.toolCallId);
|
||||
const result = event.result as JsonRecord | undefined;
|
||||
const output = result?.output;
|
||||
const content = typeof output === 'string' ? trunc(output) : truncJson(output ?? '') ?? '';
|
||||
const toolId = namespacedToolId(sessionId, wire.agentId, nativeToolId);
|
||||
toolResults.push({
|
||||
kind: 'tool_result',
|
||||
tool_use_id: toolId,
|
||||
message_uuid: callMessageUuids.get(nativeToolId) ?? '',
|
||||
session_id: sessionId,
|
||||
content,
|
||||
file_path: null,
|
||||
is_error: result?.isError === true ? 1 : 0,
|
||||
});
|
||||
const childId = typeof content === 'string' ? /^agent_id:\s*(\S+)/m.exec(content)?.[1] : undefined;
|
||||
if (childId !== undefined) childParentCalls.set(childId, toolId);
|
||||
continue;
|
||||
}
|
||||
if (event.type === 'step.end' && typeof event.uuid === 'string') {
|
||||
const step = stepMessages.get(event.uuid) ?? [];
|
||||
const last = step.at(-1);
|
||||
if (last !== undefined) {
|
||||
const usage = event.usage as JsonRecord | undefined;
|
||||
if (usage !== undefined) {
|
||||
last.input_tokens = inputUsage(usage);
|
||||
last.output_tokens = outputUsage(usage);
|
||||
}
|
||||
const started = stepStarts.get(event.uuid);
|
||||
if (started !== undefined && typeof record.time === 'number' && record.time >= started) {
|
||||
durations.push({ kind: 'message-turn-duration', uuid: last.uuid, turn_duration_ms: record.time - started });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const agents = state.agents as JsonRecord | undefined;
|
||||
const subagents: SubagentRecord[] = [];
|
||||
if (agents !== undefined) {
|
||||
for (const [agentId, candidate] of Object.entries(agents)) {
|
||||
if (agentId === 'main' || candidate === null || typeof candidate !== 'object') continue;
|
||||
const agent = candidate as JsonRecord;
|
||||
const labels = agent.labels as JsonRecord | undefined;
|
||||
subagents.push({
|
||||
kind: 'subagent',
|
||||
agent_id: namespacedAgentId(sessionId, agentId),
|
||||
session_id: sessionId,
|
||||
parent_tool_use_id: childParentCalls.get(agentId) ?? null,
|
||||
agent_type: typeof labels?.profile === 'string'
|
||||
? labels.profile
|
||||
: typeof agent.type === 'string'
|
||||
? agent.type
|
||||
: null,
|
||||
description: typeof agent.swarmItem === 'string' ? agent.swarmItem : null,
|
||||
duration_ms: null,
|
||||
total_tokens: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
toolCalls,
|
||||
toolResults,
|
||||
summaries,
|
||||
subagents,
|
||||
durations,
|
||||
mainMessageCount,
|
||||
mainWirePath: meta.wireFiles.find((wire) => wire.main)?.path ?? join(meta.sessionDir, 'wire.jsonl'),
|
||||
};
|
||||
}
|
||||
|
||||
function sessionDirectories(rootDir: string): string[] {
|
||||
const sessionsDir = join(rootDir, 'sessions');
|
||||
if (!existsSync(sessionsDir)) return [];
|
||||
const result: string[] = [];
|
||||
for (const workspace of readdirSync(sessionsDir, { withFileTypes: true })) {
|
||||
if (!workspace.isDirectory()) continue;
|
||||
const workspaceDir = join(sessionsDir, workspace.name);
|
||||
for (const session of readdirSync(workspaceDir, { withFileTypes: true })) {
|
||||
if (session.isDirectory()) result.push(join(workspaceDir, session.name));
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function changedSessionDirectories(rootDir: string, changedPaths: readonly string[]): Set<string> {
|
||||
const sessionsDir = join(rootDir, 'sessions');
|
||||
const result = new Set<string>();
|
||||
for (const changedPath of changedPaths) {
|
||||
const absolute = isAbsolute(changedPath)
|
||||
? normalize(changedPath)
|
||||
: normalize(join(sessionsDir, changedPath));
|
||||
const inside = relative(sessionsDir, absolute);
|
||||
if (!inside || inside.startsWith('..') || isAbsolute(inside)) continue;
|
||||
const [workspaceId, sessionId] = inside.split(sep);
|
||||
if (workspaceId && sessionId) result.add(join(sessionsDir, workspaceId, sessionId));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function rawFromWire(path: string, messageUuid: string): RawRecord | null {
|
||||
if (!existsSync(path)) return null;
|
||||
const fallbackLine = /:line-(\d+)$/.exec(messageUuid)?.[1];
|
||||
const nativeId = messageUuid.split(':').at(-1);
|
||||
const lines = readFileSync(path, 'utf8').split(/\r?\n/);
|
||||
const line = fallbackLine !== undefined
|
||||
? lines[Number(fallbackLine) - 1]
|
||||
: lines.find((candidate) => nativeId !== undefined && candidate.includes(nativeId));
|
||||
if (!line) return null;
|
||||
let projectedText: string | null = null;
|
||||
try {
|
||||
const record = JSON.parse(line) as JsonRecord;
|
||||
if (record.type === 'context.append_message') {
|
||||
const content = (record.message as JsonRecord | undefined)?.content;
|
||||
const parts = contentParts(content).map((part) => {
|
||||
if (part.type === 'text' && typeof part.text === 'string') return part.text;
|
||||
if (part.type === 'thinking' && typeof part.thinking === 'string') return part.thinking;
|
||||
return null;
|
||||
}).filter((part): part is string => part !== null);
|
||||
projectedText = parts.length > 0 ? parts.join('\n') : null;
|
||||
} else if (record.type === 'context.append_loop_event') {
|
||||
const part = (record.event as JsonRecord | undefined)?.part as JsonRecord | undefined;
|
||||
if (part?.type === 'text' && typeof part.text === 'string') projectedText = part.text;
|
||||
if (part?.type === 'thinking' && typeof part.thinking === 'string') projectedText = part.thinking;
|
||||
}
|
||||
} catch { /* malformed torn source line */ }
|
||||
return {
|
||||
text: line,
|
||||
totalLength: line.length,
|
||||
offset: 0,
|
||||
limit: line.length,
|
||||
hasMore: false,
|
||||
messageText: projectedText,
|
||||
};
|
||||
}
|
||||
|
||||
export function createKimiProvider({ rootDir = defaultKimiRoot() }: { rootDir?: string } = {}): ProviderAdapter {
|
||||
const name = SOURCE;
|
||||
return {
|
||||
name,
|
||||
descriptor: { id: name, name: 'Kimi Code', vendor: 'Moonshot AI', defaultRoot: rootDir, color: '#6d6afc' },
|
||||
watchRoots: (configuredRoot) => [join(configuredRoot, 'sessions'), join(configuredRoot, 'session_index.jsonl')],
|
||||
discover(ctx: DiscoverContext): IndexUnit[] {
|
||||
const units: IndexUnit[] = [];
|
||||
const changedSessions = ctx.changedPaths === undefined
|
||||
? null
|
||||
: changedSessionDirectories(rootDir, ctx.changedPaths);
|
||||
for (const sessionDir of sessionDirectories(rootDir)) {
|
||||
if (changedSessions !== null && !changedSessions.has(sessionDir)) continue;
|
||||
const statePath = join(sessionDir, 'state.json');
|
||||
const wireFiles = listWireFiles(sessionDir);
|
||||
if (wireFiles.length === 0) continue;
|
||||
const currentCursor = cursorFor(statePath, wireFiles);
|
||||
if (changedSessions === null && ctx.lastCursor(sessionDir) === currentCursor) continue;
|
||||
const state = readState(statePath);
|
||||
const cwd = typeof state.cwd === 'string' ? state.cwd : typeof state.workDir === 'string' ? state.workDir : null;
|
||||
units.push({
|
||||
key: sessionDir,
|
||||
sessionId: namespacedSessionId(basename(sessionDir)),
|
||||
project: projectSlugFromPath(cwd) ?? undefined,
|
||||
meta: { kind: 'session', sessionDir, statePath, wireFiles, currentCursor } satisfies KimiSessionUnitMeta,
|
||||
});
|
||||
}
|
||||
return units;
|
||||
},
|
||||
*parse(unit: IndexUnit, _cursor: Cursor): Generator<IndexRecord, Cursor> {
|
||||
const meta = unit.meta as KimiSessionUnitMeta;
|
||||
const before = cursorFor(meta.statePath, meta.wireFiles);
|
||||
const state = readState(meta.statePath);
|
||||
const projected = projectSession(meta, unit.sessionId, state);
|
||||
const after = cursorFor(meta.statePath, meta.wireFiles);
|
||||
if (before !== after) throw new Error(`Kimi session changed while indexing: ${meta.sessionDir}`);
|
||||
|
||||
yield { kind: 'delete-session', sessionId: unit.sessionId };
|
||||
yield {
|
||||
kind: 'session',
|
||||
id: unit.sessionId,
|
||||
title: typeof state.title === 'string'
|
||||
? state.title
|
||||
: typeof state.lastPrompt === 'string'
|
||||
? state.lastPrompt
|
||||
: null,
|
||||
project: unit.project ?? null,
|
||||
started_at: normalizeTime(state.createdAt),
|
||||
ended_at: normalizeTime(state.updatedAt),
|
||||
git_branch: null,
|
||||
version: null,
|
||||
message_count: projected.mainMessageCount,
|
||||
countMode: 'total',
|
||||
jsonl_path: projected.mainWirePath,
|
||||
source: SOURCE,
|
||||
};
|
||||
yield* projected.messages;
|
||||
yield* projected.toolCalls;
|
||||
yield* projected.toolResults;
|
||||
yield* projected.summaries;
|
||||
yield* projected.subagents;
|
||||
yield* projected.durations;
|
||||
return after;
|
||||
},
|
||||
raw(input: RawLookup): RawRecord | null {
|
||||
const mainPath = typeof input.session?.jsonl_path === 'string' ? input.session.jsonl_path : null;
|
||||
if (mainPath === null) return null;
|
||||
if (input.agentId === null) return rawFromWire(mainPath, input.messageUuid);
|
||||
const rawAgentId = input.agentId.split(':').at(-1);
|
||||
if (rawAgentId === undefined) return null;
|
||||
const sessionDir = basename(dirname(mainPath)) === 'main'
|
||||
? dirname(dirname(dirname(mainPath)))
|
||||
: dirname(mainPath);
|
||||
return rawFromWire(join(sessionDir, 'agents', rawAgentId, 'wire.jsonl'), input.messageUuid);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const kimiProvider = createKimiProvider();
|
||||
Reference in New Issue
Block a user