| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const Methods = { |
| CheckWorkspace: "checkWorkspace", |
| GetInputHistory: "getInputHistory", |
| AddInputHistory: "addInputHistory", |
|
|
| GetSlashCommands: "getSlashCommands", |
| CheckLoginStatus: "checkLoginStatus", |
| Login: "login", |
| Logout: "logout", |
| SaveConfig: "saveConfig", |
| GetExtensionConfig: "getExtensionConfig", |
| OpenSettings: "openSettings", |
| OpenFolder: "openFolder", |
| GetModels: "getModels", |
|
|
| GetMCPServers: "getMCPServers", |
| AddMCPServer: "addMCPServer", |
| UpdateMCPServer: "updateMCPServer", |
| RemoveMCPServer: "removeMCPServer", |
| AuthMCP: "authMCP", |
| ResetAuthMCP: "resetAuthMCP", |
| TestMCP: "testMCP", |
|
|
| StreamChat: "streamChat", |
| AbortChat: "abortChat", |
| ResetSession: "resetSession", |
| SetPlanMode: "setPlanMode", |
| SteerChat: "steerChat", |
| RespondApproval: "respondApproval", |
|
|
| GetKimiSessions: "getKimiSessions", |
| GetAllKimiSessions: "getAllKimiSessions", |
| GetRegisteredWorkDirs: "getRegisteredWorkDirs", |
| SetWorkDir: "setWorkDir", |
| BrowseWorkDir: "browseWorkDir", |
| LoadKimiSessionHistory: "loadKimiSessionHistory", |
| DeleteKimiSession: "deleteKimiSession", |
| ForkKimiSession: "forkKimiSession", |
| GetProjectFiles: "getProjectFiles", |
| PickMedia: "pickMedia", |
| OpenFile: "openFile", |
| CheckFileExists: "checkFileExists", |
| CheckFilesExist: "checkFilesExist", |
| OpenFileDiff: "openFileDiff", |
| TrackFiles: "trackFiles", |
| ClearTrackedFiles: "clearTrackedFiles", |
| RevertFiles: "revertFiles", |
| KeepChanges: "keepChanges", |
| GetImageDataUri: "getImageDataUri", |
| ShowLogs: "showLogs", |
| ReloadWebview: "reloadWebview", |
| RespondQuestion: "respondQuestion", |
| } as const; |
|
|
| export type RpcMethod = (typeof Methods)[keyof typeof Methods]; |
|
|
| export interface RpcMessage { |
| readonly id: string; |
| readonly method: RpcMethod; |
| readonly params?: unknown; |
| } |
|
|
| export interface RpcResult { |
| readonly id: string; |
| readonly result?: unknown; |
| readonly error?: string; |
| } |
|
|
| export type RpcMessageValidation = |
| | { readonly ok: true; readonly message: RpcMessage } |
| | { |
| readonly ok: false; |
| readonly id: string; |
| readonly method: string; |
| readonly error: string; |
| }; |
|
|
| export const Events = { |
| ExtensionConfigChanged: "extensionConfigChanged", |
| MCPServersChanged: "mcpServersChanged", |
| StreamEvent: "streamEvent", |
| FocusInput: "focusInput", |
| InsertMention: "insertMention", |
| NewConversation: "newConversation", |
| FileChangesUpdated: "fileChangesUpdated", |
| RollbackInput: "rollbackInput", |
| LoginUrl: "loginUrl", |
| } as const; |
|
|
| const rpcMethods = new Set<string>(Object.values(Methods)); |
|
|
| |
| export function validateRpcMessage(value: unknown): RpcMessageValidation { |
| if (!isPlainObject(value)) { |
| return invalidMessage("", "<invalid>", "Invalid bridge request: expected a plain object."); |
| } |
|
|
| const id = value["id"]; |
| if (!Object.hasOwn(value, "id") || typeof id !== "string" || id.trim().length === 0) { |
| return invalidMessage("", safeMethod(value["method"]), "Invalid bridge request: id must be a non-empty string."); |
| } |
|
|
| const method = value["method"]; |
| if (!Object.hasOwn(value, "method") || typeof method !== "string" || method.trim().length === 0) { |
| return invalidMessage(id, "<invalid>", "Invalid bridge request: method must be a non-empty string."); |
| } |
| if (!rpcMethods.has(method)) { |
| return invalidMessage(id, method, `Unknown bridge method: ${method}`); |
| } |
| const params = Object.hasOwn(value, "params") ? value["params"] : undefined; |
| if (!validateParams(method as RpcMethod, params)) { |
| return invalidMessage(id, method, `Invalid bridge params for method: ${method}`); |
| } |
|
|
| return { ok: true, message: { id, method: method as RpcMethod, params } }; |
| } |
|
|
| function validateParams(method: RpcMethod, params: unknown): boolean { |
| switch (method) { |
| case Methods.CheckWorkspace: |
| case Methods.GetInputHistory: |
| case Methods.GetSlashCommands: |
| case Methods.CheckLoginStatus: |
| case Methods.Login: |
| case Methods.Logout: |
| case Methods.GetExtensionConfig: |
| case Methods.OpenSettings: |
| case Methods.OpenFolder: |
| case Methods.GetModels: |
| case Methods.GetMCPServers: |
| case Methods.AbortChat: |
| case Methods.ResetSession: |
| case Methods.GetKimiSessions: |
| case Methods.GetAllKimiSessions: |
| case Methods.GetRegisteredWorkDirs: |
| case Methods.BrowseWorkDir: |
| case Methods.ClearTrackedFiles: |
| case Methods.ShowLogs: |
| case Methods.ReloadWebview: |
| return params === undefined; |
|
|
| case Methods.AddInputHistory: |
| return hasString(params, "text"); |
| case Methods.SaveConfig: |
| return isPlainObject(params) |
| && typeof params["model"] === "string" |
| && isOptionalType(params["thinking"], "boolean") |
| && isOptionalType(params["effort"], "string") |
| && isOptionalType(params["effortChanged"], "boolean"); |
| case Methods.AddMCPServer: |
| return isMcpServerConfig(params); |
| case Methods.UpdateMCPServer: |
| return isMcpUpdate(params); |
| case Methods.RemoveMCPServer: |
| case Methods.AuthMCP: |
| case Methods.ResetAuthMCP: |
| case Methods.TestMCP: |
| return hasNonEmptyString(params, "name"); |
| case Methods.StreamChat: |
| return isStreamChatParams(params); |
| case Methods.RespondApproval: |
| return isPlainObject(params) |
| && isNonEmptyString(params["requestId"]) |
| && (params["response"] === "approve" |
| || params["response"] === "approve_for_session" |
| || params["response"] === "reject"); |
| case Methods.RespondQuestion: |
| return isPlainObject(params) |
| && isNonEmptyString(params["rpcRequestId"]) |
| && isNonEmptyString(params["questionRequestId"]) |
| && isStringRecord(params["answers"]); |
| case Methods.SetPlanMode: |
| return hasBoolean(params, "enabled"); |
| case Methods.SteerChat: |
| return isPlainObject(params) && isContent(params["content"]); |
| case Methods.GetProjectFiles: |
| return params === undefined || ( |
| isPlainObject(params) |
| && isOptionalType(params["query"], "string") |
| && isOptionalType(params["directory"], "string") |
| ); |
| case Methods.SetWorkDir: |
| return isPlainObject(params) && (params["workDir"] === null || typeof params["workDir"] === "string"); |
| case Methods.LoadKimiSessionHistory: |
| return hasNonEmptyString(params, "kimiSessionId"); |
| case Methods.DeleteKimiSession: |
| return hasNonEmptyString(params, "sessionId"); |
| case Methods.ForkKimiSession: |
| return isPlainObject(params) |
| && isNonEmptyString(params["sessionId"]) |
| && Number.isInteger(params["turnIndex"]) |
| && (params["turnIndex"] as number) >= 0; |
| case Methods.PickMedia: |
| return isPlainObject(params) |
| && (params["maxCount"] === undefined |
| || (Number.isInteger(params["maxCount"]) && (params["maxCount"] as number) >= 0)) |
| && isOptionalType(params["includeVideo"], "boolean"); |
| case Methods.OpenFile: |
| case Methods.OpenFileDiff: |
| case Methods.CheckFileExists: |
| case Methods.GetImageDataUri: |
| return hasString(params, "filePath"); |
| case Methods.CheckFilesExist: |
| case Methods.TrackFiles: |
| return hasStringArray(params, "paths"); |
| case Methods.RevertFiles: |
| case Methods.KeepChanges: |
| return isPlainObject(params) && isOptionalType(params["filePath"], "string"); |
| } |
| } |
|
|
| function isStreamChatParams(value: unknown): boolean { |
| return isPlainObject(value) |
| && isContent(value["content"]) |
| && typeof value["model"] === "string" |
| && isOptionalType(value["effort"], "string") |
| && isOptionalType(value["thinking"], "boolean") |
| && isOptionalType(value["planMode"], "boolean") |
| && isOptionalType(value["sessionId"], "string"); |
| } |
|
|
| function isContent(value: unknown): boolean { |
| return typeof value === "string" || (Array.isArray(value) && value.every(isContentPart)); |
| } |
|
|
| function isContentPart(value: unknown): boolean { |
| if (!isPlainObject(value) || typeof value["type"] !== "string") return false; |
| switch (value["type"]) { |
| case "text": |
| return typeof value["text"] === "string"; |
| case "think": |
| return typeof value["think"] === "string" |
| && (value["encrypted"] === undefined |
| || value["encrypted"] === null |
| || typeof value["encrypted"] === "string"); |
| case "image_url": |
| case "audio_url": |
| case "video_url": { |
| const media = value[value["type"]]; |
| return isPlainObject(media) |
| && typeof media["url"] === "string" |
| && (media["id"] === undefined || media["id"] === null || typeof media["id"] === "string"); |
| } |
| default: |
| return false; |
| } |
| } |
|
|
| function isMcpUpdate(value: unknown): boolean { |
| if (!isPlainObject(value)) return false; |
| if (Object.hasOwn(value, "server")) { |
| return isNonEmptyString(value["originalName"]) && isMcpServerConfig(value["server"]); |
| } |
| return isMcpServerConfig(value); |
| } |
|
|
| function isMcpServerConfig(value: unknown): boolean { |
| return isPlainObject(value) |
| && isNonEmptyString(value["name"]) |
| && (value["transport"] === "stdio" || value["transport"] === "http") |
| && isOptionalType(value["url"], "string") |
| && isOptionalType(value["command"], "string") |
| && (value["args"] === undefined || isStringArray(value["args"])) |
| && (value["env"] === undefined || isStringRecord(value["env"])) |
| && (value["headers"] === undefined || isStringRecord(value["headers"])) |
| && (value["auth"] === undefined || value["auth"] === "oauth") |
| && isOptionalType(value["bearerTokenEnvVar"], "string"); |
| } |
|
|
| function hasString(value: unknown, key: string): boolean { |
| return isPlainObject(value) && typeof value[key] === "string"; |
| } |
|
|
| function hasNonEmptyString(value: unknown, key: string): boolean { |
| return isPlainObject(value) && isNonEmptyString(value[key]); |
| } |
|
|
| function hasBoolean(value: unknown, key: string): boolean { |
| return isPlainObject(value) && typeof value[key] === "boolean"; |
| } |
|
|
| function hasStringArray(value: unknown, key: string): boolean { |
| return isPlainObject(value) && isStringArray(value[key]); |
| } |
|
|
| function isStringArray(value: unknown): boolean { |
| return Array.isArray(value) && value.every((item) => typeof item === "string"); |
| } |
|
|
| function isStringRecord(value: unknown): boolean { |
| return isPlainObject(value) && Object.values(value).every((item) => typeof item === "string"); |
| } |
|
|
| function isOptionalType(value: unknown, type: "string" | "boolean"): boolean { |
| return value === undefined || typeof value === type; |
| } |
|
|
| function isNonEmptyString(value: unknown): value is string { |
| return typeof value === "string" && value.trim().length > 0; |
| } |
|
|
| function isPlainObject(value: unknown): value is Record<string, unknown> { |
| if (value === null || typeof value !== "object") return false; |
| const prototype = Object.getPrototypeOf(value); |
| return prototype === Object.prototype || prototype === null; |
| } |
|
|
| function invalidMessage(id: string, method: string, error: string): RpcMessageValidation { |
| return { ok: false, id, method, error }; |
| } |
|
|
| function safeMethod(value: unknown): string { |
| return typeof value === "string" && value.length > 0 ? value : "<invalid>"; |
| } |
|
|