diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 217cd12c020..f380ea6c01f 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -2802,6 +2802,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper set migrationCheckInFlight(val) { self.migrationCheckInFlight = val }, + refreshSessions: () => this.refreshSessions(), disposeGlobal: () => this.disposeGlobal(), broadcastComplete: () => this.connectionService.notifyMigrationComplete(), } diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts b/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts index 5eb1ed51f88..ee8a32f28a2 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts @@ -28,6 +28,7 @@ export interface MigrationContext { readonly client: KiloClient | null readonly extensionContext: MigrationExtensionContext | undefined postMessage(msg: unknown): void + refreshSessions(): void cachedLegacyData: LegacyMigrationData | null migrationCheckInFlight: boolean disposeGlobal(): Promise @@ -69,6 +70,7 @@ export async function checkAndShowMigrationWizard(ctx: MigrationContext): Promis providers: data.providers, mcpServers: data.mcpServers, customModes: data.customModes, + sessions: data.sessions, defaultModel: data.defaultModel, settings: data.settings, }, @@ -88,6 +90,7 @@ export async function handleRequestLegacyMigrationData(ctx: MigrationContext): P providers: data.providers, mcpServers: data.mcpServers, customModes: data.customModes, + sessions: data.sessions, defaultModel: data.defaultModel, settings: data.settings, }, @@ -125,6 +128,7 @@ export async function handleStartLegacyMigration( "completed", ) ctx.broadcastComplete() + ctx.refreshSessions() } ctx.postMessage({ type: "legacyMigrationComplete", results }) diff --git a/packages/kilo-vscode/src/legacy-migration/legacy-types.ts b/packages/kilo-vscode/src/legacy-migration/legacy-types.ts index 3f3af3018f9..c5a9231d5ed 100644 --- a/packages/kilo-vscode/src/legacy-migration/legacy-types.ts +++ b/packages/kilo-vscode/src/legacy-migration/legacy-types.ts @@ -288,6 +288,7 @@ export interface LegacyMigrationData { providers: MigrationProviderInfo[] mcpServers: MigrationMcpServerInfo[] customModes: MigrationCustomModeInfo[] + sessions?: string[] defaultModel?: { provider: string; model: string } settings?: LegacySettings hasData: boolean @@ -318,13 +319,14 @@ export interface MigrationSelections { providers: string[] mcpServers: string[] customModes: string[] + sessions?: string[] defaultModel: boolean settings: MigrationSettingsSelections } export interface MigrationResultItem { item: string - category: "provider" | "mcpServer" | "customMode" | "defaultModel" | "settings" + category: "provider" | "mcpServer" | "customMode" | "defaultModel" | "settings" | "session" status: "success" | "warning" | "error" message?: string } diff --git a/packages/kilo-vscode/src/legacy-migration/migration-service.ts b/packages/kilo-vscode/src/legacy-migration/migration-service.ts index c0334429415..b3e4986aac8 100644 --- a/packages/kilo-vscode/src/legacy-migration/migration-service.ts +++ b/packages/kilo-vscode/src/legacy-migration/migration-service.ts @@ -32,6 +32,7 @@ import type { MigrationCustomModeInfo, MigrationResultItem, } from "./legacy-types" +import { migrate as migrateSession } from "./sessions/migrate" // --------------------------------------------------------------------------- // Constants @@ -68,6 +69,7 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis const mcpSettings = await readLegacyMcpSettings(context) const customModes = await readLegacyCustomModes(context) const settings = readLegacySettings(context) + const sessions = await readSessionsInGlobalStorage(context) const oauthProviders = new Set() const codexRaw = await context.secrets.get(CODEX_OAUTH_SECRET_KEY) @@ -92,18 +94,29 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis Boolean(settings.language) || Boolean(settings.autocomplete) - const hasData = providers.length > 0 || mcpServers.length > 0 || modes.length > 0 || hasSettings + const hasData = + providers.length > 0 || mcpServers.length > 0 || modes.length > 0 || hasSettings || sessions.length > 0 return { providers, mcpServers, customModes: modes, + sessions: sessions.length > 0 ? sessions : undefined, defaultModel, settings: hasSettings ? settings : undefined, hasData, } } +async function readSessionsInGlobalStorage(context: vscode.ExtensionContext) { + const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks") + const items = await vscode.workspace.fs.readDirectory(dir).then( + (items) => items, + () => [] as [string, vscode.FileType][], + ) + return items.filter(([, type]) => type === vscode.FileType.Directory).map(([name]) => name) +} + // --------------------------------------------------------------------------- // Migration // --------------------------------------------------------------------------- @@ -197,6 +210,24 @@ export async function migrate( } } + if (selections.sessions?.length) { + for (const id of selections.sessions) { + onProgress("Chat sessions", "migrating") + const result = await migrateSession(id, context, client) + results.push({ + item: id, + category: "session", + status: result.ok ? "success" : "error", + message: result.ok ? "Session migrated" : "Session migration failed", + }) + onProgress( + "Chat sessions", + result.ok ? "success" : "error", + result.ok ? "Session migrated" : "Session migration failed", + ) + } + } + // Migrate default model if (selections.defaultModel && profiles) { const activeName = profiles.currentApiConfigName diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/ids.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/ids.ts new file mode 100644 index 00000000000..8f15054c049 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/ids.ts @@ -0,0 +1,29 @@ +import { createHash } from "node:crypto" + +export function createProjectID(worktree?: string) { + return hash(worktree ?? "") +} + +export function createSessionID(id: string) { + return prefixed("ses", id) +} + +export function createMessageID(id: string, index: number) { + return prefixed("msg", `${id}:${index}`) +} + +export function createPartID(id: string, index: number, part: number) { + return prefixed("prt", `${id}:${index}:${part}`) +} + +export function createExtraPartID(id: string, index: number, kind: string) { + return prefixed("prt", `${id}:${index}:${kind}`) +} + +function prefixed(prefix: string, value: string) { + return `${prefix}_migrated_${hash(value).slice(0, 26)}` +} + +function hash(value: string) { + return createHash("sha1").update(value).digest("hex") +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/legacy-conversation.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/legacy-conversation.ts new file mode 100644 index 00000000000..e103ef589d2 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/legacy-conversation.ts @@ -0,0 +1,17 @@ +import * as path from "node:path" +import * as vscode from "vscode" +import type { LegacyApiMessage } from "./legacy-types" + +export async function getApiConversationHistory(id: string, dir: string) { + const file = path.join(dir, id, "api_conversation_history.json") + return vscode.workspace.fs.readFile(vscode.Uri.file(file)) +} + +export function parseFile(file: Uint8Array): LegacyApiMessage[] { + const text = Buffer.from(file).toString("utf8") + const json = JSON.parse(text) as unknown + if (!Array.isArray(json)) { + throw new Error("Legacy conversation history must be a JSON array") + } + return json.filter((entry): entry is LegacyApiMessage => Boolean(entry && typeof entry === "object")) +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/legacy-types.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/legacy-types.ts new file mode 100644 index 00000000000..7288fd51a99 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/legacy-types.ts @@ -0,0 +1,28 @@ +import type { Anthropic } from "@anthropic-ai/sdk" + +export interface LegacyHistoryItem { + id: string + rootTaskId?: string + parentTaskId?: string + ts?: number + task?: string + workspace?: string + mode?: string +} + +export type LegacyApiMessage = Anthropic.MessageParam & { + ts?: number + isSummary?: boolean + id?: string + type?: "reasoning" + summary?: unknown[] + encrypted_content?: string + text?: string + reasoning_details?: unknown[] + reasoning_content?: string + condenseId?: string + condenseParent?: string + truncationId?: string + truncationParent?: string + isTruncationMarker?: boolean +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/messages.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/messages.ts new file mode 100644 index 00000000000..e59616216df --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/messages.ts @@ -0,0 +1,82 @@ +import type { KilocodeSessionImportMessageData as Message } from "@kilocode/sdk/v2" +import type { LegacyApiMessage, LegacyHistoryItem } from "./legacy-types" +import { createMessageID, createSessionID } from "./ids" + +type Body = NonNullable +type Data = Body["data"] +type User = Extract +type Assistant = Extract + +export function parseMessagesFromConversation( + conversation: LegacyApiMessage[], + id: string, + item?: LegacyHistoryItem, +): Array> { + return conversation + .filter((entry) => entry.role === "user" || entry.role === "assistant") + .map((entry, index) => parseMessage(entry, index, id, item)) + .filter((message): message is NonNullable => Boolean(message)) +} + +function parseMessage( + entry: LegacyApiMessage, + index: number, + id: string, + item?: LegacyHistoryItem, +): NonNullable | undefined { + const created = entry.ts ?? item?.ts ?? 0 + + if (entry.role === "user") { + const data: User = { + role: "user", + time: { created }, + agent: "user", + model: { + providerID: "legacy", + modelID: "legacy", + }, + } + + return { + id: createMessageID(id, index), + sessionID: createSessionID(id), + timeCreated: created, + data, + } + } + + if (entry.role === "assistant") { + const data: Assistant = { + role: "assistant", + time: { created, completed: created }, + parentID: index > 0 ? createMessageID(id, index - 1) : createMessageID(id, index), + modelID: "legacy", + providerID: "legacy", + mode: item?.mode ?? "code", + agent: "main", + path: { + cwd: item?.workspace ?? "", + root: item?.workspace ?? "", + }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { + read: 0, + write: 0, + }, + }, + } + + return { + id: createMessageID(id, index), + sessionID: createSessionID(id), + timeCreated: created, + data, + } + } + + return undefined +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/merge-tools.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/merge-tools.ts new file mode 100644 index 00000000000..1604654b7d8 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/merge-tools.ts @@ -0,0 +1,66 @@ +import type { KilocodeSessionImportPartData as Part } from "@kilocode/sdk/v2" +import type { LegacyApiMessage } from "../legacy-types" +import { getText, getToolUse, isToolResult, record } from "./parts-util" + +type Body = NonNullable +type Data = Body["data"] +type Tool = Extract +type ToolCompleted = Extract + +// This takes the "tool started" half and the matching "tool finished" half, +// and merges both so the migrated session keeps one complete tool action. +export function mergeToolUseAndResult( + partID: string, + messageID: string, + sessionID: string, + created: number, + conversation: LegacyApiMessage[], + result: { type?: string; tool_use_id?: string; content?: unknown }, +): NonNullable | undefined { + const tool = getToolUseFromConversation(conversation, result.tool_use_id) + if (!tool) return undefined + const callID = typeof tool.id === "string" ? tool.id : partID + const name = typeof tool.name === "string" ? tool.name : "unknown" + const output = getText(result.content) ?? name + const state: ToolCompleted = { + status: "completed", + input: record(tool.input), + output, + title: name, + metadata: {}, + time: { + start: created, + end: created, + }, + } + + const data: Tool = { + type: "tool", + callID, + tool: name, + state, + } + + return { + id: partID, + messageID, + sessionID, + timeCreated: created, + data, + } +} + +export function thereIsNoToolResult(conversation: LegacyApiMessage[], id: string | undefined) { + return !conversation.some( + (entry) => + Array.isArray(entry.content) && entry.content.some((part) => isToolResult(part) && part.tool_use_id === id), + ) +} + +function getToolUseFromConversation(conversation: LegacyApiMessage[], id: string | undefined) { + for (const entry of conversation) { + const tool = getToolUse(entry, id) + if (tool) return tool + } + return undefined +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-builder.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-builder.ts new file mode 100644 index 00000000000..fd91dbaa445 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-builder.ts @@ -0,0 +1,102 @@ +import type { KilocodeSessionImportPartData as Part } from "@kilocode/sdk/v2" +import { cleanLegacyTaskText, isLegacySystemErrorText, record } from "./parts-util" + +type Body = NonNullable +type Data = Body["data"] +type Text = Extract +type Reasoning = Extract +type Tool = Extract +type ToolCompleted = Extract + +export function toTool( + partID: string, + messageID: string, + sessionID: string, + created: number, + part: { type?: string; id?: string; name?: string; input?: unknown }, +): NonNullable { + const tool = typeof part.name === "string" ? part.name : "unknown" + const state: ToolCompleted = { + status: "completed", + input: record(part.input), + output: tool, + title: tool, + metadata: {}, + time: { + start: created, + end: created, + }, + } + + const data: Tool = { + type: "tool", + callID: part.id ?? partID, + tool, + state, + } + + return { + id: partID, + messageID, + sessionID, + timeCreated: created, + data, + } +} + +export function toText( + partID: string, + messageID: string, + sessionID: string, + created: number, + text: string, +): NonNullable { + const value = cleanLegacyTaskText(text) + const data: Text = { + type: "text", + text: value, + ...(isLegacySystemErrorText(value) && { + ignored: true, + metadata: { + source: "legacy-system-error", + }, + }), + time: { + start: created, + end: created, + }, + } + + return { + id: partID, + messageID, + sessionID, + timeCreated: created, + data, + } +} + +export function toReasoning( + partID: string, + messageID: string, + sessionID: string, + created: number, + text: string, +): NonNullable { + const data: Reasoning = { + type: "reasoning", + text, + time: { + start: created, + end: created, + }, + } + + return { + id: partID, + messageID, + sessionID, + timeCreated: created, + data, + } +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts new file mode 100644 index 00000000000..d6359eafd9d --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts @@ -0,0 +1,132 @@ +import type { LegacyApiMessage } from "../legacy-types" + +type ToolUse = { + type?: string + id?: string + name?: string + input?: unknown +} + +export function isSimpleText(input: LegacyApiMessage): input is LegacyApiMessage & { content: string } { + return typeof input.content === "string" && Boolean(input.content) +} + +export function isReasoning(input: LegacyApiMessage): input is LegacyApiMessage & { type: "reasoning"; text: string } { + return input.type === "reasoning" && typeof input.text === "string" && Boolean(input.text) +} + +export function isProviderSpecificReasoning(input: LegacyApiMessage) { + return Boolean(getReasoningText(input)) +} + +export function getReasoningText(input: LegacyApiMessage) { + if (typeof input.reasoning_content === "string" && input.reasoning_content.trim()) { + // Some providers store the model thinking outside normal content blocks, so we need to lift it manually. + return input.reasoning_content.trim() + } + + if (!Array.isArray(input.reasoning_details)) return undefined + + const text = input.reasoning_details + .flatMap((item) => { + if (!item || typeof item !== "object") return [] + if (typeof (item as { text?: unknown }).text === "string") return [(item as { text: string }).text] + if (typeof (item as { reasoning?: unknown }).reasoning === "string") + return [(item as { reasoning: string }).reasoning] + return [] + }) + .join("\n") + .trim() + + // reasoning_details can come as provider-specific arrays, so we collapse the readable text we can find. + return text || undefined +} + +export function isSingleTextWithinMessage(input: unknown): input is { type?: string; text: string } { + return isText(input) && Boolean(input.text) +} + +export function isEnvironmentDetails(input: string) { + return /^\s*[\s\S]*<\/environment_details>\s*$/i.test(input) +} + +export function isCompletionResult( + input: unknown, +): input is { type?: string; name?: string; input: { result: string } } { + return Boolean( + input && + typeof input === "object" && + "type" in input && + input.type === "tool_use" && + "name" in input && + input.name === "attempt_completion" && + "input" in input && + input.input && + typeof input.input === "object" && + "result" in input.input && + typeof input.input.result === "string" && + input.input.result, + ) +} + +export function record(input: unknown): Record { + if (!input || typeof input !== "object" || Array.isArray(input)) return {} + return input as Record +} + +export function isToolUse(input: unknown): input is { type?: string; id?: string; name?: string; input?: unknown } { + return Boolean(input && typeof input === "object" && "type" in input && input.type === "tool_use") +} + +export function isText(input: unknown): input is { type?: string; text?: string } { + return Boolean(input && typeof input === "object" && "type" in input && input.type === "text") +} + +export function isToolResult(input: unknown): input is { type?: string; tool_use_id?: string; content?: unknown } { + return Boolean(input && typeof input === "object" && "type" in input && input.type === "tool_result") +} + +// This looks through the blocks inside one legacy message and finds the tool_use whose id +// matches the tool_result we are processing, so we know which tool call produced that result. +export function getToolUse(input: LegacyApiMessage, id: string | undefined) { + if (!Array.isArray(input.content)) return undefined + return input.content.find((part) => isToolUse(part) && part.id === id) as ToolUse | undefined +} + +export function getText(input: unknown) { + if (typeof input === "string") return input + if (!Array.isArray(input)) return undefined + const text = input + .flatMap((item) => { + if (isText(item) && item.text) return [item.text] + return [] + }) + .join("\n") + .trim() + return text || undefined +} + +export function cleanLegacyTaskText(input: string) { + // Legacy sometimes stores the real user prompt wrapped inside ..., followed by + // extra ... prompt scaffolding. We only want the + // actual task text to appear in the migrated conversation, so if a block exists we keep + // just that inner text and drop the wrapper plus the extra environment block. + const task = input.match(/([\s\S]*?)<\/task>/i)?.[1]?.trim() + if (task) return task + + if (isEnvironmentDetails(input)) return "" + + return input +} + +export function isLegacySystemErrorText(input: string) { + return input.trimStart().startsWith("[ERROR]") +} + +export function getFeedbackText(input: unknown) { + const text = getText(input) + if (!text) return undefined + const match = text.match(/([\s\S]*?)<\/feedback>/i) + const value = match?.[1]?.trim() + return value || undefined +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts.ts new file mode 100644 index 00000000000..4b122a2f0a5 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts.ts @@ -0,0 +1,107 @@ +import type { KilocodeSessionImportPartData as Part } from "@kilocode/sdk/v2" +import type { LegacyApiMessage, LegacyHistoryItem } from "../legacy-types" +import { createExtraPartID, createMessageID, createPartID, createSessionID } from "../ids" +import { toReasoning, toText, toTool } from "./parts-builder" +import { + getFeedbackText, + isCompletionResult, + isEnvironmentDetails, + getReasoningText, + isProviderSpecificReasoning, + isReasoning, + isSimpleText, + isSingleTextWithinMessage, + isToolResult, + isToolUse, +} from "./parts-util" +import { mergeToolUseAndResult, thereIsNoToolResult } from "./merge-tools" + +export function parsePartsFromConversation( + conversation: LegacyApiMessage[], + id: string, + item?: LegacyHistoryItem, +): Array> { + const list = conversation.filter((entry) => entry.role === "user" || entry.role === "assistant") + return list.flatMap((entry, index) => parseParts(entry, index, id, list, item)) +} + +function parseParts( + entry: LegacyApiMessage, + index: number, + id: string, + conversation: LegacyApiMessage[], + item?: LegacyHistoryItem, +): Array> { + const messageID = createMessageID(id, index) + const sessionID = createSessionID(id) + const created = entry.ts ?? item?.ts ?? 0 + + if (isSimpleText(entry)) { + // Ignore raw blocks because they are legacy prompt scaffolding, + // not actual user-visible conversation content we want to preserve in the migrated session. + if (isEnvironmentDetails(entry.content)) return [] + return [toText(createPartID(id, index, 0), messageID, sessionID, created, entry.content)] + } + + if (!Array.isArray(entry.content)) return [] + + const parts: Array> = [] + + if (isReasoning(entry)) { + parts.push(toReasoning(createExtraPartID(id, index, "reasoning"), messageID, sessionID, created, entry.text)) + } + + // Some providers store thinking outside normal content blocks, so this handles those provider-specific fields. + if (!isReasoning(entry) && isProviderSpecificReasoning(entry)) { + const reasoning = getReasoningText(entry) + if (reasoning) { + parts.push( + toReasoning(createExtraPartID(id, index, "provider-reasoning"), messageID, sessionID, created, reasoning), + ) + } + } + + entry.content.forEach((part, partIndex) => { + const partID = createPartID(id, index, partIndex) + + // Legacy can store a message as several pieces; this handles one text block inside that larger message. + if (isSingleTextWithinMessage(part)) { + // Ignore standalone text blocks for the same reason: they describe + // editor/runtime context for the old prompt, but they are not meaningful chat content. + if (isEnvironmentDetails(part.text)) return + parts.push(toText(partID, messageID, sessionID, created, part.text)) + return + } + + // The legacy session can contain a final completion message after an assistant interaction. + // Treat it like a regular assistant text part so the migrated session keeps that final visible answer. + if (isCompletionResult(part)) { + const text = part.input.result + parts.push(toText(partID, messageID, sessionID, created, text)) + return + } + + if (isToolUse(part) && thereIsNoToolResult(conversation, part.id)) { + parts.push(toTool(partID, messageID, sessionID, created, part)) + return + } + + if (isToolResult(part)) { + const feedback = getFeedbackText(part.content) + if (feedback) { + parts.push( + toText(createExtraPartID(id, index, `feedback-${partIndex}`), messageID, sessionID, created, feedback), + ) + } + + // tool_result usually lives in the following user message, while the matching tool_use lives + // in the earlier assistant message, so we need the whole conversation to reconcile both halves. + const tool = mergeToolUseAndResult(partID, messageID, sessionID, created, conversation, part) + if (!tool) return + parts.push(tool) + return + } + }) + + return parts +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/project.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/project.ts new file mode 100644 index 00000000000..b073f535d19 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/project.ts @@ -0,0 +1,29 @@ +import type { KilocodeSessionImportProjectData as Project } from "@kilocode/sdk/v2" +import type { LegacyHistoryItem } from "./legacy-types" +import { createProjectID } from "./ids" + +export function createProject(item?: LegacyHistoryItem): NonNullable { + const project = makeProject() + + project.id = createProjectID(item?.workspace) + + project.worktree = item?.workspace ?? "" + + project.sandboxes = item?.workspace ? [item.workspace] : [] + + project.timeCreated = item?.ts ?? 0 + + project.timeUpdated = item?.ts ?? 0 + + return project +} + +function makeProject(): NonNullable { + return { + id: "", + worktree: "", + sandboxes: [], + timeCreated: 0, + timeUpdated: 0, + } +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts new file mode 100644 index 00000000000..14058c6e94f --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts @@ -0,0 +1,42 @@ +import type { KilocodeSessionImportSessionData as Session } from "@kilocode/sdk/v2" +import type { LegacyHistoryItem } from "./legacy-types" +import { createSessionID } from "./ids" + +export function createSession( + id: string, + item: LegacyHistoryItem | undefined, + projectID: string, +): NonNullable { + const session = makeSession() + + session.id = createSessionID(id) + + session.projectID = projectID + + session.slug = id + + session.directory = item?.workspace ?? "" + + session.title = item?.task ?? id + + session.version = "v2" + + session.timeCreated = item?.ts ?? 0 + + session.timeUpdated = item?.ts ?? 0 + + return session +} + +function makeSession(): NonNullable { + return { + id: "", + projectID: "", + slug: "", + directory: "", + title: "", + version: "", + timeCreated: 0, + timeUpdated: 0, + } +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts b/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts new file mode 100644 index 00000000000..8ca481e3178 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts @@ -0,0 +1,52 @@ +import * as vscode from "vscode" +import type { KiloClient } from "@kilocode/sdk/v2/client" +import type { LegacyHistoryItem } from "./lib/legacy-types" +import { parseSession } from "./parser" + +export async function migrate(id: string, context: vscode.ExtensionContext, client: KiloClient) { + const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath + const items = context.globalState.get("taskHistory", []) + const item = items.find((item) => item.id === id) + const payload = await parseSession(id, dir, item) + + try { + const project = await client.kilocode.sessionImport.project(payload.project, { throwOnError: true }) + const projectID = project.data?.id ?? payload.project.id + const session = await client.kilocode.sessionImport.session( + { + ...payload.session, + projectID, + query_directory: payload.session.directory, + body_directory: payload.session.directory, + }, + { throwOnError: true }, + ) + // Skip child imports when the session already exists so rerunning migration only imports missing sessions. + if (session.data?.skipped) { + return { + ok: true, + skipped: true, + payload, + } + } + + for (const msg of payload.messages) { + await client.kilocode.sessionImport.message(msg, { throwOnError: true }) + } + + for (const part of payload.parts) { + await client.kilocode.sessionImport.part(part, { throwOnError: true }) + } + + return { + ok: true, + payload, + } + } catch (error) { + return { + ok: false, + payload, + error, + } + } +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts b/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts new file mode 100644 index 00000000000..76a063b3f80 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts @@ -0,0 +1,35 @@ +import type { LegacyHistoryItem } from "./lib/legacy-types" +import type { + KilocodeSessionImportMessageData as Message, + KilocodeSessionImportPartData as Part, + KilocodeSessionImportProjectData as Project, + KilocodeSessionImportSessionData as Session, +} from "@kilocode/sdk/v2" +import { getApiConversationHistory, parseFile } from "./lib/legacy-conversation" +import { parseMessagesFromConversation } from "./lib/messages" +import { parsePartsFromConversation } from "./lib/parts/parts" +import { createProject } from "./lib/project" +import { createSession } from "./lib/session" + +export interface NormalizedSession { + project: NonNullable + session: NonNullable + messages: Array> + parts: Array> +} + +export async function parseSession(id: string, dir: string, item?: LegacyHistoryItem): Promise { + const project = createProject(item) + const session = createSession(id, item, project.id) + const file = await getApiConversationHistory(id, dir) + const conversation = parseFile(file) + const messages = parseMessagesFromConversation(conversation, id, item) + const parts = parsePartsFromConversation(conversation, id, item) + + return { + project, + session, + messages, + parts, + } +} diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/messages.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/messages.test.ts new file mode 100644 index 00000000000..69450d78025 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/messages.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "bun:test" +import { parseMessagesFromConversation } from "../../../src/legacy-migration/sessions/lib/messages" +import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types" + +const id = "019d3df5-d5d9-73dc-bc2c-43a6304ac62c" +const item = { + id, + ts: 1774861014564, + task: "In this folder I need you to create 3 python files with random content, oriented towards web development", + workspace: "/workspace/testing-4", + mode: "code", +} + +function sample(): LegacyApiMessage[] { + return [ + { + role: "user", + content: [ + { + type: "text", + text: "\nIn this folder I need you to create 3 python files with random content, oriented towards web development\n", + }, + ], + ts: 1774861014564, + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "\n\nI'll create 3 Python files with web development-oriented content.", + }, + ], + ts: 1774861031791, + }, + { + role: "system", + content: "ignored", + ts: 1774861032000, + } as unknown as LegacyApiMessage, + ] +} + +describe("legacy migration messages", () => { + it("parses a basic legacy conversation into ordered user and assistant messages with stable ids", () => { + const list = parseMessagesFromConversation(sample(), id, item) + + expect(list).toHaveLength(2) + expect(list[0]?.data.role).toBe("user") + expect(list[1]?.data.role).toBe("assistant") + expect(list[0]?.id).toBeDefined() + expect(list[1]?.id).toBeDefined() + }) + + it("creates valid assistant message metadata for the SDK/backend shape", () => { + const list = parseMessagesFromConversation(sample(), id, item) + const msg = list.find((x) => x.data.role === "assistant") + + expect(msg?.data.role).toBe("assistant") + if (msg?.data.role !== "assistant") throw new Error("assistant message not found") + expect(msg.data.mode).toBe("code") + expect(msg.data.path.cwd).toBe("/workspace/testing-4") + expect(msg.data.path.root).toBe("/workspace/testing-4") + expect(msg.data.tokens.input).toBe(0) + expect(msg.data.tokens.output).toBe(0) + }) + + it("ignores unsupported legacy entries instead of producing broken messages", () => { + const list = parseMessagesFromConversation(sample(), id, item) + + expect(list).toHaveLength(2) + expect(list.some((x) => x.data.role !== "user" && x.data.role !== "assistant")).toBe(false) + }) + + it("keeps assistant parentID pointing to the previous imported message when skipped entries exist", () => { + const list = parseMessagesFromConversation( + [ + { + role: "user", + content: "hello", + ts: 1, + }, + { + role: "system", + content: "ignored", + ts: 2, + } as unknown as LegacyApiMessage, + { + role: "assistant", + content: "hi", + ts: 3, + }, + ], + id, + item, + ) + + const user = list[0] + const assistant = list[1] + + expect(user?.id).toBeDefined() + expect(assistant?.data.role).toBe("assistant") + if (assistant?.data.role !== "assistant") throw new Error("assistant message not found") + expect(assistant.data.parentID).toBe(user?.id) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/migrate.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/migrate.test.ts new file mode 100644 index 00000000000..5eede710e73 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/migrate.test.ts @@ -0,0 +1,212 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test" + +const parseSession = mock(async () => ({ + project: { + id: "project-local", + worktree: "/workspace/testing", + sandboxes: ["/workspace/testing"], + timeCreated: 1, + timeUpdated: 1, + }, + session: { + id: "ses_local", + projectID: "project-local", + slug: "legacy-task-1", + directory: "/workspace/testing", + title: "Legacy task", + version: "v2", + timeCreated: 1, + timeUpdated: 1, + }, + messages: [], + parts: [], +})) + +mock.module("../../../src/legacy-migration/sessions/parser", () => ({ + parseSession, +})) + +const { migrate } = await import("../../../src/legacy-migration/sessions/migrate") + +function ctx() { + return { + globalStorageUri: { path: "/storage" }, + globalState: { + get: (key: string) => { + if (key === "taskHistory") return [{ id: "legacy-task-1", workspace: "/workspace/testing" }] + return undefined + }, + update: async () => undefined, + }, + } +} + +function client() { + const calls: Array<{ name: string; body: unknown }> = [] + return { + calls, + kilocode: { + sessionImport: { + project: async (body: unknown) => { + calls.push({ name: "project", body }) + return { data: { id: "project-real" } } + }, + session: async (body: unknown) => { + calls.push({ name: "session", body }) + return { data: { ok: true } } + }, + message: async (body: unknown) => { + calls.push({ name: "message", body }) + return { data: { ok: true } } + }, + part: async (body: unknown) => { + calls.push({ name: "part", body }) + return { data: { ok: true } } + }, + }, + }, + } +} + +describe("legacy migration migrate", () => { + beforeEach(() => { + parseSession.mockClear() + }) + + it("inserts project then session then message then part in the correct order", async () => { + const calls: string[] = [] + const api = { + kilocode: { + sessionImport: { + project: async () => { + calls.push("project") + return { data: { id: "project-real" } } + }, + session: async () => { + calls.push("session") + return { data: { ok: true } } + }, + message: async () => { + calls.push("message") + return { data: { ok: true } } + }, + part: async () => { + calls.push("part") + return { data: { ok: true } } + }, + }, + }, + } + + parseSession.mockResolvedValueOnce({ + project: { + id: "project-local", + worktree: "/workspace/testing", + sandboxes: ["/workspace/testing"], + timeCreated: 1, + timeUpdated: 1, + }, + session: { + id: "ses_local", + projectID: "project-local", + slug: "legacy-task-1", + directory: "/workspace/testing", + title: "Legacy task", + version: "v2", + timeCreated: 1, + timeUpdated: 1, + }, + messages: [ + { + id: "msg_1", + sessionID: "ses_local", + timeCreated: 1, + data: { + role: "user", + time: { created: 1 }, + agent: "user", + model: { providerID: "legacy", modelID: "legacy" }, + }, + }, + ] as never, + parts: [ + { + id: "prt_1", + sessionID: "ses_local", + messageID: "msg_1", + timeCreated: 1, + data: { + type: "text", + text: "hello", + time: { start: 1, end: 1 }, + }, + }, + ] as never, + }) + + await migrate("legacy-task-1", ctx() as never, api as never) + + expect(calls).toEqual(["project", "session", "message", "part"]) + }) + + it("uses the projectID returned by backend when inserting the session", async () => { + const api = client() + + await migrate("legacy-task-1", ctx() as never, api as never) + + const call = api.calls.find((x) => x.name === "session") + expect(call).toBeDefined() + expect((call?.body as { projectID: string }).projectID).toBe("project-real") + }) + + it("returns ok false without throwing when one backend insert fails", async () => { + const api = { + kilocode: { + sessionImport: { + project: async () => ({ data: { id: "project-real" } }), + session: async () => { + throw new Error("session failed") + }, + message: async () => ({ data: { ok: true } }), + part: async () => ({ data: { ok: true } }), + }, + }, + } + + const result = await migrate("legacy-task-1", ctx() as never, api as never) + + expect(result.ok).toBe(false) + }) + + it("skips message and part imports when backend reports the session already exists", async () => { + const calls: string[] = [] + const api = { + kilocode: { + sessionImport: { + project: async () => { + calls.push("project") + return { data: { id: "project-real" } } + }, + session: async () => { + calls.push("session") + return { data: { ok: true, skipped: true } } + }, + message: async () => { + calls.push("message") + return { data: { ok: true } } + }, + part: async () => { + calls.push("part") + return { data: { ok: true } } + }, + }, + }, + } + + const result = await migrate("legacy-task-1", ctx() as never, api as never) + + expect(result.ok).toBe(true) + expect(result).toHaveProperty("skipped", true) + expect(calls).toEqual(["project", "session"]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/parser.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/parser.test.ts new file mode 100644 index 00000000000..db2477b265d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/parser.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "bun:test" +import { createProject } from "../../../src/legacy-migration/sessions/lib/project" +import { createSession } from "../../../src/legacy-migration/sessions/lib/session" +import { createMessageID, createPartID } from "../../../src/legacy-migration/sessions/lib/ids" + +const id = "019d3df5-d5d9-73dc-bc2c-43a6304ac62c" +const item = { + id, + ts: 1774861014564, + task: "In this folder I need you to create 3 python files with random content, oriented towards web development", + workspace: "/workspace/testing-4", + mode: "code", +} + +describe("legacy migration parser", () => { + it("uses the final deterministic ids expected for migration", () => { + const project = createProject(item) + const session = createSession(id, item, project.id) + const msg1 = createMessageID(id, 0) + const msg2 = createMessageID(id, 1) + const prt1 = createPartID(id, 0, 0) + const prt2 = createPartID(id, 1, 0) + + expect(project.id).toBe(createProject(item).id) + expect(session.id).toBe(createSession(id, item, project.id).id) + expect(session.id.startsWith("ses_migrated_")).toBe(true) + expect(msg1).toBe(createMessageID(id, 0)) + expect(msg2).toBe(createMessageID(id, 1)) + expect(msg1.startsWith("msg_migrated_")).toBe(true) + expect(msg2.startsWith("msg_migrated_")).toBe(true) + expect(prt1).toBe(createPartID(id, 0, 0)) + expect(prt2).toBe(createPartID(id, 1, 0)) + expect(prt1.startsWith("prt_migrated_")).toBe(true) + expect(prt2.startsWith("prt_migrated_")).toBe(true) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/parts.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/parts.test.ts new file mode 100644 index 00000000000..60b81062410 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/parts.test.ts @@ -0,0 +1,224 @@ +import { describe, expect, it } from "bun:test" +import { parsePartsFromConversation } from "../../../src/legacy-migration/sessions/lib/parts/parts" +import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types" + +type Data = ReturnType[number]["data"] +type Text = Extract + +function text(list: ReturnType) { + return list.filter((x): x is (typeof list)[number] & { data: Text } => x.data.type === "text").map((x) => x.data.text) +} + +const id = "019d3df5-d5d9-73dc-bc2c-43a6304ac62c" +const item = { + id, + ts: 1774861014564, + task: "In this folder I need you to create 3 python files with random content, oriented towards web development", + workspace: "/workspace/testing-4", + mode: "code", +} + +function sample(): LegacyApiMessage[] { + return [ + { + role: "user", + content: [ + { + type: "text", + text: "\nIn this folder I need you to create 3 python files with random content, oriented towards web development\n", + }, + { + type: "text", + text: "\nCurrent time: 2026-03-30T12:54:59+02:00\n", + }, + ], + ts: 1774861014564, + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "\n\nI'll create 3 Python files with web development-oriented content.", + }, + ], + ts: 1774861031791, + }, + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu_attempt_1", + name: "attempt_completion", + input: { + result: "Created 3 Python files with web development content.", + }, + }, + ], + ts: 1774861079951, + }, + ] +} + +describe("legacy migration parts", () => { + it("converts plain text and text blocks into visible text parts without losing content", async () => { + const list = parsePartsFromConversation(sample(), id, item) + + const items = text(list) + + expect(items.some((x) => x.includes("In this folder I need you to create 3 python files"))).toBe(true) + expect(items.some((x) => x.includes("I'll create 3 Python files with web development-oriented content."))).toBe( + true, + ) + }) + + it("drops standalone environment_details blocks but keeps the real task text", async () => { + const list = parsePartsFromConversation(sample(), id, item) + + const items = text(list) + + expect(items.some((x) => x.includes(""))).toBe(false) + expect(items.some((x) => x.includes("In this folder I need you to create 3 python files"))).toBe(true) + }) + + it("keeps only the first task block content when text exists outside the legacy task wrapper", () => { + const list = parsePartsFromConversation( + [ + { + role: "user", + content: [ + { + type: "text", + text: "Some preamble\nactual task\nSome postamble", + }, + ], + ts: 1774861014564, + }, + ] as LegacyApiMessage[], + id, + item, + ) + + expect(text(list)).toEqual(["actual task"]) + }) + + it("preserves attempt_completion input.result as assistant-visible text", async () => { + const list = parsePartsFromConversation(sample(), id, item) + + const items = text(list) + + expect(items.some((x) => x.includes("Created 3 Python files with web development content"))).toBe(true) + }) + + it("adds a visible user text part for feedback embedded in tool_result", () => { + const list = parsePartsFromConversation( + [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu_feedback_1", + name: "attempt_completion", + input: { + result: "Done.", + }, + }, + ], + ts: 10, + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_feedback_1", + content: [ + { + type: "text", + text: "The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n\nAdd two more lines\n", + }, + ], + }, + ], + ts: 11, + }, + ] as LegacyApiMessage[], + id, + item, + ) + + expect(text(list)).toContain("Add two more lines") + }) + + it("does not create parts for skipped legacy entries like system messages", () => { + const list = parsePartsFromConversation( + [ + { + role: "system", + content: "ignored", + ts: 1774861015000, + } as unknown as LegacyApiMessage, + ], + id, + item, + ) + + expect(list).toEqual([]) + }) + + it("keeps part message ids aligned with imported message ids when skipped entries exist", () => { + const list = parsePartsFromConversation( + [ + { + role: "user", + content: "hello", + ts: 1, + }, + { + role: "system", + content: "ignored", + ts: 2, + } as unknown as LegacyApiMessage, + { + role: "assistant", + content: "hi", + ts: 3, + }, + ], + id, + item, + ) + + const ids = list.map((x) => x.messageID) + + expect(ids.some((x) => x === "msg_legacy-task-1_2")).toBe(false) + }) + + it("uses non-colliding ids for reasoning and normal content parts in the same message", () => { + const list = parsePartsFromConversation( + [ + { + role: "assistant", + type: "reasoning", + text: "Think first", + content: [ + { + type: "text", + text: "Visible answer", + }, + ], + ts: 1774861015000, + } as unknown as LegacyApiMessage, + ], + id, + item, + ) + + const ids = list.map((x) => x.id) + + expect(ids).toHaveLength(2) + expect(new Set(ids).size).toBe(2) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/project.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/project.test.ts new file mode 100644 index 00000000000..2f7331af489 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/project.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "bun:test" +import { createProject } from "../../../src/legacy-migration/sessions/lib/project" + +describe("legacy migration project", () => { + it("builds a project from the legacy workspace and timestamp", () => { + const project = createProject({ + id: "legacy-task-1", + ts: 1774861014564, + workspace: "/workspace/testing", + task: "Understand this project", + mode: "code", + }) + + expect(project.worktree).toBe("/workspace/testing") + expect(project.sandboxes).toEqual(["/workspace/testing"]) + expect(project.timeCreated).toBe(1774861014564) + expect(project.timeUpdated).toBe(1774861014564) + }) + + it("creates a deterministic project id for the same workspace", () => { + const a = createProject({ id: "legacy-1", workspace: "/workspace/testing" }) + const b = createProject({ id: "legacy-1", workspace: "/workspace/testing" }) + + expect(a.id).toBe(b.id) + }) + + it("falls back to empty defaults when legacy workspace data is missing", () => { + const project = createProject() + + expect(project.worktree).toBe("") + expect(project.sandboxes).toEqual([]) + expect(project.timeCreated).toBe(0) + expect(project.timeUpdated).toBe(0) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/reasoning.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/reasoning.test.ts new file mode 100644 index 00000000000..f5c9a688849 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/reasoning.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from "bun:test" +import { parsePartsFromConversation } from "../../../src/legacy-migration/sessions/lib/parts/parts" +import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types" + +type Reasoning = Extract[number]["data"], { type: "reasoning" }> + +function reasoning(list: ReturnType) { + return list + .filter((x): x is (typeof list)[number] & { data: Reasoning } => x.data.type === "reasoning") + .map((x) => x.data.text) +} + +const id = "legacy-reasoning-1" +const item = { + id, + ts: 1774861014564, + task: "Reasoning parsing test", + workspace: "/workspace/testing", + mode: "code", +} + +describe("legacy migration reasoning", () => { + it("converts type reasoning plus text into a reasoning part", () => { + const list = parsePartsFromConversation( + [ + { + role: "assistant", + type: "reasoning", + text: "I should inspect the files first.", + content: [], + ts: 1774861015000, + } as unknown as LegacyApiMessage, + ], + id, + item, + ) + + const out = reasoning(list) + expect(out).toEqual(["I should inspect the files first."]) + }) + + it("extracts reasoning from reasoning_content", () => { + const list = parsePartsFromConversation( + [ + { + role: "assistant", + content: [], + reasoning_content: "I should read the markdown before answering.", + ts: 1774861015000, + } as unknown as LegacyApiMessage, + ], + id, + item, + ) + + const out = reasoning(list) + expect(out).toEqual(["I should read the markdown before answering."]) + }) + + it("extracts readable reasoning text from reasoning_details", () => { + const list = parsePartsFromConversation( + [ + { + role: "assistant", + content: [], + reasoning_details: [{ text: "First inspect the repo." }, { reasoning: "Then summarize the structure." }], + ts: 1774861015000, + } as unknown as LegacyApiMessage, + ], + id, + item, + ) + + const out = reasoning(list) + expect(out).toHaveLength(1) + expect(out[0]).toContain("First inspect the repo.") + expect(out[0]).toContain("Then summarize the structure.") + }) + + it("prefers explicit reasoning entries over provider-specific reasoning fields when both exist", () => { + const list = parsePartsFromConversation( + [ + { + role: "assistant", + type: "reasoning", + text: "I should inspect the files first.", + reasoning_content: "I should inspect the files first.", + content: [], + ts: 1774861015000, + } as unknown as LegacyApiMessage, + ], + id, + item, + ) + + const out = reasoning(list) + expect(out).toEqual(["I should inspect the files first."]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/session.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/session.test.ts new file mode 100644 index 00000000000..7764b88ea4d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/session.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "bun:test" +import { createSession } from "../../../src/legacy-migration/sessions/lib/session" + +describe("legacy migration session", () => { + it("builds a session from legacy task metadata and the resolved project id", () => { + const session = createSession( + "019d3df5-d5d9-73dc-bc2c-43a6304ac62c", + { + id: "019d3df5-d5d9-73dc-bc2c-43a6304ac62c", + ts: 1774861014564, + workspace: "/workspace/testing", + task: "Understand this project", + mode: "code", + }, + "project-1", + ) + + expect(session.projectID).toBe("project-1") + expect(session.directory).toBe("/workspace/testing") + expect(session.title).toBe("Understand this project") + expect(session.version).toBe("v2") + expect(session.timeCreated).toBe(1774861014564) + expect(session.timeUpdated).toBe(1774861014564) + }) + + it("creates a deterministic session id from the legacy task id", () => { + const a = createSession("legacy-task-1", undefined, "project-1") + const b = createSession("legacy-task-1", undefined, "project-1") + + expect(a.id).toBe(b.id) + expect(a.id.startsWith("ses_migrated_")).toBe(true) + }) + + it("falls back to the legacy id as title when task metadata is missing", () => { + const session = createSession("legacy-task-1", undefined, "project-1") + + expect(session.slug).toBe("legacy-task-1") + expect(session.title).toBe("legacy-task-1") + expect(session.directory).toBe("") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/tools.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/tools.test.ts new file mode 100644 index 00000000000..e1262a352be --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/tools.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "bun:test" +import { parsePartsFromConversation } from "../../../src/legacy-migration/sessions/lib/parts/parts" +import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types" + +type Tool = Extract[number]["data"], { type: "tool" }> + +function tools(list: ReturnType) { + return list.filter((x): x is (typeof list)[number] & { data: Tool } => x.data.type === "tool").map((x) => x.data) +} + +const id = "legacy-task-1" +const item = { + id, + ts: 1774861014564, + task: "Tool parsing test", + workspace: "/workspace/testing", + mode: "code", +} + +function fallback(): LegacyApiMessage[] { + return [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu_fallback_1", + name: "read_file", + input: { path: "app.py" }, + }, + ], + ts: 1774861015000, + }, + ] +} + +function merged(): LegacyApiMessage[] { + return [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu_merge_1", + name: "read_file", + input: { path: "app.py" }, + }, + ], + ts: 1774861015000, + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_merge_1", + content: [ + { + type: "text", + text: "File: app.py\nHello world", + }, + ], + }, + ], + ts: 1774861016000, + }, + ] +} + +function noTextResult(): LegacyApiMessage[] { + return [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu_merge_2", + name: "read_file", + input: { path: "app.py" }, + }, + ], + ts: 1774861015000, + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_merge_2", + content: [], + }, + ], + ts: 1774861016000, + }, + ] +} + +describe("legacy migration tools", () => { + it("creates a fallback tool part from tool_use when there is no matching tool_result", () => { + const list = parsePartsFromConversation(fallback(), id, item) + const out = tools(list) + + expect(out).toHaveLength(1) + expect(out[0]?.tool).toBe("read_file") + expect(out[0]?.state.status).toBe("completed") + }) + + it("merges tool_use and tool_result into one completed tool part using the correct tool id", () => { + const list = parsePartsFromConversation(merged(), id, item) + const out = tools(list) + + expect(out).toHaveLength(1) + expect(out[0]?.callID).toBe("toolu_merge_1") + expect(out[0]?.tool).toBe("read_file") + if (out[0]?.state.status !== "completed") throw new Error("tool was not completed") + expect(out[0].state.output).toContain("File: app.py") + }) + + it("does not duplicate a tool part when a matching tool_result exists later in the conversation", () => { + const list = parsePartsFromConversation(merged(), id, item) + const out = tools(list) + + expect(out).toHaveLength(1) + }) + + it("falls back to the tool name when tool_result has no readable text content", () => { + const list = parsePartsFromConversation(noTextResult(), id, item) + const out = tools(list) + + expect(out).toHaveLength(1) + if (out[0]?.state.status !== "completed") throw new Error("tool was not completed") + expect(out[0].state.output).toBe("read_file") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx b/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx index 1358048b543..3cfda340070 100644 --- a/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx @@ -6,7 +6,8 @@ * 2. Migrate — grouped selection, live in-place progress, cleanup */ -import { Component, Show, createSignal, onMount, onCleanup, JSX } from "solid-js" +import { Show, createSignal, onMount, onCleanup } from "solid-js" +import type { Component, JSX } from "solid-js" import { useVSCode } from "../../context/vscode" import { useLanguage } from "../../context/language" import type { @@ -182,6 +183,7 @@ const MigrationWizard: Component = (props) => { const [providers, setProviders] = createSignal([]) const [mcpServers, setMcpServers] = createSignal([]) const [customModes, setCustomModes] = createSignal([]) + const [sessions, setSessions] = createSignal([]) const [defaultModel, setDefaultModel] = createSignal<{ provider: string; model: string } | undefined>(undefined) const [legacySettings, setLegacySettings] = createSignal(undefined) @@ -189,6 +191,7 @@ const MigrationWizard: Component = (props) => { const [migrateProviders, setMigrateProviders] = createSignal(true) const [migrateMcpServers, setMigrateMcpServers] = createSignal(true) const [migrateModes, setMigrateModes] = createSignal(true) + const [migrateSessions, setMigrateSessions] = createSignal(true) const [migrateDefaultModel, setMigrateDefaultModel] = createSignal(true) const [migrateAutoApproval, setMigrateAutoApproval] = createSignal(true) const [migrateLanguage, setMigrateLanguage] = createSignal(true) @@ -213,6 +216,7 @@ const MigrationWizard: Component = (props) => { setProviders(data.providers) setMcpServers(data.mcpServers) setCustomModes(data.customModes) + setSessions(data.sessions ?? []) setDefaultModel(data.defaultModel) setLegacySettings(data.settings) @@ -220,6 +224,7 @@ const MigrationWizard: Component = (props) => { setMigrateProviders(data.providers.some((p) => p.supported && p.hasApiKey)) setMigrateMcpServers(data.mcpServers.length > 0) setMigrateModes(data.customModes.length > 0) + setMigrateSessions((data.sessions?.length ?? 0) > 0) setMigrateDefaultModel(Boolean(data.defaultModel)) const s = data.settings @@ -318,6 +323,9 @@ const MigrationWizard: Component = (props) => { const mode = customModes().find((m) => m.slug === slug) return { item: mode?.name ?? slug, group: "customModes", status: "pending" as const } }), + ...(migrateSessions() && sessions().length > 0 + ? [{ item: "Chat sessions", group: "sessions", status: "pending" as const }] + : []), ...(migrateDefaultModel() && defaultModel() ? [{ item: "Default model", group: "defaultModel", status: "pending" as const }] : []), @@ -356,6 +364,7 @@ const MigrationWizard: Component = (props) => { providers: selectedProviderNames, mcpServers: selectedMcpNames, customModes: selectedModesSlugs, + sessions: migrateSessions() ? sessions() : [], defaultModel: migrateDefaultModel(), settings: { autoApproval, @@ -398,11 +407,13 @@ const MigrationWizard: Component = (props) => { const hasLanguageData = () => Boolean(legacySettings()?.language) const hasAutocompleteData = () => Boolean(legacySettings()?.autocomplete) + const hasSessions = () => sessions().length > 0 const hasAnySelection = () => (migrateProviders() && supportedProviderCount() > 0) || (migrateMcpServers() && mcpServers().length > 0) || (migrateModes() && customModes().length > 0) || + (migrateSessions() && hasSessions()) || (migrateDefaultModel() && Boolean(defaultModel())) || (migrateAutoApproval() && hasAnyAutoApprovalData()) || (migrateLanguage() && hasLanguageData()) || @@ -412,6 +423,7 @@ const MigrationWizard: Component = (props) => { supportedProviderCount() === 0 && mcpServers().length === 0 && customModes().length === 0 && + !hasSessions() && !defaultModel() && !hasAnyAutoApprovalData() && !hasLanguageData() && @@ -650,6 +662,27 @@ const MigrationWizard: Component = (props) => { + +
+ }> + + +
+
Chat Sessions & History
+
{sessions().length} sessions detected
+
+
+
+ {/* Default Model */}
@@ -734,20 +767,6 @@ const MigrationWizard: Component = (props) => {
- {/* Divider + Cannot be migrated */} -
- -
- -
-
{language.t("migration.migrate.chatHistory")}
-
{language.t("migration.migrate.chatHistoryDesc")}
-
-
- {/* Cleanup option after done */}
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index cd7e25b9656..bc35beb8cc5 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -1053,6 +1053,7 @@ export interface MigrationStateMessage { providers: MigrationProviderInfo[] mcpServers: MigrationMcpServerInfo[] customModes: MigrationCustomModeInfo[] + sessions?: string[] defaultModel?: { provider: string; model: string } settings?: LegacySettings } @@ -1064,6 +1065,7 @@ export interface LegacyMigrationDataMessage { providers: MigrationProviderInfo[] mcpServers: MigrationMcpServerInfo[] customModes: MigrationCustomModeInfo[] + sessions?: string[] defaultModel?: { provider: string; model: string } settings?: LegacySettings } @@ -1100,6 +1102,7 @@ export interface StartLegacyMigrationMessage { providers: string[] mcpServers: string[] customModes: string[] + sessions?: string[] defaultModel: boolean settings: { autoApproval: MigrationAutoApprovalSelections diff --git a/packages/opencode/src/kilocode/session-import/routes.ts b/packages/opencode/src/kilocode/session-import/routes.ts new file mode 100644 index 00000000000..755a725f240 --- /dev/null +++ b/packages/opencode/src/kilocode/session-import/routes.ts @@ -0,0 +1,94 @@ +import { Hono } from "hono" +import { describeRoute, resolver, validator } from "hono-openapi" +import { lazy } from "../../util/lazy" +import { errors } from "../../server/error" +import { SessionImportService } from "./service" +import { SessionImportType } from "./types" + +export const SessionImportRoutes = lazy(() => + new Hono() + .post( + "/project", + describeRoute({ + summary: "Insert project for session import", + description: "Insert or update a project row used by legacy session import.", + operationId: "kilocode.sessionImport.project", + responses: { + 200: { + description: "Project import result", + content: { + "application/json": { + schema: resolver(SessionImportType.Result), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", SessionImportType.Project), + async (c) => c.json(await SessionImportService.project(c.req.valid("json"))), + ) + .post( + "/session", + describeRoute({ + summary: "Insert session for session import", + description: "Insert or update a session row used by legacy session import.", + operationId: "kilocode.sessionImport.session", + responses: { + 200: { + description: "Session import result", + content: { + "application/json": { + schema: resolver(SessionImportType.Result), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", SessionImportType.Session), + async (c) => c.json(await SessionImportService.session(c.req.valid("json"))), + ) + .post( + "/message", + describeRoute({ + summary: "Insert message for session import", + description: "Insert or update a message row used by legacy session import.", + operationId: "kilocode.sessionImport.message", + responses: { + 200: { + description: "Message import result", + content: { + "application/json": { + schema: resolver(SessionImportType.Result), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", SessionImportType.Message), + async (c) => c.json(await SessionImportService.message(c.req.valid("json"))), + ) + .post( + "/part", + describeRoute({ + summary: "Insert part for session import", + description: "Insert or update a part row used by legacy session import.", + operationId: "kilocode.sessionImport.part", + responses: { + 200: { + description: "Part import result", + content: { + "application/json": { + schema: resolver(SessionImportType.Result), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", SessionImportType.Part), + async (c) => c.json(await SessionImportService.part(c.req.valid("json"))), + ), +) diff --git a/packages/opencode/src/kilocode/session-import/service.ts b/packages/opencode/src/kilocode/session-import/service.ts new file mode 100644 index 00000000000..9c61a257182 --- /dev/null +++ b/packages/opencode/src/kilocode/session-import/service.ts @@ -0,0 +1,97 @@ +import { Database } from "../../storage/db" +import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" +import { SessionImportType } from "./types" +import { Project } from "../../project/project" +import { eq } from "drizzle-orm" + +const key = (input: unknown) => [input] as never +const target = (input: unknown) => input as never + +export namespace SessionImportService { + export async function project(input: SessionImportType.Project): Promise { + // Do not resolve an empty legacy worktree, because that would fall back to the current + // process directory and silently attach the migrated session to the wrong project. + if (!input.worktree.trim()) { + throw new Error("Legacy project import requires a non-empty worktree") + } + + const result = await Project.fromDirectory(input.worktree) + return { ok: true, id: result.project.id } + } + + export async function session(input: SessionImportType.Session): Promise { + const row = Database.use((db) => db.select().from(SessionTable).where(eq(target(SessionTable.id), input.id)).get()) + if (row) return { ok: true, id: input.id, skipped: true } + + Database.use((db) => { + db.insert(SessionTable) + .values({ + id: input.id, + project_id: input.projectID, + workspace_id: input.workspaceID, + parent_id: input.parentID, + slug: input.slug, + directory: input.directory, + title: input.title, + version: input.version, + share_url: input.shareURL, + summary_additions: input.summary?.additions, + summary_deletions: input.summary?.deletions, + summary_files: input.summary?.files, + summary_diffs: input.summary?.diffs as never, + revert: input.revert, + permission: input.permission as never, + time_created: input.timeCreated, + time_updated: input.timeUpdated, + time_compacting: input.timeCompacting, + time_archived: input.timeArchived, + }) + .onConflictDoNothing({ + target: key(SessionTable.id), + }) + .run() + }) + return { ok: true, id: input.id } + } + + export async function message(input: SessionImportType.Message): Promise { + Database.use((db) => { + db.insert(MessageTable) + .values({ + id: input.id, + session_id: input.sessionID, + time_created: input.timeCreated, + data: input.data as never, + }) + .onConflictDoUpdate({ + target: key(MessageTable.id), + set: { + data: input.data as never, + }, + }) + .run() + }) + return { ok: true, id: input.id } + } + + export async function part(input: SessionImportType.Part): Promise { + Database.use((db) => { + db.insert(PartTable) + .values({ + id: input.id, + message_id: input.messageID, + session_id: input.sessionID, + time_created: input.timeCreated, + data: input.data as never, + }) + .onConflictDoUpdate({ + target: key(PartTable.id), + set: { + data: input.data as never, + }, + }) + .run() + }) + return { ok: true, id: input.id } + } +} diff --git a/packages/opencode/src/kilocode/session-import/types.ts b/packages/opencode/src/kilocode/session-import/types.ts new file mode 100644 index 00000000000..7bd5d6b9f14 --- /dev/null +++ b/packages/opencode/src/kilocode/session-import/types.ts @@ -0,0 +1,216 @@ +import z from "zod" + +export namespace SessionImportType { + export const UserMessageData = z.object({ + role: z.literal("user"), + time: z.object({ + created: z.number(), + }), + agent: z.string(), + model: z.object({ + providerID: z.string(), + modelID: z.string(), + }), + tools: z.record(z.string(), z.boolean()).optional(), + }) + + export const AssistantMessageData = z.object({ + role: z.literal("assistant"), + time: z.object({ + created: z.number(), + completed: z.number().optional(), + }), + parentID: z.string(), + modelID: z.string(), + providerID: z.string(), + mode: z.string(), + agent: z.string(), + path: z.object({ + cwd: z.string(), + root: z.string(), + }), + summary: z.boolean().optional(), + cost: z.number(), + tokens: z.object({ + total: z.number().optional(), + input: z.number(), + output: z.number(), + reasoning: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + }), + structured: z.unknown().optional(), + variant: z.string().optional(), + finish: z.string().optional(), + }) + + export const MessageData = z.discriminatedUnion("role", [UserMessageData, AssistantMessageData]) + + export const TextPartData = z.object({ + type: z.literal("text"), + text: z.string(), + synthetic: z.boolean().optional(), + ignored: z.boolean().optional(), + time: z + .object({ + start: z.number(), + end: z.number().optional(), + }) + .optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + }) + + export const ReasoningPartData = z.object({ + type: z.literal("reasoning"), + text: z.string(), + metadata: z.record(z.string(), z.unknown()).optional(), + time: z.object({ + start: z.number(), + end: z.number().optional(), + }), + }) + + export const ToolStatePending = z.object({ + status: z.literal("pending"), + input: z.record(z.string(), z.unknown()), + raw: z.string(), + }) + + export const ToolStateRunning = z.object({ + status: z.literal("running"), + input: z.record(z.string(), z.unknown()), + title: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + time: z.object({ + start: z.number(), + }), + }) + + export const ToolStateCompleted = z.object({ + status: z.literal("completed"), + input: z.record(z.string(), z.unknown()), + output: z.string(), + title: z.string(), + metadata: z.record(z.string(), z.unknown()), + time: z.object({ + start: z.number(), + end: z.number(), + compacted: z.number().optional(), + }), + }) + + export const ToolStateError = z.object({ + status: z.literal("error"), + input: z.record(z.string(), z.unknown()), + error: z.string(), + metadata: z.record(z.string(), z.unknown()).optional(), + time: z.object({ + start: z.number(), + end: z.number(), + }), + }) + + export const ToolState = z.discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) + + export const ToolPartData = z.object({ + type: z.literal("tool"), + callID: z.string(), + tool: z.string(), + state: ToolState, + metadata: z.record(z.string(), z.unknown()).optional(), + }) + + export const PartData = z.discriminatedUnion("type", [TextPartData, ReasoningPartData, ToolPartData]) + + export const Result = z.object({ + ok: z.boolean(), + id: z.string(), + skipped: z.boolean().optional(), + }) + + export const Project = z.object({ + id: z.string(), + worktree: z.string(), + vcs: z.string().optional(), + name: z.string().optional(), + iconUrl: z.string().optional(), + iconColor: z.string().optional(), + timeCreated: z.number(), + timeUpdated: z.number(), + timeInitialized: z.number().optional(), + sandboxes: z.array(z.string()), + commands: z + .object({ + start: z.string().optional(), + }) + .optional(), + }) + + export const Session = z.object({ + id: z.string(), + projectID: z.string(), + workspaceID: z.string().optional(), + parentID: z.string().optional(), + slug: z.string(), + directory: z.string(), + title: z.string(), + version: z.string(), + shareURL: z.string().optional(), + summary: z + .object({ + additions: z.number(), + deletions: z.number(), + files: z.number(), + diffs: z.array(z.record(z.string(), z.unknown())).optional(), + }) + .optional(), + revert: z + .object({ + messageID: z.string(), + partID: z.string().optional(), + snapshot: z.string().optional(), + diff: z.string().optional(), + }) + .optional(), + permission: z.record(z.string(), z.unknown()).optional(), + timeCreated: z.number(), + timeUpdated: z.number(), + timeCompacting: z.number().optional(), + timeArchived: z.number().optional(), + }) + + export const Message = z.object({ + id: z.string(), + sessionID: z.string(), + timeCreated: z.number(), + data: MessageData, + }) + + export const Part = z.object({ + id: z.string(), + messageID: z.string(), + sessionID: z.string(), + timeCreated: z.number().optional(), + data: PartData, + }) + + export type Result = z.infer + export type UserMessageData = z.infer + export type AssistantMessageData = z.infer + export type MessageData = z.infer + export type TextPartData = z.infer + export type ReasoningPartData = z.infer + export type ToolStatePending = z.infer + export type ToolStateRunning = z.infer + export type ToolStateCompleted = z.infer + export type ToolStateError = z.infer + export type ToolState = z.infer + export type ToolPartData = z.infer + export type PartData = z.infer + export type Project = z.infer + export type Session = z.infer + export type Message = z.infer + export type Part = z.infer +} diff --git a/packages/opencode/src/server/routes/kilocode.ts b/packages/opencode/src/server/routes/kilocode.ts index aa7d0503994..75572a9ad84 100644 --- a/packages/opencode/src/server/routes/kilocode.ts +++ b/packages/opencode/src/server/routes/kilocode.ts @@ -8,9 +8,11 @@ import { Skill } from "../../skill/skill" import { Agent } from "../../agent/agent" import { lazy } from "../../util/lazy" import { errors } from "../error" +import { SessionImportRoutes } from "../../kilocode/session-import/routes" export const KilocodeRoutes = lazy(() => new Hono() + .route("/session-import", SessionImportRoutes()) .post( "/skill/remove", describeRoute({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 640cc66fb0d..84355f79a57 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -61,6 +61,14 @@ import type { KilocodeRemoveAgentResponses, KilocodeRemoveSkillErrors, KilocodeRemoveSkillResponses, + KilocodeSessionImportMessageErrors, + KilocodeSessionImportMessageResponses, + KilocodeSessionImportPartErrors, + KilocodeSessionImportPartResponses, + KilocodeSessionImportProjectErrors, + KilocodeSessionImportProjectResponses, + KilocodeSessionImportSessionErrors, + KilocodeSessionImportSessionResponses, KiloFimErrors, KiloFimResponses, KiloModesResponses, @@ -3095,6 +3103,388 @@ export class EnhancePrompt extends HeyApiClient { } } +export class SessionImport extends HeyApiClient { + /** + * Insert project for session import + * + * Insert or update a project row used by legacy session import. + */ + public project( + parameters?: { + directory?: string + workspace?: string + id?: string + worktree?: string + vcs?: string + name?: string + iconUrl?: string + iconColor?: string + timeCreated?: number + timeUpdated?: number + timeInitialized?: number + sandboxes?: Array + commands?: { + start?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "worktree" }, + { in: "body", key: "vcs" }, + { in: "body", key: "name" }, + { in: "body", key: "iconUrl" }, + { in: "body", key: "iconColor" }, + { in: "body", key: "timeCreated" }, + { in: "body", key: "timeUpdated" }, + { in: "body", key: "timeInitialized" }, + { in: "body", key: "sandboxes" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + KilocodeSessionImportProjectResponses, + KilocodeSessionImportProjectErrors, + ThrowOnError + >({ + url: "/kilocode/session-import/project", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Insert session for session import + * + * Insert or update a session row used by legacy session import. + */ + public session( + parameters?: { + query_directory?: string + workspace?: string + id?: string + projectID?: string + workspaceID?: string + parentID?: string + slug?: string + body_directory?: string + title?: string + version?: string + shareURL?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array<{ + [key: string]: unknown + }> + } + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + permission?: { + [key: string]: unknown + } + timeCreated?: number + timeUpdated?: number + timeCompacting?: number + timeArchived?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { + in: "query", + key: "query_directory", + map: "directory", + }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "projectID" }, + { in: "body", key: "workspaceID" }, + { in: "body", key: "parentID" }, + { in: "body", key: "slug" }, + { + in: "body", + key: "body_directory", + map: "directory", + }, + { in: "body", key: "title" }, + { in: "body", key: "version" }, + { in: "body", key: "shareURL" }, + { in: "body", key: "summary" }, + { in: "body", key: "revert" }, + { in: "body", key: "permission" }, + { in: "body", key: "timeCreated" }, + { in: "body", key: "timeUpdated" }, + { in: "body", key: "timeCompacting" }, + { in: "body", key: "timeArchived" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + KilocodeSessionImportSessionResponses, + KilocodeSessionImportSessionErrors, + ThrowOnError + >({ + url: "/kilocode/session-import/session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Insert message for session import + * + * Insert or update a message row used by legacy session import. + */ + public message( + parameters?: { + directory?: string + workspace?: string + id?: string + sessionID?: string + timeCreated?: number + data?: + | { + role: "user" + time: { + created: number + } + agent: string + model: { + providerID: string + modelID: string + } + tools?: { + [key: string]: boolean + } + } + | { + role: "assistant" + time: { + created: number + completed?: number + } + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + structured?: unknown + variant?: string + finish?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "sessionID" }, + { in: "body", key: "timeCreated" }, + { in: "body", key: "data" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + KilocodeSessionImportMessageResponses, + KilocodeSessionImportMessageErrors, + ThrowOnError + >({ + url: "/kilocode/session-import/message", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Insert part for session import + * + * Insert or update a part row used by legacy session import. + */ + public part( + parameters?: { + directory?: string + workspace?: string + id?: string + messageID?: string + sessionID?: string + timeCreated?: number + data?: + | { + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } + } + | { + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } + } + | { + type: "tool" + callID: string + tool: string + state: + | { + status: "pending" + input: { + [key: string]: unknown + } + raw: string + } + | { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } + } + | { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + } + | { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } + } + metadata?: { + [key: string]: unknown + } + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "messageID" }, + { in: "body", key: "sessionID" }, + { in: "body", key: "timeCreated" }, + { in: "body", key: "data" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + KilocodeSessionImportPartResponses, + KilocodeSessionImportPartErrors, + ThrowOnError + >({ + url: "/kilocode/session-import/part", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Kilocode extends HeyApiClient { /** * Remove a skill @@ -3173,6 +3563,11 @@ export class Kilocode extends HeyApiClient { }, ) } + + private _sessionImport?: SessionImport + get sessionImport(): SessionImport { + return (this._sessionImport ??= new SessionImport({ client: this.client })) + } } export class Organization extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index fa05a8e95f9..b22f7018043 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4529,6 +4529,325 @@ export type EnhancePromptEnhanceResponses = { export type EnhancePromptEnhanceResponse = EnhancePromptEnhanceResponses[keyof EnhancePromptEnhanceResponses] +export type KilocodeSessionImportProjectData = { + body?: { + id: string + worktree: string + vcs?: string + name?: string + iconUrl?: string + iconColor?: string + timeCreated: number + timeUpdated: number + timeInitialized?: number + sandboxes: Array + commands?: { + start?: string + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilocode/session-import/project" +} + +export type KilocodeSessionImportProjectErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KilocodeSessionImportProjectError = + KilocodeSessionImportProjectErrors[keyof KilocodeSessionImportProjectErrors] + +export type KilocodeSessionImportProjectResponses = { + /** + * Project import result + */ + 200: { + ok: boolean + id: string + skipped?: boolean + } +} + +export type KilocodeSessionImportProjectResponse = + KilocodeSessionImportProjectResponses[keyof KilocodeSessionImportProjectResponses] + +export type KilocodeSessionImportSessionData = { + body?: { + id: string + projectID: string + workspaceID?: string + parentID?: string + slug: string + directory: string + title: string + version: string + shareURL?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array<{ + [key: string]: unknown + }> + } + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + permission?: { + [key: string]: unknown + } + timeCreated: number + timeUpdated: number + timeCompacting?: number + timeArchived?: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilocode/session-import/session" +} + +export type KilocodeSessionImportSessionErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KilocodeSessionImportSessionError = + KilocodeSessionImportSessionErrors[keyof KilocodeSessionImportSessionErrors] + +export type KilocodeSessionImportSessionResponses = { + /** + * Session import result + */ + 200: { + ok: boolean + id: string + skipped?: boolean + } +} + +export type KilocodeSessionImportSessionResponse = + KilocodeSessionImportSessionResponses[keyof KilocodeSessionImportSessionResponses] + +export type KilocodeSessionImportMessageData = { + body?: { + id: string + sessionID: string + timeCreated: number + data: + | { + role: "user" + time: { + created: number + } + agent: string + model: { + providerID: string + modelID: string + } + tools?: { + [key: string]: boolean + } + } + | { + role: "assistant" + time: { + created: number + completed?: number + } + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + structured?: unknown + variant?: string + finish?: string + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilocode/session-import/message" +} + +export type KilocodeSessionImportMessageErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KilocodeSessionImportMessageError = + KilocodeSessionImportMessageErrors[keyof KilocodeSessionImportMessageErrors] + +export type KilocodeSessionImportMessageResponses = { + /** + * Message import result + */ + 200: { + ok: boolean + id: string + skipped?: boolean + } +} + +export type KilocodeSessionImportMessageResponse = + KilocodeSessionImportMessageResponses[keyof KilocodeSessionImportMessageResponses] + +export type KilocodeSessionImportPartData = { + body?: { + id: string + messageID: string + sessionID: string + timeCreated?: number + data: + | { + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } + } + | { + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } + } + | { + type: "tool" + callID: string + tool: string + state: + | { + status: "pending" + input: { + [key: string]: unknown + } + raw: string + } + | { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } + } + | { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + } + | { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } + } + metadata?: { + [key: string]: unknown + } + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilocode/session-import/part" +} + +export type KilocodeSessionImportPartErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KilocodeSessionImportPartError = KilocodeSessionImportPartErrors[keyof KilocodeSessionImportPartErrors] + +export type KilocodeSessionImportPartResponses = { + /** + * Part import result + */ + 200: { + ok: boolean + id: string + skipped?: boolean + } +} + +export type KilocodeSessionImportPartResponse = + KilocodeSessionImportPartResponses[keyof KilocodeSessionImportPartResponses] + export type KilocodeRemoveSkillData = { body?: { location: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3f4f142364e..7211c8fd359 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -5042,6 +5042,15 @@ "isFree": { "type": "boolean" }, + "ai_sdk_provider": { + "type": "string", + "enum": [ + "anthropic", + "openai", + "openai-compatible", + "openrouter" + ] + }, "experimental": { "type": "boolean" }, @@ -5473,7 +5482,10 @@ "type": "boolean" } }, - "required": ["enabled", "connected"] + "required": [ + "enabled", + "connected" + ] } } } @@ -5523,7 +5535,10 @@ "type": "boolean" } }, - "required": ["enabled", "connected"] + "required": [ + "enabled", + "connected" + ] } } } @@ -5573,7 +5588,10 @@ "type": "boolean" } }, - "required": ["enabled", "connected"] + "required": [ + "enabled", + "connected" + ] } } } @@ -5753,6 +5771,946 @@ ] } }, + "/kilocode/session-import/project": { + "post": { + "operationId": "kilocode.sessionImport.project", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Insert project for session import", + "description": "Insert or update a project row used by legacy session import.", + "responses": { + "200": { + "description": "Project import result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "required": [ + "ok", + "id" + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "vcs": { + "type": "string" + }, + "name": { + "type": "string" + }, + "iconUrl": { + "type": "string" + }, + "iconColor": { + "type": "string" + }, + "timeCreated": { + "type": "number" + }, + "timeUpdated": { + "type": "number" + }, + "timeInitialized": { + "type": "number" + }, + "sandboxes": { + "type": "array", + "items": { + "type": "string" + } + }, + "commands": { + "type": "object", + "properties": { + "start": { + "type": "string" + } + } + } + }, + "required": [ + "id", + "worktree", + "timeCreated", + "timeUpdated", + "sandboxes" + ] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilocode.sessionImport.project({\n ...\n})" + } + ] + } + }, + "/kilocode/session-import/session": { + "post": { + "operationId": "kilocode.sessionImport.session", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Insert session for session import", + "description": "Insert or update a session row used by legacy session import.", + "responses": { + "200": { + "description": "Session import result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "required": [ + "ok", + "id" + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "workspaceID": { + "type": "string" + }, + "parentID": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "title": { + "type": "string" + }, + "version": { + "type": "string" + }, + "shareURL": { + "type": "string" + }, + "summary": { + "type": "object", + "properties": { + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "files": { + "type": "number" + }, + "diffs": { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + } + }, + "required": [ + "additions", + "deletions", + "files" + ] + }, + "revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "partID": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + } + }, + "required": [ + "messageID" + ] + }, + "permission": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "timeCreated": { + "type": "number" + }, + "timeUpdated": { + "type": "number" + }, + "timeCompacting": { + "type": "number" + }, + "timeArchived": { + "type": "number" + } + }, + "required": [ + "id", + "projectID", + "slug", + "directory", + "title", + "version", + "timeCreated", + "timeUpdated" + ] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilocode.sessionImport.session({\n ...\n})" + } + ] + } + }, + "/kilocode/session-import/message": { + "post": { + "operationId": "kilocode.sessionImport.message", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Insert message for session import", + "description": "Insert or update a message row used by legacy session import.", + "responses": { + "200": { + "description": "Message import result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "required": [ + "ok", + "id" + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "timeCreated": { + "type": "number" + }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "role": { + "type": "string", + "const": "user" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ] + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": [ + "providerID", + "modelID" + ] + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + } + }, + "required": [ + "role", + "time", + "agent", + "model" + ] + }, + { + "type": "object", + "properties": { + "role": { + "type": "string", + "const": "assistant" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ] + }, + "parentID": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "path": { + "type": "object", + "properties": { + "cwd": { + "type": "string" + }, + "root": { + "type": "string" + } + }, + "required": [ + "cwd", + "root" + ] + }, + "summary": { + "type": "boolean" + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "type": "number" + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ] + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ] + }, + "structured": {}, + "variant": { + "type": "string" + }, + "finish": { + "type": "string" + } + }, + "required": [ + "role", + "time", + "parentID", + "modelID", + "providerID", + "mode", + "agent", + "path", + "cost", + "tokens" + ] + } + ] + } + }, + "required": [ + "id", + "sessionID", + "timeCreated", + "data" + ] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilocode.sessionImport.message({\n ...\n})" + } + ] + } + }, + "/kilocode/session-import/part": { + "post": { + "operationId": "kilocode.sessionImport.part", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Insert part for session import", + "description": "Insert or update a part row used by legacy session import.", + "responses": { + "200": { + "description": "Part import result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "required": [ + "ok", + "id" + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "timeCreated": { + "type": "number" + }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "text": { + "type": "string" + }, + "synthetic": { + "type": "boolean" + }, + "ignored": { + "type": "boolean" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "start" + ] + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "type", + "text" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "reasoning" + }, + "text": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "start" + ] + } + }, + "required": [ + "type", + "text", + "time" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tool" + }, + "callID": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "state": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "pending" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "raw": { + "type": "string" + } + }, + "required": [ + "status", + "input", + "raw" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "running" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + } + }, + "required": [ + "start" + ] + } + }, + "required": [ + "status", + "input", + "time" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "completed" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "output": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "compacted": { + "type": "number" + } + }, + "required": [ + "start", + "end" + ] + } + }, + "required": [ + "status", + "input", + "output", + "title", + "metadata", + "time" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "error" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "error": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "start", + "end" + ] + } + }, + "required": [ + "status", + "input", + "error", + "time" + ] + } + ] + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "type", + "callID", + "tool", + "state" + ] + } + ] + } + }, + "required": [ + "id", + "messageID", + "sessionID", + "data" + ] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilocode.sessionImport.part({\n ...\n})" + } + ] + } + }, "/kilocode/skill/remove": { "post": { "operationId": "kilocode.removeSkill", @@ -6158,7 +7116,32 @@ }, "groups": { "type": "array", - "items": {} + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "fileRegex": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + ] + } + ] + } } } } @@ -12659,6 +13642,15 @@ "isFree": { "type": "boolean" }, + "ai_sdk_provider": { + "type": "string", + "enum": [ + "anthropic", + "openai", + "openai-compatible", + "openrouter" + ] + }, "experimental": { "type": "boolean" }, @@ -13740,6 +14732,15 @@ }, "isFree": { "type": "boolean" + }, + "ai_sdk_provider": { + "type": "string", + "enum": [ + "anthropic", + "openai", + "openai-compatible", + "openrouter" + ] } }, "required": [