From 817a2e5bc469e778ad0f5264ab786d57e5c8cda8 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 9 Jan 2026 11:26:49 +0100 Subject: [PATCH 1/8] Cli Core Schema Migration --- cli/package.json | 1 + cli/src/auth/types.ts | 85 +-- cli/src/config/types.ts | 633 ++++-------------- cli/src/types/cli.ts | 38 +- cli/src/types/keyboard.ts | 62 +- cli/src/types/messages.ts | 36 +- cli/src/types/theme.ts | 126 +--- packages/core-schemas/package.json | 38 ++ .../core-schemas/src/agent-manager/index.ts | 2 + .../core-schemas/src/agent-manager/types.ts | 115 ++++ packages/core-schemas/src/auth/index.ts | 6 + packages/core-schemas/src/auth/kilocode.ts | 76 +++ .../core-schemas/src/config/auto-approval.ts | 108 +++ .../core-schemas/src/config/cli-config.ts | 53 ++ packages/core-schemas/src/config/history.ts | 22 + packages/core-schemas/src/config/index.ts | 11 + packages/core-schemas/src/config/provider.ts | 479 +++++++++++++ packages/core-schemas/src/index.ts | 27 + packages/core-schemas/src/keyboard/index.ts | 2 + packages/core-schemas/src/keyboard/key.ts | 52 ++ packages/core-schemas/src/mcp/index.ts | 2 + packages/core-schemas/src/mcp/server.ts | 59 ++ packages/core-schemas/src/messages/cli.ts | 63 ++ .../core-schemas/src/messages/extension.ts | 60 ++ packages/core-schemas/src/messages/index.ts | 5 + packages/core-schemas/src/theme/index.ts | 2 + packages/core-schemas/src/theme/theme.ts | 117 ++++ packages/core-schemas/tsconfig.json | 9 + packages/core-schemas/tsup.config.ts | 11 + pnpm-lock.yaml | 301 +++++++-- src/core/kilocode/agent-manager/types.ts | 93 +-- src/package.json | 1 + 32 files changed, 1808 insertions(+), 887 deletions(-) create mode 100644 packages/core-schemas/package.json create mode 100644 packages/core-schemas/src/agent-manager/index.ts create mode 100644 packages/core-schemas/src/agent-manager/types.ts create mode 100644 packages/core-schemas/src/auth/index.ts create mode 100644 packages/core-schemas/src/auth/kilocode.ts create mode 100644 packages/core-schemas/src/config/auto-approval.ts create mode 100644 packages/core-schemas/src/config/cli-config.ts create mode 100644 packages/core-schemas/src/config/history.ts create mode 100644 packages/core-schemas/src/config/index.ts create mode 100644 packages/core-schemas/src/config/provider.ts create mode 100644 packages/core-schemas/src/index.ts create mode 100644 packages/core-schemas/src/keyboard/index.ts create mode 100644 packages/core-schemas/src/keyboard/key.ts create mode 100644 packages/core-schemas/src/mcp/index.ts create mode 100644 packages/core-schemas/src/mcp/server.ts create mode 100644 packages/core-schemas/src/messages/cli.ts create mode 100644 packages/core-schemas/src/messages/extension.ts create mode 100644 packages/core-schemas/src/messages/index.ts create mode 100644 packages/core-schemas/src/theme/index.ts create mode 100644 packages/core-schemas/src/theme/theme.ts create mode 100644 packages/core-schemas/tsconfig.json create mode 100644 packages/core-schemas/tsup.config.ts diff --git a/cli/package.json b/cli/package.json index 4cb3c88a356..6801308fab0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -43,6 +43,7 @@ "@modelcontextprotocol/sdk": "^1.24.0", "@qdrant/js-client-rest": "^1.14.0", "@roo-code/cloud": "workspace:^", + "@kilocode/core-schemas": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", diff --git a/cli/src/auth/types.ts b/cli/src/auth/types.ts index 52888c15ce4..69aa9f88b61 100644 --- a/cli/src/auth/types.ts +++ b/cli/src/auth/types.ts @@ -1,5 +1,27 @@ +/** + * Authentication Types + * + * Re-exports types from @kilocode/core-schemas for runtime validation + * and backward compatibility with existing code. + */ + import type { ProviderConfig } from "../config/types.js" +// Re-export Kilocode schemas from core-schemas +export { kilocodeOrganizationSchema, kilocodeProfileDataSchema } from "@kilocode/core-schemas" + +// Re-export Kilocode types from core-schemas +export type { KilocodeOrganization, KilocodeProfileData } from "@kilocode/core-schemas" + +// Device auth (from @roo-code/types via core-schemas) +export { + DeviceAuthInitiateResponseSchema, + DeviceAuthPollResponseSchema, + type DeviceAuthInitiateResponse, + type DeviceAuthPollResponse, + type DeviceAuthState, +} from "@kilocode/core-schemas" + /** * Result of a successful authentication flow */ @@ -20,50 +42,15 @@ export interface AuthProvider { } /** - * Device authorization response from initiate endpoint - */ -export interface DeviceAuthInitiateResponse { - /** Verification code to display to user */ - code: string - /** URL for user to visit in browser */ - verificationUrl: string - /** Time in seconds until code expires */ - expiresIn: number -} - -/** - * Device authorization poll response - */ -export interface DeviceAuthPollResponse { - /** Current status of the authorization */ - status: "pending" | "approved" | "denied" | "expired" - /** API token (only present when approved) */ - token?: string - /** User ID (only present when approved) */ - userId?: string - /** User email (only present when approved) */ - userEmail?: string -} - -/** - * Organization data from Kilocode API - */ -export interface KilocodeOrganization { - id: string - name: string - role: string -} - -/** - * Profile data structure from Kilocode API + * Result of a poll operation */ -export interface KilocodeProfileData { - user?: { - name?: string - email?: string - image?: string - } - organizations?: KilocodeOrganization[] +export interface PollResult { + /** Whether polling should continue */ + continue: boolean + /** Optional data returned when polling completes */ + data?: unknown + /** Optional error if polling failed */ + error?: Error } /** @@ -79,15 +66,3 @@ export interface PollingOptions { /** Optional callback for progress updates */ onProgress?: (attempt: number, maxAttempts: number) => void } - -/** - * Result of a poll operation - */ -export interface PollResult { - /** Whether polling should continue */ - continue: boolean - /** Optional data returned when polling completes */ - data?: unknown - /** Optional error if polling failed */ - error?: Error -} diff --git a/cli/src/config/types.ts b/cli/src/config/types.ts index 93b9207acf4..a31a5f3fefd 100644 --- a/cli/src/config/types.ts +++ b/cli/src/config/types.ts @@ -1,501 +1,148 @@ -import type { ThemeId, Theme } from "../types/theme.js" - /** - * Auto approval configuration for read operations + * CLI Configuration Types + * + * Re-exports types from @kilocode/core-schemas for runtime validation + * and backward compatibility with existing code. */ -export interface AutoApprovalReadConfig { - enabled?: boolean - outside?: boolean -} -/** - * Auto approval configuration for write operations - */ -export interface AutoApprovalWriteConfig { - enabled?: boolean - outside?: boolean - protected?: boolean -} +import type { + ProviderConfig as CoreProviderConfig, + CLIConfig as CoreCLIConfig, + AutoApprovalConfig, + Theme, + ThemeId, +} from "@kilocode/core-schemas" -/** - * Auto approval configuration for browser operations - */ -export interface AutoApprovalBrowserConfig { - enabled?: boolean -} +// ProviderConfig with index signature for dynamic property access (backward compatibility) +export type ProviderConfig = CoreProviderConfig & { [key: string]: unknown } -/** - * Auto approval configuration for retry operations - */ -export interface AutoApprovalRetryConfig { - enabled?: boolean - delay?: number -} - -/** - * Auto approval configuration for MCP operations - */ -export interface AutoApprovalMcpConfig { - enabled?: boolean -} - -/** - * Auto approval configuration for mode switching - */ -export interface AutoApprovalModeConfig { - enabled?: boolean -} - -/** - * Auto approval configuration for subtasks - */ -export interface AutoApprovalSubtasksConfig { - enabled?: boolean -} - -/** - * Auto approval configuration for command execution - */ -export interface AutoApprovalExecuteConfig { - enabled?: boolean - allowed?: string[] - denied?: string[] -} - -/** - * Auto approval configuration for followup questions - */ -export interface AutoApprovalQuestionConfig { - enabled?: boolean - timeout?: number -} - -/** - * Auto approval configuration for todo list updates - */ -export interface AutoApprovalTodoConfig { - enabled?: boolean -} - -/** - * Complete auto approval configuration - */ -export interface AutoApprovalConfig { - enabled?: boolean - read?: AutoApprovalReadConfig - write?: AutoApprovalWriteConfig - browser?: AutoApprovalBrowserConfig - retry?: AutoApprovalRetryConfig - mcp?: AutoApprovalMcpConfig - mode?: AutoApprovalModeConfig - subtasks?: AutoApprovalSubtasksConfig - execute?: AutoApprovalExecuteConfig - question?: AutoApprovalQuestionConfig - todo?: AutoApprovalTodoConfig -} - -export interface CLIConfig { - version: "1.0.0" - mode: string - telemetry: boolean - provider: string +// CLIConfig with our enhanced ProviderConfig type +export interface CLIConfig extends Omit { providers: ProviderConfig[] - autoApproval?: AutoApprovalConfig - theme?: ThemeId - customThemes?: Record -} - -// Base provider config with common fields -interface BaseProviderConfig { - id: string - [key: string]: unknown // Allow additional fields for flexibility -} - -// Provider-specific configurations with discriminated unions -type KilocodeProviderConfig = BaseProviderConfig & { - provider: "kilocode" - kilocodeModel?: string - kilocodeToken?: string - kilocodeOrganizationId?: string - openRouterSpecificProvider?: string - openRouterProviderDataCollection?: "allow" | "deny" - openRouterProviderSort?: "price" | "throughput" | "latency" - openRouterZdr?: boolean - kilocodeTesterWarningsDisabledUntil?: number -} - -type AnthropicProviderConfig = BaseProviderConfig & { - provider: "anthropic" - apiModelId?: string - apiKey?: string - anthropicBaseUrl?: string - anthropicUseAuthToken?: boolean - anthropicBeta1MContext?: boolean -} - -type OpenAINativeProviderConfig = BaseProviderConfig & { - provider: "openai-native" - apiModelId?: string - openAiNativeApiKey?: string - openAiNativeBaseUrl?: string - openAiNativeServiceTier?: "auto" | "default" | "flex" | "priority" -} - -type OpenAIProviderConfig = BaseProviderConfig & { - provider: "openai" - openAiModelId?: string - openAiBaseUrl?: string - openAiApiKey?: string - openAiLegacyFormat?: boolean - openAiR1FormatEnabled?: boolean - openAiUseAzure?: boolean - azureApiVersion?: string - openAiStreamingEnabled?: boolean - openAiHeaders?: Record -} - -type OpenRouterProviderConfig = BaseProviderConfig & { - provider: "openrouter" - openRouterModelId?: string - openRouterApiKey?: string - openRouterBaseUrl?: string - openRouterSpecificProvider?: string - openRouterUseMiddleOutTransform?: boolean - openRouterProviderDataCollection?: "allow" | "deny" - openRouterProviderSort?: "price" | "throughput" | "latency" - openRouterZdr?: boolean -} - -type OllamaProviderConfig = BaseProviderConfig & { - provider: "ollama" - ollamaModelId?: string - ollamaBaseUrl?: string - ollamaApiKey?: string - ollamaNumCtx?: number -} - -type LMStudioProviderConfig = BaseProviderConfig & { - provider: "lmstudio" - lmStudioModelId?: string - lmStudioBaseUrl?: string - lmStudioDraftModelId?: string - lmStudioSpeculativeDecodingEnabled?: boolean -} - -type GlamaProviderConfig = BaseProviderConfig & { - provider: "glama" - glamaModelId?: string - glamaApiKey?: string -} - -type LiteLLMProviderConfig = BaseProviderConfig & { - provider: "litellm" - litellmModelId?: string - litellmBaseUrl?: string - litellmApiKey?: string - litellmUsePromptCache?: boolean -} - -type DeepInfraProviderConfig = BaseProviderConfig & { - provider: "deepinfra" - deepInfraModelId?: string - deepInfraBaseUrl?: string - deepInfraApiKey?: string -} - -type UnboundProviderConfig = BaseProviderConfig & { - provider: "unbound" - unboundModelId?: string - unboundApiKey?: string -} - -type RequestyProviderConfig = BaseProviderConfig & { - provider: "requesty" - requestyModelId?: string - requestyBaseUrl?: string - requestyApiKey?: string -} - -type VercelAiGatewayProviderConfig = BaseProviderConfig & { - provider: "vercel-ai-gateway" - vercelAiGatewayModelId?: string - vercelAiGatewayApiKey?: string -} - -type IOIntelligenceProviderConfig = BaseProviderConfig & { - provider: "io-intelligence" - ioIntelligenceModelId?: string - ioIntelligenceApiKey?: string -} - -type OVHCloudProviderConfig = BaseProviderConfig & { - provider: "ovhcloud" - ovhCloudAiEndpointsModelId?: string - ovhCloudAiEndpointsApiKey?: string - ovhCloudAiEndpointsBaseUrl?: string -} - -type InceptionProviderConfig = BaseProviderConfig & { - provider: "inception" - inceptionLabsModelId?: string - inceptionLabsBaseUrl?: string - inceptionLabsApiKey?: string -} - -type BedrockProviderConfig = BaseProviderConfig & { - provider: "bedrock" - apiModelId?: string - awsAccessKey?: string - awsSecretKey?: string - awsSessionToken?: string - awsRegion?: string - awsUseCrossRegionInference?: boolean - awsUsePromptCache?: boolean - awsProfile?: string - awsUseProfile?: boolean - awsApiKey?: string - awsUseApiKey?: boolean - awsCustomArn?: string - awsModelContextWindow?: number - awsBedrockEndpointEnabled?: boolean - awsBedrockEndpoint?: string - awsBedrock1MContext?: boolean -} - -type VertexProviderConfig = BaseProviderConfig & { - provider: "vertex" - apiModelId?: string - vertexKeyFile?: string - vertexJsonCredentials?: string - vertexProjectId?: string - vertexRegion?: string - enableUrlContext?: boolean - enableGrounding?: boolean -} - -type GeminiProviderConfig = BaseProviderConfig & { - provider: "gemini" - apiModelId?: string - geminiApiKey?: string - googleGeminiBaseUrl?: string - enableUrlContext?: boolean - enableGrounding?: boolean -} - -type GeminiCliProviderConfig = BaseProviderConfig & { - provider: "gemini-cli" - apiModelId?: string - geminiCliOAuthPath?: string - geminiCliProjectId?: string -} - -type MistralProviderConfig = BaseProviderConfig & { - provider: "mistral" - apiModelId?: string - mistralApiKey?: string - mistralCodestralUrl?: string -} - -type MoonshotProviderConfig = BaseProviderConfig & { - provider: "moonshot" - apiModelId?: string - moonshotBaseUrl?: string - moonshotApiKey?: string -} - -type MinimaxProviderConfig = BaseProviderConfig & { - provider: "minimax" - apiModelId?: string - minimaxBaseUrl?: string - minimaxApiKey?: string -} - -type DeepSeekProviderConfig = BaseProviderConfig & { - provider: "deepseek" - apiModelId?: string - deepSeekBaseUrl?: string - deepSeekApiKey?: string -} - -type DoubaoProviderConfig = BaseProviderConfig & { - provider: "doubao" - apiModelId?: string - doubaoBaseUrl?: string - doubaoApiKey?: string -} - -type QwenCodeProviderConfig = BaseProviderConfig & { - provider: "qwen-code" - apiModelId?: string - qwenCodeOauthPath?: string -} - -type XAIProviderConfig = BaseProviderConfig & { - provider: "xai" - apiModelId?: string - xaiApiKey?: string -} - -type GroqProviderConfig = BaseProviderConfig & { - provider: "groq" - apiModelId?: string - groqApiKey?: string } -type ChutesProviderConfig = BaseProviderConfig & { - provider: "chutes" - apiModelId?: string - chutesApiKey?: string -} - -type CerebrasProviderConfig = BaseProviderConfig & { - provider: "cerebras" - apiModelId?: string - cerebrasApiKey?: string -} - -type SambaNovaProviderConfig = BaseProviderConfig & { - provider: "sambanova" - apiModelId?: string - sambaNovaApiKey?: string -} - -type ZAIProviderConfig = BaseProviderConfig & { - provider: "zai" - apiModelId?: string - zaiApiKey?: string - zaiApiLine?: "international_coding" | "china_coding" -} - -type FireworksProviderConfig = BaseProviderConfig & { - provider: "fireworks" - apiModelId?: string - fireworksApiKey?: string -} - -type FeatherlessProviderConfig = BaseProviderConfig & { - provider: "featherless" - apiModelId?: string - featherlessApiKey?: string -} - -type RooProviderConfig = BaseProviderConfig & { - provider: "roo" - apiModelId?: string -} - -type ClaudeCodeProviderConfig = BaseProviderConfig & { - provider: "claude-code" - apiModelId?: string - claudeCodePath?: string - claudeCodeMaxOutputTokens?: number -} - -type VSCodeLMProviderConfig = BaseProviderConfig & { - provider: "vscode-lm" - vsCodeLmModelSelector?: { - vendor?: string - family?: string - version?: string - id?: string - } -} - -type HuggingFaceProviderConfig = BaseProviderConfig & { - provider: "huggingface" - huggingFaceModelId?: string - huggingFaceApiKey?: string - huggingFaceInferenceProvider?: string -} - -type SyntheticProviderConfig = BaseProviderConfig & { - provider: "synthetic" - apiModelId?: string - syntheticApiKey?: string -} - -type VirtualQuotaFallbackProviderConfig = BaseProviderConfig & { - provider: "virtual-quota-fallback" - profiles?: Array<{ - profileName?: string - profileId?: string - profileLimits?: { - tokensPerMinute?: number - tokensPerHour?: number - tokensPerDay?: number - requestsPerMinute?: number - requestsPerHour?: number - requestsPerDay?: number - } - }> -} - -type HumanRelayProviderConfig = BaseProviderConfig & { - provider: "human-relay" - // No model ID field -} - -type FakeAIProviderConfig = BaseProviderConfig & { - provider: "fake-ai" - fakeAi?: unknown -} - -// Discriminated union of all provider configs -export type ProviderConfig = - | KilocodeProviderConfig - | AnthropicProviderConfig - | OpenAINativeProviderConfig - | OpenAIProviderConfig - | OpenRouterProviderConfig - | OllamaProviderConfig - | LMStudioProviderConfig - | GlamaProviderConfig - | LiteLLMProviderConfig - | DeepInfraProviderConfig - | UnboundProviderConfig - | RequestyProviderConfig - | VercelAiGatewayProviderConfig - | IOIntelligenceProviderConfig - | OVHCloudProviderConfig - | InceptionProviderConfig - | BedrockProviderConfig - | VertexProviderConfig - | GeminiProviderConfig - | GeminiCliProviderConfig - | MistralProviderConfig - | MoonshotProviderConfig - | MinimaxProviderConfig - | DeepSeekProviderConfig - | DoubaoProviderConfig - | QwenCodeProviderConfig - | XAIProviderConfig - | GroqProviderConfig - | ChutesProviderConfig - | CerebrasProviderConfig - | SambaNovaProviderConfig - | ZAIProviderConfig - | FireworksProviderConfig - | FeatherlessProviderConfig - | RooProviderConfig - | ClaudeCodeProviderConfig - | VSCodeLMProviderConfig - | HuggingFaceProviderConfig - | SyntheticProviderConfig - | VirtualQuotaFallbackProviderConfig - | HumanRelayProviderConfig - | FakeAIProviderConfig - -// Type guards -export function isValidConfig(config: unknown): config is CLIConfig { - return ( - typeof config === "object" && - config !== null && - "version" in config && - "provider" in config && - "providers" in config - ) -} - -export function isProviderConfig(provider: unknown): provider is ProviderConfig { - return typeof provider === "object" && provider !== null && "id" in provider && "provider" in provider -} +// Re-export all config types from core-schemas +export { + // Provider schemas + providerConfigSchema, + kilocodeProviderSchema, + anthropicProviderSchema, + openAINativeProviderSchema, + openAIProviderSchema, + openRouterProviderSchema, + ollamaProviderSchema, + lmStudioProviderSchema, + glamaProviderSchema, + liteLLMProviderSchema, + deepInfraProviderSchema, + unboundProviderSchema, + requestyProviderSchema, + vercelAiGatewayProviderSchema, + ioIntelligenceProviderSchema, + ovhCloudProviderSchema, + inceptionProviderSchema, + bedrockProviderSchema, + vertexProviderSchema, + geminiProviderSchema, + geminiCliProviderSchema, + mistralProviderSchema, + moonshotProviderSchema, + minimaxProviderSchema, + deepSeekProviderSchema, + doubaoProviderSchema, + qwenCodeProviderSchema, + xaiProviderSchema, + groqProviderSchema, + chutesProviderSchema, + cerebrasProviderSchema, + sambaNovaProviderSchema, + zaiProviderSchema, + fireworksProviderSchema, + featherlessProviderSchema, + rooProviderSchema, + claudeCodeProviderSchema, + vsCodeLMProviderSchema, + huggingFaceProviderSchema, + syntheticProviderSchema, + virtualQuotaFallbackProviderSchema, + humanRelayProviderSchema, + fakeAIProviderSchema, + // Provider types (ProviderConfig and CLIConfig are defined locally with index signature) + type KilocodeProviderConfig, + type AnthropicProviderConfig, + type OpenAINativeProviderConfig, + type OpenAIProviderConfig, + type OpenRouterProviderConfig, + type OllamaProviderConfig, + type LMStudioProviderConfig, + type GlamaProviderConfig, + type LiteLLMProviderConfig, + type DeepInfraProviderConfig, + type UnboundProviderConfig, + type RequestyProviderConfig, + type VercelAiGatewayProviderConfig, + type IOIntelligenceProviderConfig, + type OVHCloudProviderConfig, + type InceptionProviderConfig, + type BedrockProviderConfig, + type VertexProviderConfig, + type GeminiProviderConfig, + type GeminiCliProviderConfig, + type MistralProviderConfig, + type MoonshotProviderConfig, + type MinimaxProviderConfig, + type DeepSeekProviderConfig, + type DoubaoProviderConfig, + type QwenCodeProviderConfig, + type XAIProviderConfig, + type GroqProviderConfig, + type ChutesProviderConfig, + type CerebrasProviderConfig, + type SambaNovaProviderConfig, + type ZAIProviderConfig, + type FireworksProviderConfig, + type FeatherlessProviderConfig, + type RooProviderConfig, + type ClaudeCodeProviderConfig, + type VSCodeLMProviderConfig, + type HuggingFaceProviderConfig, + type SyntheticProviderConfig, + type VirtualQuotaFallbackProviderConfig, + type HumanRelayProviderConfig, + type FakeAIProviderConfig, + // Type guard + isProviderConfig, + // Auto-approval schemas + autoApprovalConfigSchema, + autoApprovalReadSchema, + autoApprovalWriteSchema, + autoApprovalBrowserSchema, + autoApprovalRetrySchema, + autoApprovalMcpSchema, + autoApprovalModeSchema, + autoApprovalSubtasksSchema, + autoApprovalExecuteSchema, + autoApprovalQuestionSchema, + autoApprovalTodoSchema, + // Auto-approval types + type AutoApprovalConfig, + type AutoApprovalReadConfig, + type AutoApprovalWriteConfig, + type AutoApprovalBrowserConfig, + type AutoApprovalRetryConfig, + type AutoApprovalMcpConfig, + type AutoApprovalModeConfig, + type AutoApprovalSubtasksConfig, + type AutoApprovalExecuteConfig, + type AutoApprovalQuestionConfig, + type AutoApprovalTodoConfig, + // CLI config schema (CLIConfig type is defined locally) + cliConfigSchema, + isValidConfig, + // ValidationResult is defined in validation.ts, not re-exported here to avoid conflict + // History + historyEntrySchema, + historyDataSchema, + type HistoryEntry, + type HistoryData, +} from "@kilocode/core-schemas" diff --git a/cli/src/types/cli.ts b/cli/src/types/cli.ts index eb8544a0d72..72c5a5710aa 100644 --- a/cli/src/types/cli.ts +++ b/cli/src/types/cli.ts @@ -1,30 +1,20 @@ -import type { ModeConfig } from "./messages.js" +/** + * CLI Types + * + * Re-exports types from @kilocode/core-schemas for runtime validation + * and backward compatibility with existing code. + */ -export interface WelcomeMessageOptions { - // Clear viewport before showing the message - clearScreen?: boolean - // Display options - showInstructions?: boolean - // Content customization - instructions?: string[] // Custom instruction lines - // Parallel mode branch name - worktreeBranch?: string | undefined - // Workspace directory - workspace?: string | undefined -} +import type { ModeConfig } from "@roo-code/types" -export interface CliMessage { - id: string - type: "user" | "assistant" | "system" | "error" | "welcome" | "empty" | "requestCheckpointRestoreApproval" - content: string - ts: number - partial?: boolean | undefined - metadata?: { - welcomeOptions?: WelcomeMessageOptions | undefined - } - payload?: unknown -} +// Re-export schemas from core-schemas +export { welcomeMessageOptionsSchema, cliMessageSchema, cliOptionsSchema } from "@kilocode/core-schemas" + +// Re-export schema-inferred types for simpler cases +export type { WelcomeMessageOptions, CliMessage } from "@kilocode/core-schemas" +// CLIOptions interface with proper typing for ModeConfig +// (The schema uses z.unknown() but we want proper types at compile time) export interface CLIOptions { mode?: string workspace?: string diff --git a/cli/src/types/keyboard.ts b/cli/src/types/keyboard.ts index cb07ad37d91..001a567ccc0 100644 --- a/cli/src/types/keyboard.ts +++ b/cli/src/types/keyboard.ts @@ -1,49 +1,19 @@ /** - * Key event types and interfaces for the keyboard system + * Keyboard Types + * + * Re-exports types from @kilocode/core-schemas for runtime validation + * and backward compatibility with existing code. */ -/** - * Represents a parsed key event with all relevant information - */ -export interface Key { - /** Key name (e.g., 'a', 'return', 'escape', 'up', 'down') */ - name: string - /** Whether Ctrl modifier is pressed */ - ctrl: boolean - /** Whether Alt/Meta modifier is pressed */ - meta: boolean - /** Whether Shift modifier is pressed */ - shift: boolean - /** Whether this is a paste event containing multiple characters */ - paste: boolean - /** Raw key sequence as received from terminal */ - sequence: string - /** Whether this was parsed using Kitty keyboard protocol */ - kittyProtocol?: boolean -} - -/** - * Represents a key object from Node's readline keypress event - */ -export interface ReadlineKey { - name?: string - sequence: string - ctrl?: boolean - meta?: boolean - shift?: boolean -} - -/** - * Handler function type for key events - */ -export type KeypressHandler = (key: Key) => void - -/** - * Configuration for the KeyboardProvider - */ -export interface KeyboardProviderConfig { - /** Enable debug logging for keystrokes */ - debugKeystrokeLogging?: boolean - /** Custom escape code timeout (ms) */ - escapeCodeTimeout?: number -} +// Re-export all keyboard types from core-schemas +export { + // Schemas + keySchema, + readlineKeySchema, + keyboardProviderConfigSchema, + // Types + type Key, + type ReadlineKey, + type KeyboardProviderConfig, + type KeypressHandler, +} from "@kilocode/core-schemas" diff --git a/cli/src/types/messages.ts b/cli/src/types/messages.ts index 85d82b61225..e8a63f76e15 100644 --- a/cli/src/types/messages.ts +++ b/cli/src/types/messages.ts @@ -42,8 +42,24 @@ export type ExtensionChatMessage = ClineMessage export type { RouterName, ModelInfo, ModelRecord, RouterModels } // ============================================ -// CLI-SPECIFIC TYPES (Keep these) +// CLI-SPECIFIC TYPES from core-schemas // ============================================ +export { + // Extension message schemas + organizationAllowListSchema, + extensionMessageSchema, + extensionStateSchema, + // Extension message types (also exported for backward compat) + type OrganizationAllowList, + type Mode, +} from "@kilocode/core-schemas" + +// ============================================ +// CLI-SPECIFIC TYPES (Interface definitions for complex types +// that require imports from @roo-code/types) +// ============================================ + +// ExtensionMessage interface with proper typing export interface ExtensionMessage { type: string action?: string @@ -55,19 +71,7 @@ export interface ExtensionMessage { [key: string]: unknown } -// Organization Allow List for provider validation -export interface OrganizationAllowList { - allowAll: boolean - providers: Record< - string, - { - allowAll: boolean - models?: string[] - } - > -} - -// CLI-specific ExtensionState +// CLI-specific ExtensionState with proper typing export interface ExtensionState { version: string apiConfiguration: ProviderSettings @@ -85,10 +89,8 @@ export interface ExtensionState { telemetrySetting: string renderContext: "sidebar" | "editor" | "cli" cwd?: string - organizationAllowList?: OrganizationAllowList + organizationAllowList?: import("@kilocode/core-schemas").OrganizationAllowList routerModels?: RouterModels appendSystemPrompt?: string // Custom text to append to system prompt (CLI only) [key: string]: unknown } - -export type Mode = string diff --git a/cli/src/types/theme.ts b/cli/src/types/theme.ts index 6a40b847af0..f3ad6c0c08b 100644 --- a/cli/src/types/theme.ts +++ b/cli/src/types/theme.ts @@ -1,116 +1,18 @@ /** - * Theme type definitions for Kilo Code CLI + * Theme Types * - * Defines the structure for color themes used throughout the CLI interface. + * Re-exports types from @kilocode/core-schemas for runtime validation + * and backward compatibility with existing code. */ -/** - * Theme type for categorization - */ -export type ThemeType = "dark" | "light" | "custom" - -/** - * Core theme interface defining all color categories - */ -export interface Theme { - /** Theme identifier */ - id: string - /** Theme display name */ - name: string - /** Theme type for categorization */ - type: ThemeType - - /** Brand identity colors */ - brand: { - primary: string - secondary: string - } - - /** Semantic colors for common states */ - semantic: { - success: string - error: string - warning: string - info: string - neutral: string - } - - /** Interactive element colors */ - interactive: { - prompt: string - selection: string - hover: string - disabled: string - focus: string - } - - /** Message type colors */ - messages: { - user: string - assistant: string - system: string - error: string - } - - /** Action colors (unified approve/reject/cancel patterns) */ - actions: { - approve: string - reject: string - cancel: string - pending: string - } - - /** Code and diff display colors */ - code: { - addition: string - deletion: string - modification: string - context: string - lineNumber: string - } - - /** Markdown rendering colors */ - markdown: { - text: string - heading: string - strong: string - em: string - code: string - blockquote: string - link: string - list: string - } - - /** UI structure colors */ - ui: { - border: { - default: string - active: string - warning: string - error: string - } - text: { - primary: string - secondary: string - dimmed: string - highlight: string - } - background: { - default: string - elevated: string - } - } - - /** Status indicator colors */ - status: { - online: string - offline: string - busy: string - idle: string - } -} - -/** - * Theme identifier type - */ -export type ThemeId = "dark" | "light" | string +// Re-export all theme types from core-schemas +export { + // Schemas + themeTypeSchema, + themeSchema, + themeIdSchema, + // Types + type ThemeType, + type Theme, + type ThemeId, +} from "@kilocode/core-schemas" diff --git a/packages/core-schemas/package.json b/packages/core-schemas/package.json new file mode 100644 index 00000000000..de30dfb8e45 --- /dev/null +++ b/packages/core-schemas/package.json @@ -0,0 +1,38 @@ +{ + "name": "@kilocode/core-schemas", + "version": "0.0.0", + "type": "module", + "main": "./dist/index.cjs", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "scripts": { + "lint": "eslint src --ext=ts --max-warnings=0", + "check-types": "tsc --noEmit", + "test": "vitest run", + "build": "tsup", + "clean": "rimraf dist .turbo" + }, + "dependencies": { + "zod": "^3.25.61" + }, + "peerDependencies": { + "@roo-code/types": "workspace:^" + }, + "devDependencies": { + "@roo-code/config-eslint": "workspace:^", + "@roo-code/config-typescript": "workspace:^", + "@roo-code/types": "workspace:^", + "@types/node": "20.x", + "globals": "^16.3.0", + "tsup": "^8.3.5", + "vitest": "^3.2.3" + } +} diff --git a/packages/core-schemas/src/agent-manager/index.ts b/packages/core-schemas/src/agent-manager/index.ts new file mode 100644 index 00000000000..2d6a0aee31a --- /dev/null +++ b/packages/core-schemas/src/agent-manager/index.ts @@ -0,0 +1,2 @@ +// Agent manager schemas and types +export * from "./types.js" diff --git a/packages/core-schemas/src/agent-manager/types.ts b/packages/core-schemas/src/agent-manager/types.ts new file mode 100644 index 00000000000..60fc385ab36 --- /dev/null +++ b/packages/core-schemas/src/agent-manager/types.ts @@ -0,0 +1,115 @@ +import { z } from "zod" + +/** + * Agent Manager Types + * + * These types are used by the agent-manager in the extension for managing + * CLI sessions and parallel mode worktrees. + */ + +/** + * Agent status schema + */ +export const agentStatusSchema = z.enum(["creating", "running", "done", "error", "stopped"]) + +/** + * Session source schema + */ +export const sessionSourceSchema = z.enum(["local", "remote"]) + +/** + * Parallel mode (worktree) information schema + */ +export const parallelModeInfoSchema = z.object({ + enabled: z.boolean(), + branch: z.string().optional(), // e.g., "add-authentication-1702734891234" + worktreePath: z.string().optional(), // e.g., ".kilocode/worktrees/add-auth..." + parentBranch: z.string().optional(), // e.g., "main" - the branch worktree was created from + completionMessage: z.string().optional(), // Merge instructions from CLI on completion +}) + +/** + * Agent session schema + */ +export const agentSessionSchema = z.object({ + sessionId: z.string(), + label: z.string(), + prompt: z.string(), + status: agentStatusSchema, + startTime: z.number(), + endTime: z.number().optional(), + exitCode: z.number().optional(), + error: z.string().optional(), + logs: z.array(z.string()), + pid: z.number().optional(), + source: sessionSourceSchema, + parallelMode: parallelModeInfoSchema.optional(), + gitUrl: z.string().optional(), +}) + +/** + * Pending session schema (waiting for CLI's session_created event) + */ +export const pendingSessionSchema = z.object({ + prompt: z.string(), + label: z.string(), + startTime: z.number(), + parallelMode: z.boolean().optional(), + gitUrl: z.string().optional(), +}) + +/** + * Agent manager state schema + */ +export const agentManagerStateSchema = z.object({ + sessions: z.array(agentSessionSchema), + selectedId: z.string().nullable(), +}) + +/** + * Messages from Webview to Extension + */ +export const agentManagerMessageSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("agentManager.webviewReady") }), + z.object({ + type: z.literal("agentManager.startSession"), + prompt: z.string(), + parallelMode: z.boolean().optional(), + existingBranch: z.string().optional(), + }), + z.object({ type: z.literal("agentManager.stopSession"), sessionId: z.string() }), + z.object({ type: z.literal("agentManager.selectSession"), sessionId: z.string() }), + z.object({ type: z.literal("agentManager.refreshRemoteSessions") }), + z.object({ type: z.literal("agentManager.listBranches") }), +]) + +/** + * Remote session schema (simplified - full type comes from shared session client) + */ +export const remoteSessionSchema = z.object({ + id: z.string(), + name: z.string().optional(), + status: z.string().optional(), +}).passthrough() // Allow additional fields from the full RemoteSession type + +/** + * Messages from Extension to Webview + */ +export const agentManagerExtensionMessageSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("agentManager.state"), state: agentManagerStateSchema }), + z.object({ type: z.literal("agentManager.sessionUpdated"), session: agentSessionSchema }), + z.object({ type: z.literal("agentManager.sessionRemoved"), sessionId: z.string() }), + z.object({ type: z.literal("agentManager.error"), error: z.string() }), + z.object({ type: z.literal("agentManager.remoteSessions"), sessions: z.array(remoteSessionSchema) }), + z.object({ type: z.literal("agentManager.branches"), branches: z.array(z.string()), currentBranch: z.string().optional() }), +]) + +// Inferred types +export type AgentStatus = z.infer +export type SessionSource = z.infer +export type ParallelModeInfo = z.infer +export type AgentSession = z.infer +export type PendingSession = z.infer +export type AgentManagerState = z.infer +export type AgentManagerMessage = z.infer +export type AgentManagerExtensionMessage = z.infer diff --git a/packages/core-schemas/src/auth/index.ts b/packages/core-schemas/src/auth/index.ts new file mode 100644 index 00000000000..5059773edd4 --- /dev/null +++ b/packages/core-schemas/src/auth/index.ts @@ -0,0 +1,6 @@ +// Re-export device auth types from @roo-code/types +export type { DeviceAuthInitiateResponse, DeviceAuthPollResponse, DeviceAuthState } from "@roo-code/types" +export { DeviceAuthInitiateResponseSchema, DeviceAuthPollResponseSchema } from "@roo-code/types" + +// Kilocode-specific auth types +export * from "./kilocode.js" diff --git a/packages/core-schemas/src/auth/kilocode.ts b/packages/core-schemas/src/auth/kilocode.ts new file mode 100644 index 00000000000..b53f6563c21 --- /dev/null +++ b/packages/core-schemas/src/auth/kilocode.ts @@ -0,0 +1,76 @@ +import { z } from "zod" +import type { ProviderConfig } from "../config/provider.js" + +/** + * Organization data from Kilocode API + */ +export const kilocodeOrganizationSchema = z.object({ + id: z.string(), + name: z.string(), + role: z.string(), +}) + +/** + * Profile data structure from Kilocode API + */ +export const kilocodeProfileDataSchema = z.object({ + user: z + .object({ + name: z.string().optional(), + email: z.string().optional(), + image: z.string().optional(), + }) + .optional(), + organizations: z.array(kilocodeOrganizationSchema).optional(), +}) + +/** + * Options for polling operations + */ +export const pollingOptionsSchema = z.object({ + /** Interval between polls in milliseconds */ + interval: z.number(), + /** Maximum number of attempts before timeout */ + maxAttempts: z.number(), + /** Function to execute on each poll */ + pollFn: z.function().args().returns(z.promise(z.unknown())), + /** Optional callback for progress updates */ + onProgress: z.function().args(z.number(), z.number()).returns(z.void()).optional(), +}) + +/** + * Result of a poll operation + */ +export const pollResultSchema = z.object({ + /** Whether polling should continue */ + continue: z.boolean(), + /** Optional data returned when polling completes */ + data: z.unknown().optional(), + /** Optional error if polling failed */ + error: z.instanceof(Error).optional(), +}) + +// Inferred types +export type KilocodeOrganization = z.infer +export type KilocodeProfileData = z.infer +export type PollingOptions = z.infer +export type PollResult = z.infer + +/** + * Result of a successful authentication flow + */ +export interface AuthResult { + providerConfig: ProviderConfig +} + +/** + * Base interface for all authentication providers + */ +export interface AuthProvider { + /** Display name shown to users */ + name: string + /** Unique identifier for the provider */ + value: string + /** Execute the authentication flow */ + authenticate(): Promise +} diff --git a/packages/core-schemas/src/config/auto-approval.ts b/packages/core-schemas/src/config/auto-approval.ts new file mode 100644 index 00000000000..657bb874744 --- /dev/null +++ b/packages/core-schemas/src/config/auto-approval.ts @@ -0,0 +1,108 @@ +import { z } from "zod" + +/** + * Auto approval configuration for read operations + */ +export const autoApprovalReadSchema = z.object({ + enabled: z.boolean().optional(), + outside: z.boolean().optional(), +}) + +/** + * Auto approval configuration for write operations + */ +export const autoApprovalWriteSchema = z.object({ + enabled: z.boolean().optional(), + outside: z.boolean().optional(), + protected: z.boolean().optional(), +}) + +/** + * Auto approval configuration for browser operations + */ +export const autoApprovalBrowserSchema = z.object({ + enabled: z.boolean().optional(), +}) + +/** + * Auto approval configuration for retry operations + */ +export const autoApprovalRetrySchema = z.object({ + enabled: z.boolean().optional(), + delay: z.number().optional(), +}) + +/** + * Auto approval configuration for MCP operations + */ +export const autoApprovalMcpSchema = z.object({ + enabled: z.boolean().optional(), +}) + +/** + * Auto approval configuration for mode switching + */ +export const autoApprovalModeSchema = z.object({ + enabled: z.boolean().optional(), +}) + +/** + * Auto approval configuration for subtasks + */ +export const autoApprovalSubtasksSchema = z.object({ + enabled: z.boolean().optional(), +}) + +/** + * Auto approval configuration for command execution + */ +export const autoApprovalExecuteSchema = z.object({ + enabled: z.boolean().optional(), + allowed: z.array(z.string()).optional(), + denied: z.array(z.string()).optional(), +}) + +/** + * Auto approval configuration for followup questions + */ +export const autoApprovalQuestionSchema = z.object({ + enabled: z.boolean().optional(), + timeout: z.number().optional(), +}) + +/** + * Auto approval configuration for todo list updates + */ +export const autoApprovalTodoSchema = z.object({ + enabled: z.boolean().optional(), +}) + +/** + * Complete auto approval configuration + */ +export const autoApprovalConfigSchema = z.object({ + enabled: z.boolean().optional(), + read: autoApprovalReadSchema.optional(), + write: autoApprovalWriteSchema.optional(), + browser: autoApprovalBrowserSchema.optional(), + retry: autoApprovalRetrySchema.optional(), + mcp: autoApprovalMcpSchema.optional(), + mode: autoApprovalModeSchema.optional(), + subtasks: autoApprovalSubtasksSchema.optional(), + execute: autoApprovalExecuteSchema.optional(), + question: autoApprovalQuestionSchema.optional(), + todo: autoApprovalTodoSchema.optional(), +}) + +// Inferred types +export type AutoApprovalReadConfig = z.infer +export type AutoApprovalWriteConfig = z.infer +export type AutoApprovalBrowserConfig = z.infer +export type AutoApprovalRetryConfig = z.infer +export type AutoApprovalMcpConfig = z.infer +export type AutoApprovalModeConfig = z.infer +export type AutoApprovalSubtasksConfig = z.infer +export type AutoApprovalExecuteConfig = z.infer +export type AutoApprovalQuestionConfig = z.infer +export type AutoApprovalTodoConfig = z.infer +export type AutoApprovalConfig = z.infer diff --git a/packages/core-schemas/src/config/cli-config.ts b/packages/core-schemas/src/config/cli-config.ts new file mode 100644 index 00000000000..a0bd38bfe83 --- /dev/null +++ b/packages/core-schemas/src/config/cli-config.ts @@ -0,0 +1,53 @@ +import { z } from "zod" +import { providerConfigSchema } from "./provider.js" +import { autoApprovalConfigSchema } from "./auto-approval.js" +import { themeSchema, themeIdSchema } from "../theme/theme.js" + +/** + * CLI configuration schema + */ +export const cliConfigSchema = z.object({ + version: z.literal("1.0.0"), + mode: z.string(), + telemetry: z.boolean(), + provider: z.string(), + providers: z.array(providerConfigSchema), + autoApproval: autoApprovalConfigSchema.optional(), + theme: themeIdSchema.optional(), + customThemes: z.record(themeSchema).optional(), +}) + +// Inferred type +export type CLIConfig = z.infer + +// Type guard +export function isValidConfig(config: unknown): config is CLIConfig { + return cliConfigSchema.safeParse(config).success +} + +/** + * Validation result structure + */ +export const validationResultSchema = z.object({ + valid: z.boolean(), + errors: z + .array( + z.object({ + path: z.array(z.union([z.string(), z.number()])), + message: z.string(), + }), + ) + .optional(), +}) + +export type ValidationResult = z.infer + +/** + * Config load result structure + */ +export const configLoadResultSchema = z.object({ + config: cliConfigSchema.optional(), + validation: validationResultSchema, +}) + +export type ConfigLoadResult = z.infer diff --git a/packages/core-schemas/src/config/history.ts b/packages/core-schemas/src/config/history.ts new file mode 100644 index 00000000000..428464629e7 --- /dev/null +++ b/packages/core-schemas/src/config/history.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +/** + * Single history entry + */ +export const historyEntrySchema = z.object({ + prompt: z.string(), + timestamp: z.number(), +}) + +/** + * History data structure + */ +export const historyDataSchema = z.object({ + version: z.string(), + maxSize: z.number(), + entries: z.array(historyEntrySchema), +}) + +// Inferred types +export type HistoryEntry = z.infer +export type HistoryData = z.infer diff --git a/packages/core-schemas/src/config/index.ts b/packages/core-schemas/src/config/index.ts new file mode 100644 index 00000000000..9187d07e31b --- /dev/null +++ b/packages/core-schemas/src/config/index.ts @@ -0,0 +1,11 @@ +// Provider configuration schemas and types +export * from "./provider.js" + +// Auto-approval configuration schemas and types +export * from "./auto-approval.js" + +// CLI configuration schemas and types +export * from "./cli-config.js" + +// History schemas and types +export * from "./history.js" diff --git a/packages/core-schemas/src/config/provider.ts b/packages/core-schemas/src/config/provider.ts new file mode 100644 index 00000000000..c5980c03978 --- /dev/null +++ b/packages/core-schemas/src/config/provider.ts @@ -0,0 +1,479 @@ +import { z } from "zod" + +// Base schema all providers extend +const baseProviderSchema = z.object({ + id: z.string(), +}) + +// Kilocode provider +export const kilocodeProviderSchema = baseProviderSchema.extend({ + provider: z.literal("kilocode"), + kilocodeModel: z.string().optional(), + kilocodeToken: z.string().optional(), + kilocodeOrganizationId: z.string().optional(), + openRouterSpecificProvider: z.string().optional(), + openRouterProviderDataCollection: z.enum(["allow", "deny"]).optional(), + openRouterProviderSort: z.enum(["price", "throughput", "latency"]).optional(), + openRouterZdr: z.boolean().optional(), + kilocodeTesterWarningsDisabledUntil: z.number().optional(), +}) + +// Anthropic provider +export const anthropicProviderSchema = baseProviderSchema.extend({ + provider: z.literal("anthropic"), + apiModelId: z.string().optional(), + apiKey: z.string().optional(), + anthropicBaseUrl: z.string().optional(), + anthropicUseAuthToken: z.boolean().optional(), + anthropicBeta1MContext: z.boolean().optional(), +}) + +// OpenAI Native provider +export const openAINativeProviderSchema = baseProviderSchema.extend({ + provider: z.literal("openai-native"), + apiModelId: z.string().optional(), + openAiNativeApiKey: z.string().optional(), + openAiNativeBaseUrl: z.string().optional(), + openAiNativeServiceTier: z.enum(["auto", "default", "flex", "priority"]).optional(), +}) + +// OpenAI provider +export const openAIProviderSchema = baseProviderSchema.extend({ + provider: z.literal("openai"), + openAiModelId: z.string().optional(), + openAiBaseUrl: z.string().optional(), + openAiApiKey: z.string().optional(), + openAiLegacyFormat: z.boolean().optional(), + openAiR1FormatEnabled: z.boolean().optional(), + openAiUseAzure: z.boolean().optional(), + azureApiVersion: z.string().optional(), + openAiStreamingEnabled: z.boolean().optional(), + openAiHeaders: z.record(z.string()).optional(), +}) + +// OpenRouter provider +export const openRouterProviderSchema = baseProviderSchema.extend({ + provider: z.literal("openrouter"), + openRouterModelId: z.string().optional(), + openRouterApiKey: z.string().optional(), + openRouterBaseUrl: z.string().optional(), + openRouterSpecificProvider: z.string().optional(), + openRouterUseMiddleOutTransform: z.boolean().optional(), + openRouterProviderDataCollection: z.enum(["allow", "deny"]).optional(), + openRouterProviderSort: z.enum(["price", "throughput", "latency"]).optional(), + openRouterZdr: z.boolean().optional(), +}) + +// Ollama provider +export const ollamaProviderSchema = baseProviderSchema.extend({ + provider: z.literal("ollama"), + ollamaModelId: z.string().optional(), + ollamaBaseUrl: z.string().optional(), + ollamaApiKey: z.string().optional(), + ollamaNumCtx: z.number().optional(), +}) + +// LM Studio provider +export const lmStudioProviderSchema = baseProviderSchema.extend({ + provider: z.literal("lmstudio"), + lmStudioModelId: z.string().optional(), + lmStudioBaseUrl: z.string().optional(), + lmStudioDraftModelId: z.string().optional(), + lmStudioSpeculativeDecodingEnabled: z.boolean().optional(), +}) + +// Glama provider +export const glamaProviderSchema = baseProviderSchema.extend({ + provider: z.literal("glama"), + glamaModelId: z.string().optional(), + glamaApiKey: z.string().optional(), +}) + +// LiteLLM provider +export const liteLLMProviderSchema = baseProviderSchema.extend({ + provider: z.literal("litellm"), + litellmModelId: z.string().optional(), + litellmBaseUrl: z.string().optional(), + litellmApiKey: z.string().optional(), + litellmUsePromptCache: z.boolean().optional(), +}) + +// DeepInfra provider +export const deepInfraProviderSchema = baseProviderSchema.extend({ + provider: z.literal("deepinfra"), + deepInfraModelId: z.string().optional(), + deepInfraBaseUrl: z.string().optional(), + deepInfraApiKey: z.string().optional(), +}) + +// Unbound provider +export const unboundProviderSchema = baseProviderSchema.extend({ + provider: z.literal("unbound"), + unboundModelId: z.string().optional(), + unboundApiKey: z.string().optional(), +}) + +// Requesty provider +export const requestyProviderSchema = baseProviderSchema.extend({ + provider: z.literal("requesty"), + requestyModelId: z.string().optional(), + requestyBaseUrl: z.string().optional(), + requestyApiKey: z.string().optional(), +}) + +// Vercel AI Gateway provider +export const vercelAiGatewayProviderSchema = baseProviderSchema.extend({ + provider: z.literal("vercel-ai-gateway"), + vercelAiGatewayModelId: z.string().optional(), + vercelAiGatewayApiKey: z.string().optional(), +}) + +// IO Intelligence provider +export const ioIntelligenceProviderSchema = baseProviderSchema.extend({ + provider: z.literal("io-intelligence"), + ioIntelligenceModelId: z.string().optional(), + ioIntelligenceApiKey: z.string().optional(), +}) + +// OVH Cloud provider +export const ovhCloudProviderSchema = baseProviderSchema.extend({ + provider: z.literal("ovhcloud"), + ovhCloudAiEndpointsModelId: z.string().optional(), + ovhCloudAiEndpointsApiKey: z.string().optional(), + ovhCloudAiEndpointsBaseUrl: z.string().optional(), +}) + +// Inception provider +export const inceptionProviderSchema = baseProviderSchema.extend({ + provider: z.literal("inception"), + inceptionLabsModelId: z.string().optional(), + inceptionLabsBaseUrl: z.string().optional(), + inceptionLabsApiKey: z.string().optional(), +}) + +// Bedrock provider (AWS) +export const bedrockProviderSchema = baseProviderSchema.extend({ + provider: z.literal("bedrock"), + apiModelId: z.string().optional(), + awsAccessKey: z.string().optional(), + awsSecretKey: z.string().optional(), + awsSessionToken: z.string().optional(), + awsRegion: z.string().optional(), + awsUseCrossRegionInference: z.boolean().optional(), + awsUsePromptCache: z.boolean().optional(), + awsProfile: z.string().optional(), + awsUseProfile: z.boolean().optional(), + awsApiKey: z.string().optional(), + awsUseApiKey: z.boolean().optional(), + awsCustomArn: z.string().optional(), + awsModelContextWindow: z.number().optional(), + awsBedrockEndpointEnabled: z.boolean().optional(), + awsBedrockEndpoint: z.string().optional(), + awsBedrock1MContext: z.boolean().optional(), +}) + +// Vertex provider +export const vertexProviderSchema = baseProviderSchema.extend({ + provider: z.literal("vertex"), + apiModelId: z.string().optional(), + vertexKeyFile: z.string().optional(), + vertexJsonCredentials: z.string().optional(), + vertexProjectId: z.string().optional(), + vertexRegion: z.string().optional(), + enableUrlContext: z.boolean().optional(), + enableGrounding: z.boolean().optional(), +}) + +// Gemini provider +export const geminiProviderSchema = baseProviderSchema.extend({ + provider: z.literal("gemini"), + apiModelId: z.string().optional(), + geminiApiKey: z.string().optional(), + googleGeminiBaseUrl: z.string().optional(), + enableUrlContext: z.boolean().optional(), + enableGrounding: z.boolean().optional(), +}) + +// Gemini CLI provider +export const geminiCliProviderSchema = baseProviderSchema.extend({ + provider: z.literal("gemini-cli"), + apiModelId: z.string().optional(), + geminiCliOAuthPath: z.string().optional(), + geminiCliProjectId: z.string().optional(), +}) + +// Mistral provider +export const mistralProviderSchema = baseProviderSchema.extend({ + provider: z.literal("mistral"), + apiModelId: z.string().optional(), + mistralApiKey: z.string().optional(), + mistralCodestralUrl: z.string().optional(), +}) + +// Moonshot provider +export const moonshotProviderSchema = baseProviderSchema.extend({ + provider: z.literal("moonshot"), + apiModelId: z.string().optional(), + moonshotBaseUrl: z.string().optional(), + moonshotApiKey: z.string().optional(), +}) + +// Minimax provider +export const minimaxProviderSchema = baseProviderSchema.extend({ + provider: z.literal("minimax"), + apiModelId: z.string().optional(), + minimaxBaseUrl: z.string().optional(), + minimaxApiKey: z.string().optional(), +}) + +// DeepSeek provider +export const deepSeekProviderSchema = baseProviderSchema.extend({ + provider: z.literal("deepseek"), + apiModelId: z.string().optional(), + deepSeekBaseUrl: z.string().optional(), + deepSeekApiKey: z.string().optional(), +}) + +// Doubao provider +export const doubaoProviderSchema = baseProviderSchema.extend({ + provider: z.literal("doubao"), + apiModelId: z.string().optional(), + doubaoBaseUrl: z.string().optional(), + doubaoApiKey: z.string().optional(), +}) + +// Qwen Code provider +export const qwenCodeProviderSchema = baseProviderSchema.extend({ + provider: z.literal("qwen-code"), + apiModelId: z.string().optional(), + qwenCodeOauthPath: z.string().optional(), +}) + +// XAI provider +export const xaiProviderSchema = baseProviderSchema.extend({ + provider: z.literal("xai"), + apiModelId: z.string().optional(), + xaiApiKey: z.string().optional(), +}) + +// Groq provider +export const groqProviderSchema = baseProviderSchema.extend({ + provider: z.literal("groq"), + apiModelId: z.string().optional(), + groqApiKey: z.string().optional(), +}) + +// Chutes provider +export const chutesProviderSchema = baseProviderSchema.extend({ + provider: z.literal("chutes"), + apiModelId: z.string().optional(), + chutesApiKey: z.string().optional(), +}) + +// Cerebras provider +export const cerebrasProviderSchema = baseProviderSchema.extend({ + provider: z.literal("cerebras"), + apiModelId: z.string().optional(), + cerebrasApiKey: z.string().optional(), +}) + +// SambaNova provider +export const sambaNovaProviderSchema = baseProviderSchema.extend({ + provider: z.literal("sambanova"), + apiModelId: z.string().optional(), + sambaNovaApiKey: z.string().optional(), +}) + +// ZAI provider +export const zaiProviderSchema = baseProviderSchema.extend({ + provider: z.literal("zai"), + apiModelId: z.string().optional(), + zaiApiKey: z.string().optional(), + zaiApiLine: z.enum(["international_coding", "china_coding"]).optional(), +}) + +// Fireworks provider +export const fireworksProviderSchema = baseProviderSchema.extend({ + provider: z.literal("fireworks"), + apiModelId: z.string().optional(), + fireworksApiKey: z.string().optional(), +}) + +// Featherless provider +export const featherlessProviderSchema = baseProviderSchema.extend({ + provider: z.literal("featherless"), + apiModelId: z.string().optional(), + featherlessApiKey: z.string().optional(), +}) + +// Roo provider +export const rooProviderSchema = baseProviderSchema.extend({ + provider: z.literal("roo"), + apiModelId: z.string().optional(), +}) + +// Claude Code provider +export const claudeCodeProviderSchema = baseProviderSchema.extend({ + provider: z.literal("claude-code"), + apiModelId: z.string().optional(), + claudeCodePath: z.string().optional(), + claudeCodeMaxOutputTokens: z.number().optional(), +}) + +// VSCode LM provider +export const vsCodeLMProviderSchema = baseProviderSchema.extend({ + provider: z.literal("vscode-lm"), + vsCodeLmModelSelector: z + .object({ + vendor: z.string().optional(), + family: z.string().optional(), + version: z.string().optional(), + id: z.string().optional(), + }) + .optional(), +}) + +// HuggingFace provider +export const huggingFaceProviderSchema = baseProviderSchema.extend({ + provider: z.literal("huggingface"), + huggingFaceModelId: z.string().optional(), + huggingFaceApiKey: z.string().optional(), + huggingFaceInferenceProvider: z.string().optional(), +}) + +// Synthetic provider +export const syntheticProviderSchema = baseProviderSchema.extend({ + provider: z.literal("synthetic"), + apiModelId: z.string().optional(), + syntheticApiKey: z.string().optional(), +}) + +// Virtual Quota Fallback provider +export const virtualQuotaFallbackProviderSchema = baseProviderSchema.extend({ + provider: z.literal("virtual-quota-fallback"), + profiles: z + .array( + z.object({ + profileName: z.string().optional(), + profileId: z.string().optional(), + profileLimits: z + .object({ + tokensPerMinute: z.number().optional(), + tokensPerHour: z.number().optional(), + tokensPerDay: z.number().optional(), + requestsPerMinute: z.number().optional(), + requestsPerHour: z.number().optional(), + requestsPerDay: z.number().optional(), + }) + .optional(), + }), + ) + .optional(), +}) + +// Human Relay provider +export const humanRelayProviderSchema = baseProviderSchema.extend({ + provider: z.literal("human-relay"), +}) + +// Fake AI provider (for testing) +export const fakeAIProviderSchema = baseProviderSchema.extend({ + provider: z.literal("fake-ai"), + fakeAi: z.unknown().optional(), +}) + +// Discriminated union of all provider configs +export const providerConfigSchema = z.discriminatedUnion("provider", [ + kilocodeProviderSchema, + anthropicProviderSchema, + openAINativeProviderSchema, + openAIProviderSchema, + openRouterProviderSchema, + ollamaProviderSchema, + lmStudioProviderSchema, + glamaProviderSchema, + liteLLMProviderSchema, + deepInfraProviderSchema, + unboundProviderSchema, + requestyProviderSchema, + vercelAiGatewayProviderSchema, + ioIntelligenceProviderSchema, + ovhCloudProviderSchema, + inceptionProviderSchema, + bedrockProviderSchema, + vertexProviderSchema, + geminiProviderSchema, + geminiCliProviderSchema, + mistralProviderSchema, + moonshotProviderSchema, + minimaxProviderSchema, + deepSeekProviderSchema, + doubaoProviderSchema, + qwenCodeProviderSchema, + xaiProviderSchema, + groqProviderSchema, + chutesProviderSchema, + cerebrasProviderSchema, + sambaNovaProviderSchema, + zaiProviderSchema, + fireworksProviderSchema, + featherlessProviderSchema, + rooProviderSchema, + claudeCodeProviderSchema, + vsCodeLMProviderSchema, + huggingFaceProviderSchema, + syntheticProviderSchema, + virtualQuotaFallbackProviderSchema, + humanRelayProviderSchema, + fakeAIProviderSchema, +]) + +// Inferred types +export type KilocodeProviderConfig = z.infer +export type AnthropicProviderConfig = z.infer +export type OpenAINativeProviderConfig = z.infer +export type OpenAIProviderConfig = z.infer +export type OpenRouterProviderConfig = z.infer +export type OllamaProviderConfig = z.infer +export type LMStudioProviderConfig = z.infer +export type GlamaProviderConfig = z.infer +export type LiteLLMProviderConfig = z.infer +export type DeepInfraProviderConfig = z.infer +export type UnboundProviderConfig = z.infer +export type RequestyProviderConfig = z.infer +export type VercelAiGatewayProviderConfig = z.infer +export type IOIntelligenceProviderConfig = z.infer +export type OVHCloudProviderConfig = z.infer +export type InceptionProviderConfig = z.infer +export type BedrockProviderConfig = z.infer +export type VertexProviderConfig = z.infer +export type GeminiProviderConfig = z.infer +export type GeminiCliProviderConfig = z.infer +export type MistralProviderConfig = z.infer +export type MoonshotProviderConfig = z.infer +export type MinimaxProviderConfig = z.infer +export type DeepSeekProviderConfig = z.infer +export type DoubaoProviderConfig = z.infer +export type QwenCodeProviderConfig = z.infer +export type XAIProviderConfig = z.infer +export type GroqProviderConfig = z.infer +export type ChutesProviderConfig = z.infer +export type CerebrasProviderConfig = z.infer +export type SambaNovaProviderConfig = z.infer +export type ZAIProviderConfig = z.infer +export type FireworksProviderConfig = z.infer +export type FeatherlessProviderConfig = z.infer +export type RooProviderConfig = z.infer +export type ClaudeCodeProviderConfig = z.infer +export type VSCodeLMProviderConfig = z.infer +export type HuggingFaceProviderConfig = z.infer +export type SyntheticProviderConfig = z.infer +export type VirtualQuotaFallbackProviderConfig = z.infer +export type HumanRelayProviderConfig = z.infer +export type FakeAIProviderConfig = z.infer +export type ProviderConfig = z.infer + +// Type guards +export function isProviderConfig(provider: unknown): provider is ProviderConfig { + return providerConfigSchema.safeParse(provider).success +} diff --git a/packages/core-schemas/src/index.ts b/packages/core-schemas/src/index.ts new file mode 100644 index 00000000000..cccb56c3885 --- /dev/null +++ b/packages/core-schemas/src/index.ts @@ -0,0 +1,27 @@ +/** + * @kilocode/core-schemas + * + * Zod schemas and inferred types for CLI configuration and runtime data. + * This package provides runtime validation and type inference for the CLI. + */ + +// Configuration schemas +export * from "./config/index.js" + +// Authentication schemas +export * from "./auth/index.js" + +// Message schemas +export * from "./messages/index.js" + +// MCP (Model Context Protocol) schemas +export * from "./mcp/index.js" + +// Keyboard input schemas +export * from "./keyboard/index.js" + +// Theme schemas +export * from "./theme/index.js" + +// Agent manager schemas +export * from "./agent-manager/index.js" diff --git a/packages/core-schemas/src/keyboard/index.ts b/packages/core-schemas/src/keyboard/index.ts new file mode 100644 index 00000000000..0e6e01c193b --- /dev/null +++ b/packages/core-schemas/src/keyboard/index.ts @@ -0,0 +1,2 @@ +// Keyboard input schemas and types +export * from "./key.js" diff --git a/packages/core-schemas/src/keyboard/key.ts b/packages/core-schemas/src/keyboard/key.ts new file mode 100644 index 00000000000..03b90824e4d --- /dev/null +++ b/packages/core-schemas/src/keyboard/key.ts @@ -0,0 +1,52 @@ +import { z } from "zod" + +/** + * Represents a parsed key event with all relevant information + */ +export const keySchema = z.object({ + /** Key name (e.g., 'a', 'return', 'escape', 'up', 'down') */ + name: z.string(), + /** Whether Ctrl modifier is pressed */ + ctrl: z.boolean(), + /** Whether Alt/Meta modifier is pressed */ + meta: z.boolean(), + /** Whether Shift modifier is pressed */ + shift: z.boolean(), + /** Whether this is a paste event containing multiple characters */ + paste: z.boolean(), + /** Raw key sequence as received from terminal */ + sequence: z.string(), + /** Whether this was parsed using Kitty keyboard protocol */ + kittyProtocol: z.boolean().optional(), +}) + +/** + * Represents a key object from Node's readline keypress event + */ +export const readlineKeySchema = z.object({ + name: z.string().optional(), + sequence: z.string(), + ctrl: z.boolean().optional(), + meta: z.boolean().optional(), + shift: z.boolean().optional(), +}) + +/** + * Configuration for the KeyboardProvider + */ +export const keyboardProviderConfigSchema = z.object({ + /** Enable debug logging for keystrokes */ + debugKeystrokeLogging: z.boolean().optional(), + /** Custom escape code timeout (ms) */ + escapeCodeTimeout: z.number().optional(), +}) + +// Inferred types +export type Key = z.infer +export type ReadlineKey = z.infer +export type KeyboardProviderConfig = z.infer + +/** + * Handler function type for key events + */ +export type KeypressHandler = (key: Key) => void diff --git a/packages/core-schemas/src/mcp/index.ts b/packages/core-schemas/src/mcp/index.ts new file mode 100644 index 00000000000..903c38ffc61 --- /dev/null +++ b/packages/core-schemas/src/mcp/index.ts @@ -0,0 +1,2 @@ +// MCP server schemas and types +export * from "./server.js" diff --git a/packages/core-schemas/src/mcp/server.ts b/packages/core-schemas/src/mcp/server.ts new file mode 100644 index 00000000000..4718dd83107 --- /dev/null +++ b/packages/core-schemas/src/mcp/server.ts @@ -0,0 +1,59 @@ +import { z } from "zod" + +/** + * MCP Tool schema + */ +export const mcpToolSchema = z.object({ + name: z.string(), + description: z.string().optional(), + inputSchema: z.record(z.unknown()).optional(), +}) + +/** + * MCP Resource schema + */ +export const mcpResourceSchema = z.object({ + uri: z.string(), + name: z.string(), + description: z.string().optional(), + mimeType: z.string().optional(), +}) + +/** + * MCP Server status schema + */ +export const mcpServerStatusSchema = z.enum(["connected", "connecting", "disconnected"]) + +/** + * MCP Server schema + */ +export const mcpServerSchema = z.object({ + name: z.string(), + config: z.record(z.unknown()), + status: mcpServerStatusSchema, + tools: z.array(mcpToolSchema).optional(), + resources: z.array(mcpResourceSchema).optional(), + resourceTemplates: z.array(z.unknown()).optional(), + errorHistory: z.array(z.string()).optional(), + timeout: z.number().optional(), + source: z.enum(["global", "project"]).optional(), +}) + +/** + * MCP server data for message rendering + */ +export const mcpServerDataSchema = z.object({ + type: z.string().optional(), + serverName: z.string().optional(), + toolName: z.string().optional(), + arguments: z.string().optional(), + uri: z.string().optional(), + response: z.string().optional(), +}) + +// Inferred types +export type McpTool = z.infer +export type McpResource = z.infer +export type McpServerStatus = z.infer +export type McpServer = z.infer +export type McpServerData = z.infer diff --git a/packages/core-schemas/src/messages/cli.ts b/packages/core-schemas/src/messages/cli.ts new file mode 100644 index 00000000000..d7d3f68c5c9 --- /dev/null +++ b/packages/core-schemas/src/messages/cli.ts @@ -0,0 +1,63 @@ +import { z } from "zod" + +/** + * Welcome message options schema + */ +export const welcomeMessageOptionsSchema = z.object({ + // Clear viewport before showing the message + clearScreen: z.boolean().optional(), + // Display options + showInstructions: z.boolean().optional(), + // Content customization + instructions: z.array(z.string()).optional(), + // Parallel mode branch name + worktreeBranch: z.string().optional(), + // Workspace directory + workspace: z.string().optional(), +}) + +/** + * CLI message schema + */ +export const cliMessageSchema = z.object({ + id: z.string(), + type: z.enum(["user", "assistant", "system", "error", "welcome", "empty", "requestCheckpointRestoreApproval"]), + content: z.string(), + ts: z.number(), + partial: z.boolean().optional(), + metadata: z + .object({ + welcomeOptions: welcomeMessageOptionsSchema.optional(), + }) + .optional(), + payload: z.unknown().optional(), +}) + +/** + * CLI options schema + */ +export const cliOptionsSchema = z.object({ + mode: z.string().optional(), + workspace: z.string().optional(), + ci: z.boolean().optional(), + yolo: z.boolean().optional(), + json: z.boolean().optional(), + jsonInteractive: z.boolean().optional(), + prompt: z.string().optional(), + timeout: z.number().optional(), + customModes: z.array(z.unknown()).optional(), // ModeConfig from @roo-code/types + parallel: z.boolean().optional(), + worktreeBranch: z.string().optional(), + continue: z.boolean().optional(), + provider: z.string().optional(), + model: z.string().optional(), + session: z.string().optional(), + fork: z.string().optional(), + noSplash: z.boolean().optional(), + appendSystemPrompt: z.string().optional(), +}) + +// Inferred types +export type WelcomeMessageOptions = z.infer +export type CliMessage = z.infer +export type CLIOptions = z.infer diff --git a/packages/core-schemas/src/messages/extension.ts b/packages/core-schemas/src/messages/extension.ts new file mode 100644 index 00000000000..5c996223d95 --- /dev/null +++ b/packages/core-schemas/src/messages/extension.ts @@ -0,0 +1,60 @@ +import { z } from "zod" + +/** + * Organization Allow List for provider validation + */ +export const organizationAllowListSchema = z.object({ + allowAll: z.boolean(), + providers: z.record( + z.object({ + allowAll: z.boolean(), + models: z.array(z.string()).optional(), + }), + ), +}) + +/** + * Extension message schema + */ +export const extensionMessageSchema = z.object({ + type: z.string(), + action: z.string().optional(), + text: z.string().optional(), + state: z.unknown().optional(), // ExtensionState + images: z.array(z.string()).optional(), + chatMessages: z.array(z.unknown()).optional(), // ExtensionChatMessage[] + values: z.record(z.unknown()).optional(), +}) + +/** + * CLI-specific ExtensionState schema + */ +export const extensionStateSchema = z.object({ + version: z.string(), + apiConfiguration: z.unknown(), // ProviderSettings + currentApiConfigName: z.string().optional(), + listApiConfigMeta: z.array(z.unknown()).optional(), // ProviderSettingsEntry[] + chatMessages: z.array(z.unknown()), // ExtensionChatMessage[] + clineMessages: z.array(z.unknown()).optional(), // Cline Legacy + currentTaskItem: z.unknown().optional(), // HistoryItem + currentTaskTodos: z.array(z.unknown()).optional(), // TodoItem[] + mode: z.string(), + customModes: z.array(z.unknown()), // ModeConfig[] + taskHistoryFullLength: z.number(), + taskHistoryVersion: z.number(), + mcpServers: z.array(z.unknown()).optional(), // McpServer[] + telemetrySetting: z.string(), + renderContext: z.enum(["sidebar", "editor", "cli"]), + cwd: z.string().optional(), + organizationAllowList: organizationAllowListSchema.optional(), + routerModels: z.unknown().optional(), // RouterModels + appendSystemPrompt: z.string().optional(), // Custom text to append to system prompt (CLI only) +}) + +// Inferred types +export type OrganizationAllowList = z.infer +export type ExtensionMessage = z.infer +export type ExtensionState = z.infer + +// Mode type alias +export type Mode = string diff --git a/packages/core-schemas/src/messages/index.ts b/packages/core-schemas/src/messages/index.ts new file mode 100644 index 00000000000..9376c9a2fa1 --- /dev/null +++ b/packages/core-schemas/src/messages/index.ts @@ -0,0 +1,5 @@ +// CLI-specific message types +export * from "./cli.js" + +// Extension message types +export * from "./extension.js" diff --git a/packages/core-schemas/src/theme/index.ts b/packages/core-schemas/src/theme/index.ts new file mode 100644 index 00000000000..b280cae4532 --- /dev/null +++ b/packages/core-schemas/src/theme/index.ts @@ -0,0 +1,2 @@ +// Theme schemas and types +export * from "./theme.js" diff --git a/packages/core-schemas/src/theme/theme.ts b/packages/core-schemas/src/theme/theme.ts new file mode 100644 index 00000000000..db478261e2a --- /dev/null +++ b/packages/core-schemas/src/theme/theme.ts @@ -0,0 +1,117 @@ +import { z } from "zod" + +/** + * Theme type for categorization + */ +export const themeTypeSchema = z.enum(["dark", "light", "custom"]) + +/** + * Core theme interface defining all color categories + */ +export const themeSchema = z.object({ + /** Theme identifier */ + id: z.string(), + /** Theme display name */ + name: z.string(), + /** Theme type for categorization */ + type: themeTypeSchema, + + /** Brand identity colors */ + brand: z.object({ + primary: z.string(), + secondary: z.string(), + }), + + /** Semantic colors for common states */ + semantic: z.object({ + success: z.string(), + error: z.string(), + warning: z.string(), + info: z.string(), + neutral: z.string(), + }), + + /** Interactive element colors */ + interactive: z.object({ + prompt: z.string(), + selection: z.string(), + hover: z.string(), + disabled: z.string(), + focus: z.string(), + }), + + /** Message type colors */ + messages: z.object({ + user: z.string(), + assistant: z.string(), + system: z.string(), + error: z.string(), + }), + + /** Action colors (unified approve/reject/cancel patterns) */ + actions: z.object({ + approve: z.string(), + reject: z.string(), + cancel: z.string(), + pending: z.string(), + }), + + /** Code and diff display colors */ + code: z.object({ + addition: z.string(), + deletion: z.string(), + modification: z.string(), + context: z.string(), + lineNumber: z.string(), + }), + + /** Markdown rendering colors */ + markdown: z.object({ + text: z.string(), + heading: z.string(), + strong: z.string(), + em: z.string(), + code: z.string(), + blockquote: z.string(), + link: z.string(), + list: z.string(), + }), + + /** UI structure colors */ + ui: z.object({ + border: z.object({ + default: z.string(), + active: z.string(), + warning: z.string(), + error: z.string(), + }), + text: z.object({ + primary: z.string(), + secondary: z.string(), + dimmed: z.string(), + highlight: z.string(), + }), + background: z.object({ + default: z.string(), + elevated: z.string(), + }), + }), + + /** Status indicator colors */ + status: z.object({ + online: z.string(), + offline: z.string(), + busy: z.string(), + idle: z.string(), + }), +}) + +/** + * Theme identifier type - can be a built-in theme or custom theme ID + */ +export const themeIdSchema = z.union([z.literal("dark"), z.literal("light"), z.string()]) + +// Inferred types +export type ThemeType = z.infer +export type Theme = z.infer +export type ThemeId = z.infer diff --git a/packages/core-schemas/tsconfig.json b/packages/core-schemas/tsconfig.json new file mode 100644 index 00000000000..9893fe2966c --- /dev/null +++ b/packages/core-schemas/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@roo-code/config-typescript/base.json", + "compilerOptions": { + "types": ["vitest/globals"], + "outDir": "dist" + }, + "include": ["src", "*.config.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/core-schemas/tsup.config.ts b/packages/core-schemas/tsup.config.ts new file mode 100644 index 00000000000..38b458806ae --- /dev/null +++ b/packages/core-schemas/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + outDir: "dist", +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36c16a5f10d..16982ec8d99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -607,6 +607,9 @@ importers: '@google/genai': specifier: ^1.0.0 version: 1.3.0(@modelcontextprotocol/sdk@1.24.0(zod@3.25.76))(encoding@0.1.13) + '@kilocode/core-schemas': + specifier: workspace:^ + version: link:../packages/core-schemas '@lmstudio/sdk': specifier: ^1.1.1 version: 1.2.0 @@ -1529,6 +1532,34 @@ importers: packages/config-typescript: {} + packages/core-schemas: + dependencies: + zod: + specifier: ^3.25.61 + version: 3.25.76 + devDependencies: + '@roo-code/config-eslint': + specifier: workspace:^ + version: link:../config-eslint + '@roo-code/config-typescript': + specifier: workspace:^ + version: link:../config-typescript + '@roo-code/types': + specifier: workspace:^ + version: link:../types + '@types/node': + specifier: 20.x + version: 20.19.27 + globals: + specifier: ^16.3.0 + version: 16.3.0 + tsup: + specifier: ^8.3.5 + version: 8.5.0(@swc/core@1.15.1(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.1) + vitest: + specifier: ^3.2.3 + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.27)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + packages/evals: dependencies: '@roo-code/ipc': @@ -1694,6 +1725,9 @@ importers: '@google/genai': specifier: ^1.29.1 version: 1.29.1(@modelcontextprotocol/sdk@1.24.0(zod@3.25.61)) + '@kilocode/core-schemas': + specifier: workspace:^ + version: link:../packages/core-schemas '@lancedb/lancedb': specifier: ^0.21.2 version: 0.21.3(apache-arrow@18.1.0) @@ -20152,7 +20186,7 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.25 '@antfu/install-pkg@1.1.0': @@ -23763,7 +23797,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -23776,14 +23810,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.57)(ts-node@10.9.2(@swc/core@1.15.1(@swc/helpers@0.5.17))(@types/node@20.17.57)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@20.19.27)(ts-node@10.9.2(@swc/core@1.15.1(@swc/helpers@0.5.17))(@types/node@20.17.57)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -23812,7 +23846,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -23830,7 +23864,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -23846,7 +23880,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-regex-util: 30.0.1 '@jest/reporters@29.7.0': @@ -23856,12 +23890,12 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.17.57 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 20.19.27 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 - glob: 11.1.0 + glob: 13.0.0 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 @@ -23888,7 +23922,7 @@ snapshots: '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -23931,7 +23965,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -23941,7 +23975,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -23976,8 +24010,8 @@ snapshots: '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -23985,8 +24019,8 @@ snapshots: '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.0': {} @@ -27144,17 +27178,17 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/bonjour@3.5.13': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/responselike': 1.0.3 '@types/cardinal@2.1.1': {} @@ -27172,11 +27206,11 @@ snapshots: '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.7 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/connect@3.4.38': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/cookie@0.3.3': {} @@ -27333,7 +27367,7 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -27355,13 +27389,13 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.17.57 + '@types/node': 20.19.27 optional: true '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/glob@9.0.0': dependencies: @@ -27369,7 +27403,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/gtag.js@0.0.12': {} @@ -27402,7 +27436,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/istanbul-lib-coverage@2.0.6': {} @@ -27423,13 +27457,13 @@ snapshots: '@types/jsdom@20.0.1': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 '@types/jsdom@21.1.7': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -27441,7 +27475,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/katex@0.16.7': {} @@ -27449,7 +27483,7 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/lodash.debounce@4.0.9': dependencies: @@ -27492,16 +27526,16 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 form-data: 4.0.4 '@types/node-forge@1.3.14': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/node-ipc@9.2.3': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/node@12.20.55': {} @@ -27526,6 +27560,7 @@ snapshots: '@types/node@24.2.1': dependencies: undici-types: 7.10.0 + optional: true '@types/normalize-package-data@2.4.4': {} @@ -27541,7 +27576,7 @@ snapshots: '@types/qrcode@1.5.6': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/qs@6.14.0': {} @@ -27575,13 +27610,13 @@ snapshots: '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 24.2.1 + '@types/node': 20.19.27 '@types/resolve@1.20.6': {} '@types/responselike@1.0.3': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/retry@0.12.2': {} @@ -27589,7 +27624,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/seedrandom@3.0.8': {} @@ -27600,11 +27635,11 @@ snapshots: '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/send@1.2.1': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/serve-index@1.9.4': dependencies: @@ -27613,7 +27648,7 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/send': 0.17.6 '@types/shell-quote@1.7.5': {} @@ -27630,7 +27665,7 @@ snapshots: '@types/sockjs@0.3.36': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/stack-utils@2.0.3': {} @@ -27640,11 +27675,11 @@ snapshots: '@types/stream-chain@2.1.0': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/stream-json@1.7.8': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/stream-chain': 2.1.0 '@types/string-similarity@4.0.2': {} @@ -27681,7 +27716,7 @@ snapshots: '@types/vinyl@2.0.12': dependencies: '@types/expect': 1.20.4 - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/vscode-notebook-renderer@1.72.4': {} @@ -27693,7 +27728,7 @@ snapshots: '@types/wait-on@5.3.4': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/webpack@5.28.5(@swc/core@1.15.1(@swc/helpers@0.5.17))(esbuild@0.25.9)(webpack-cli@5.1.4)': dependencies: @@ -27716,7 +27751,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 '@types/yargs-parser@21.0.3': {} @@ -27962,6 +27997,14 @@ snapshots: optionalDependencies: vite: 6.3.6(@types/node@20.17.57)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@6.3.6(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.6(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@6.3.6(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -30060,7 +30103,7 @@ snapshots: copyfiles@2.4.1: dependencies: - glob: 11.1.0 + glob: 13.0.0 minimatch: 3.1.2 mkdirp: 1.0.4 noms: 0.0.0 @@ -31629,7 +31672,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 require-like: 0.1.2 event-emitter@0.3.5: @@ -32498,7 +32541,7 @@ snapshots: glob-stream@6.1.0: dependencies: extend: 3.0.2 - glob: 11.1.0 + glob: 13.0.0 glob-parent: 3.1.0 is-negated-glob: 1.0.0 ordered-read-streams: 1.0.1 @@ -34016,7 +34059,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.0 @@ -34083,7 +34126,7 @@ snapshots: chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 - glob: 11.1.0 + glob: 13.0.0 graceful-fs: 4.2.11 jest-circus: 29.7.0 jest-environment-node: 29.7.0 @@ -34105,6 +34148,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.19.27)(ts-node@10.9.2(@swc/core@1.15.1(@swc/helpers@0.5.17))(@types/node@20.17.57)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.27.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 13.0.0 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.27 + ts-node: 10.9.2(@swc/core@1.15.1(@swc/helpers@0.5.17))(@types/node@20.17.57)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@24.2.1): dependencies: '@babel/core': 7.27.1 @@ -34114,7 +34188,7 @@ snapshots: chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 - glob: 11.1.0 + glob: 13.0.0 graceful-fs: 4.2.11 jest-circus: 29.7.0 jest-environment-node: 29.7.0 @@ -34160,7 +34234,7 @@ snapshots: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -34174,7 +34248,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -34184,7 +34258,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.17.57 + '@types/node': 20.19.27 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -34230,7 +34304,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-util: 29.7.0 jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@24.2.1)): @@ -34299,7 +34373,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -34327,11 +34401,11 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 - glob: 11.1.0 + glob: 13.0.0 graceful-fs: 4.2.11 jest-haste-map: 29.7.0 jest-message-util: 29.7.0 @@ -34379,7 +34453,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -34409,7 +34483,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.27 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -34418,13 +34492,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.27 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -39022,7 +39096,7 @@ snapshots: rimraf@2.6.3: dependencies: - glob: 11.1.0 + glob: 13.0.0 rimraf@2.7.1: dependencies: @@ -39030,11 +39104,11 @@ snapshots: rimraf@3.0.2: dependencies: - glob: 11.1.0 + glob: 13.0.0 rimraf@5.0.10: dependencies: - glob: 11.1.0 + glob: 13.0.0 rimraf@6.0.1: dependencies: @@ -40141,9 +40215,9 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 11.1.0 + glob: 13.0.0 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 @@ -40368,7 +40442,7 @@ snapshots: test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 - glob: 11.1.0 + glob: 13.0.0 minimatch: 3.1.2 text-decoder@1.2.3: @@ -40897,7 +40971,8 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.10.0: {} + undici-types@7.10.0: + optional: true undici-types@7.16.0: {} @@ -41196,7 +41271,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 @@ -41399,6 +41474,27 @@ snapshots: - tsx - yaml + vite-node@3.2.4(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.6(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -41488,6 +41584,23 @@ snapshots: tsx: 4.19.4 yaml: 2.8.1 + vite@6.3.6(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.40.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.19.27 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.44.1 + tsx: 4.19.4 + yaml: 2.8.1 + vite@6.3.6(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: esbuild: 0.25.9 @@ -41637,6 +41750,50 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.27)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.3.6(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1(supports-color@8.1.1) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.3.6(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@20.19.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.19.27 + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 diff --git a/src/core/kilocode/agent-manager/types.ts b/src/core/kilocode/agent-manager/types.ts index 0dccd17d006..95a06358d91 100644 --- a/src/core/kilocode/agent-manager/types.ts +++ b/src/core/kilocode/agent-manager/types.ts @@ -1,76 +1,33 @@ -import type { Session as RemoteSession } from "../../../shared/kilocode/cli-sessions/core/SessionClient" - /** * Agent Manager Types + * + * Re-exports types from @kilocode/core-schemas for consistency + * and backward compatibility. */ -export type AgentStatus = "creating" | "running" | "done" | "error" | "stopped" -export type SessionSource = "local" | "remote" - -/** - * Parallel mode (worktree) information for a session - */ -export interface ParallelModeInfo { - enabled: boolean - branch?: string // e.g., "add-authentication-1702734891234" - worktreePath?: string // e.g., ".kilocode/worktrees/add-auth..." - parentBranch?: string // e.g., "main" - the branch worktree was created from - completionMessage?: string // Merge instructions from CLI on completion -} - -export interface AgentSession { - sessionId: string - label: string - prompt: string - status: AgentStatus - startTime: number - endTime?: number - exitCode?: number - error?: string - logs: string[] - pid?: number - source: SessionSource - parallelMode?: ParallelModeInfo - gitUrl?: string -} +import type { Session as RemoteSession } from "../../../shared/kilocode/cli-sessions/core/SessionClient" -/** - * Represents a session that is being created (waiting for CLI's session_created event) - */ -export interface PendingSession { - prompt: string - label: string - startTime: number - parallelMode?: boolean - gitUrl?: string -} +// Re-export all agent manager types from core-schemas +export { + // Schemas + agentStatusSchema, + sessionSourceSchema, + parallelModeInfoSchema, + agentSessionSchema, + pendingSessionSchema, + agentManagerStateSchema, + agentManagerMessageSchema, + agentManagerExtensionMessageSchema, + // Types + type AgentStatus, + type SessionSource, + type ParallelModeInfo, + type AgentSession, + type PendingSession, + type AgentManagerState, + type AgentManagerMessage, + type AgentManagerExtensionMessage, +} from "@kilocode/core-schemas" // Re-export remote session shape from shared session client for consistency export type { RemoteSession } - -export interface AgentManagerState { - sessions: AgentSession[] - selectedId: string | null -} - -/** - * Messages from Webview to Extension - */ -export type AgentManagerMessage = - | { type: "agentManager.webviewReady" } - | { type: "agentManager.startSession"; prompt: string; parallelMode?: boolean; existingBranch?: string } - | { type: "agentManager.stopSession"; sessionId: string } - | { type: "agentManager.selectSession"; sessionId: string } - | { type: "agentManager.refreshRemoteSessions" } - | { type: "agentManager.listBranches" } - -/** - * Messages from Extension to Webview - */ -export type AgentManagerExtensionMessage = - | { type: "agentManager.state"; state: AgentManagerState } - | { type: "agentManager.sessionUpdated"; session: AgentSession } - | { type: "agentManager.sessionRemoved"; sessionId: string } - | { type: "agentManager.error"; error: string } - | { type: "agentManager.remoteSessions"; sessions: RemoteSession[] } - | { type: "agentManager.branches"; branches: string[]; currentBranch?: string } diff --git a/src/package.json b/src/package.json index be31d7590f7..00da66ed228 100644 --- a/src/package.json +++ b/src/package.json @@ -675,6 +675,7 @@ "@modelcontextprotocol/sdk": "^1.24.0", "@qdrant/js-client-rest": "^1.14.0", "@roo-code/cloud": "workspace:^", + "@kilocode/core-schemas": "workspace:^", "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", From f38387182c6806e71dacfe87a1ef46e96c592b84 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 9 Jan 2026 12:04:20 +0100 Subject: [PATCH 2/8] Fix schema issues --- cli/src/config/types.ts | 8 +------- packages/core-schemas/eslint.config.mjs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 packages/core-schemas/eslint.config.mjs diff --git a/cli/src/config/types.ts b/cli/src/config/types.ts index a31a5f3fefd..849caa1f63d 100644 --- a/cli/src/config/types.ts +++ b/cli/src/config/types.ts @@ -5,13 +5,7 @@ * and backward compatibility with existing code. */ -import type { - ProviderConfig as CoreProviderConfig, - CLIConfig as CoreCLIConfig, - AutoApprovalConfig, - Theme, - ThemeId, -} from "@kilocode/core-schemas" +import type { ProviderConfig as CoreProviderConfig, CLIConfig as CoreCLIConfig } from "@kilocode/core-schemas" // ProviderConfig with index signature for dynamic property access (backward compatibility) export type ProviderConfig = CoreProviderConfig & { [key: string]: unknown } diff --git a/packages/core-schemas/eslint.config.mjs b/packages/core-schemas/eslint.config.mjs new file mode 100644 index 00000000000..c603a68f12d --- /dev/null +++ b/packages/core-schemas/eslint.config.mjs @@ -0,0 +1,20 @@ +import { config } from "@roo-code/config-eslint/base" +import globals from "globals" + +/** @type {import("eslint").Linter.Config} */ +export default [ + ...config, + { + files: ["**/*.cjs"], + languageOptions: { + globals: { + ...globals.node, + ...globals.commonjs, + }, + sourceType: "commonjs", + }, + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, +] From c9a085f37d8d4ec84b7eae534e42eb3d641c8068 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 12 Jan 2026 13:32:20 +0100 Subject: [PATCH 3/8] Add --attach flag for file attachments in CLI Implements a new --attach flag to allow users to attach image files (png, jpg, jpeg, webp, gif, tiff) to CLI prompts in auto/yolo modes. Adds validation for mode requirements, file existence, and supported formats. Updates CLI options, types, and UI to support and process attachments. --- cli/src/__tests__/attach-flag.test.ts | 294 ++++++++++++++++++++++++++ cli/src/cli.ts | 1 + cli/src/index.ts | 37 ++++ cli/src/types/cli.ts | 1 + cli/src/ui/App.tsx | 1 + cli/src/ui/UI.tsx | 45 +++- 6 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 cli/src/__tests__/attach-flag.test.ts diff --git a/cli/src/__tests__/attach-flag.test.ts b/cli/src/__tests__/attach-flag.test.ts new file mode 100644 index 00000000000..c6aea32bebc --- /dev/null +++ b/cli/src/__tests__/attach-flag.test.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import { existsSync } from "fs" +import { extname } from "node:path" + +// Mock fs.existsSync +vi.mock("fs", () => ({ + existsSync: vi.fn(), +})) + +/** + * Tests for the --attach flag behavior. + * + * The --attach flag allows users to attach files (currently images) to CLI prompts + * in auto/yolo mode or json-io mode. + */ +describe("CLI --attach flag", () => { + const supportedExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".tiff"] + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("Flag accumulation", () => { + /** + * Test the Commander.js accumulator function for --attach flag + */ + function accumulateAttachments(value: string, previous: string[]): string[] { + return previous.concat([value]) + } + + it("should accept a single --attach flag", () => { + const result = accumulateAttachments("./screenshot.png", []) + expect(result).toEqual(["./screenshot.png"]) + }) + + it("should accumulate multiple --attach flags", () => { + let attachments: string[] = [] + attachments = accumulateAttachments("./screenshot.png", attachments) + attachments = accumulateAttachments("./diagram.png", attachments) + attachments = accumulateAttachments("./photo.jpg", attachments) + + expect(attachments).toEqual(["./screenshot.png", "./diagram.png", "./photo.jpg"]) + expect(attachments.length).toBe(3) + }) + + it("should start with an empty array by default", () => { + const defaultValue: string[] = [] + expect(defaultValue).toEqual([]) + }) + }) + + describe("Mode validation", () => { + /** + * Mirrors the validation logic from cli/src/index.ts + */ + function validateAttachRequiresAutoOrJsonIo(options: { attach?: string[]; auto?: boolean; jsonIo?: boolean }): { + valid: boolean + error?: string + } { + const attachments = options.attach || [] + if (attachments.length > 0) { + if (!options.auto && !options.jsonIo) { + return { + valid: false, + error: "Error: --attach option requires --auto or --json-io flag", + } + } + } + return { valid: true } + } + + it("should reject --attach without --auto or --json-io", () => { + const result = validateAttachRequiresAutoOrJsonIo({ + attach: ["./screenshot.png"], + }) + expect(result.valid).toBe(false) + expect(result.error).toBe("Error: --attach option requires --auto or --json-io flag") + }) + + it("should accept --attach with --auto flag", () => { + const result = validateAttachRequiresAutoOrJsonIo({ + attach: ["./screenshot.png"], + auto: true, + }) + expect(result.valid).toBe(true) + }) + + it("should accept --attach with --json-io flag", () => { + const result = validateAttachRequiresAutoOrJsonIo({ + attach: ["./screenshot.png"], + jsonIo: true, + }) + expect(result.valid).toBe(true) + }) + + it("should accept --attach with both --auto and --json-io flags", () => { + const result = validateAttachRequiresAutoOrJsonIo({ + attach: ["./screenshot.png"], + auto: true, + jsonIo: true, + }) + expect(result.valid).toBe(true) + }) + + it("should accept when no attachments are provided", () => { + const result = validateAttachRequiresAutoOrJsonIo({}) + expect(result.valid).toBe(true) + }) + + it("should accept when attachments array is empty", () => { + const result = validateAttachRequiresAutoOrJsonIo({ attach: [] }) + expect(result.valid).toBe(true) + }) + }) + + describe("File existence validation", () => { + /** + * Mirrors the file existence validation from cli/src/index.ts + */ + function validateAttachmentExists(attachPath: string): { valid: boolean; error?: string } { + if (!existsSync(attachPath)) { + return { + valid: false, + error: `Error: Attachment file not found: ${attachPath}`, + } + } + return { valid: true } + } + + it("should error on non-existent attachment file", () => { + vi.mocked(existsSync).mockReturnValue(false) + + const result = validateAttachmentExists("/non/existent/path.png") + expect(result.valid).toBe(false) + expect(result.error).toBe("Error: Attachment file not found: /non/existent/path.png") + }) + + it("should accept existing attachment file", () => { + vi.mocked(existsSync).mockReturnValue(true) + + const result = validateAttachmentExists("./existing-image.png") + expect(result.valid).toBe(true) + }) + }) + + describe("File format validation", () => { + /** + * Mirrors the file format validation from cli/src/index.ts + */ + function validateAttachmentFormat(attachPath: string): { valid: boolean; error?: string } { + const ext = extname(attachPath).toLowerCase() + if (!supportedExtensions.includes(ext)) { + return { + valid: false, + error: `Error: Unsupported attachment format "${ext}". Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff. Other file types can be read using @path mentions or the read_file tool.`, + } + } + return { valid: true } + } + + it("should error on unsupported file format with helpful message", () => { + const result = validateAttachmentFormat("./document.pdf") + expect(result.valid).toBe(false) + expect(result.error).toContain('Unsupported attachment format ".pdf"') + expect(result.error).toContain("Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff") + expect(result.error).toContain("Other file types can be read using @path mentions or the read_file tool") + }) + + it("should error on text file format", () => { + const result = validateAttachmentFormat("./readme.txt") + expect(result.valid).toBe(false) + expect(result.error).toContain('Unsupported attachment format ".txt"') + }) + + it("should error on unknown extension", () => { + const result = validateAttachmentFormat("./file.xyz") + expect(result.valid).toBe(false) + expect(result.error).toContain('Unsupported attachment format ".xyz"') + }) + + it("should accept .png format", () => { + const result = validateAttachmentFormat("./image.png") + expect(result.valid).toBe(true) + }) + + it("should accept .jpg format", () => { + const result = validateAttachmentFormat("./photo.jpg") + expect(result.valid).toBe(true) + }) + + it("should accept .jpeg format", () => { + const result = validateAttachmentFormat("./photo.jpeg") + expect(result.valid).toBe(true) + }) + + it("should accept .webp format", () => { + const result = validateAttachmentFormat("./image.webp") + expect(result.valid).toBe(true) + }) + + it("should accept .gif format", () => { + const result = validateAttachmentFormat("./animation.gif") + expect(result.valid).toBe(true) + }) + + it("should accept .tiff format", () => { + const result = validateAttachmentFormat("./scan.tiff") + expect(result.valid).toBe(true) + }) + + it("should handle case-insensitive extensions", () => { + expect(validateAttachmentFormat("./image.PNG").valid).toBe(true) + expect(validateAttachmentFormat("./image.Jpg").valid).toBe(true) + expect(validateAttachmentFormat("./image.JPEG").valid).toBe(true) + expect(validateAttachmentFormat("./image.WebP").valid).toBe(true) + expect(validateAttachmentFormat("./image.GIF").valid).toBe(true) + expect(validateAttachmentFormat("./image.TIFF").valid).toBe(true) + }) + }) + + describe("Complete validation flow", () => { + /** + * Mirrors the complete validation flow from cli/src/index.ts + */ + function validateAttachments(options: { attach?: string[]; auto?: boolean; jsonIo?: boolean }): { + valid: boolean + errors: string[] + } { + const errors: string[] = [] + const attachments = options.attach || [] + + if (attachments.length === 0) { + return { valid: true, errors: [] } + } + + // Validate mode requirement + if (!options.auto && !options.jsonIo) { + errors.push("Error: --attach option requires --auto or --json-io flag") + return { valid: false, errors } + } + + // Validate each attachment + for (const attachPath of attachments) { + // Check existence + if (!existsSync(attachPath)) { + errors.push(`Error: Attachment file not found: ${attachPath}`) + continue + } + + // Check format + const ext = extname(attachPath).toLowerCase() + if (!supportedExtensions.includes(ext)) { + errors.push( + `Error: Unsupported attachment format "${ext}". Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff. Other file types can be read using @path mentions or the read_file tool.`, + ) + } + } + + return { valid: errors.length === 0, errors } + } + + it("should validate multiple attachments", () => { + vi.mocked(existsSync).mockReturnValue(true) + + const result = validateAttachments({ + attach: ["./image1.png", "./image2.jpg", "./image3.webp"], + auto: true, + }) + + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + + it("should report all validation errors", () => { + vi.mocked(existsSync) + .mockReturnValueOnce(true) // first file exists + .mockReturnValueOnce(false) // second file doesn't exist + + const result = validateAttachments({ + attach: ["./invalid.pdf", "./missing.png"], + auto: true, + }) + + expect(result.valid).toBe(false) + expect(result.errors.length).toBe(2) + expect(result.errors[0]).toContain("Unsupported attachment format") + expect(result.errors[1]).toContain("Attachment file not found") + }) + }) +}) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 06071559acd..91baa8ce56e 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -358,6 +358,7 @@ export class CLI { parallel: this.options.parallel || false, worktreeBranch: this.options.worktreeBranch || undefined, noSplash: this.options.noSplash || false, + attachments: this.options.attachments, }, onExit: () => this.dispose(), }), diff --git a/cli/src/index.ts b/cli/src/index.ts index 10c0ca258e1..7f1c48d9fc2 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -6,6 +6,7 @@ loadEnvFile() import { Command } from "commander" import { existsSync } from "fs" +import { extname } from "node:path" import { CLI } from "./cli.js" import { DEFAULT_MODES, getAllModes } from "./constants/modes/defaults.js" import { getTelemetryService } from "./services/telemetry/index.js" @@ -52,6 +53,12 @@ program .option("-f, --fork ", "Fork a session by ID") .option("--nosplash", "Disable the welcome message and update notifications", false) .option("--append-system-prompt ", "Append custom instructions to the system prompt") + .option( + "--attach ", + "Attach a file to the prompt (can be repeated). Currently supports images: png, jpg, jpeg, webp, gif, tiff", + (value: string, previous: string[]) => previous.concat([value]), + [] as string[], + ) .argument("[prompt]", "The prompt or command to execute") .action(async (prompt, options) => { // Validate that --existing-branch requires --parallel @@ -151,6 +158,35 @@ program } } + // Validate attachments if specified + const attachments: string[] = options.attach || [] + if (attachments.length > 0) { + // Validate that --attach requires --auto or --json-io flag + if (!options.auto && !options.jsonIo) { + console.error("Error: --attach option requires --auto or --json-io flag") + process.exit(1) + } + + // Validate each attachment + const supportedExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".tiff"] + for (const attachPath of attachments) { + // Check that file exists + if (!existsSync(attachPath)) { + console.error(`Error: Attachment file not found: ${attachPath}`) + process.exit(1) + } + + // Validate file extension + const ext = extname(attachPath).toLowerCase() + if (!supportedExtensions.includes(ext)) { + console.error( + `Error: Unsupported attachment format "${ext}". Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff. Other file types can be read using @path mentions or the read_file tool.`, + ) + process.exit(1) + } + } + } + // Track autonomous mode start if applicable if (options.auto && finalPrompt) { getTelemetryService().trackCIModeStarted(finalPrompt.length, options.timeout) @@ -233,6 +269,7 @@ program fork: options.fork, noSplash: options.nosplash, appendSystemPrompt: options.appendSystemPrompt, + attachments: attachments.length > 0 ? attachments : undefined, }) await cli.start() await cli.dispose() diff --git a/cli/src/types/cli.ts b/cli/src/types/cli.ts index 72c5a5710aa..bea7a0b1153 100644 --- a/cli/src/types/cli.ts +++ b/cli/src/types/cli.ts @@ -34,4 +34,5 @@ export interface CLIOptions { fork?: string noSplash?: boolean appendSystemPrompt?: string + attachments?: string[] | undefined } diff --git a/cli/src/ui/App.tsx b/cli/src/ui/App.tsx index 751b5749171..2f739011104 100644 --- a/cli/src/ui/App.tsx +++ b/cli/src/ui/App.tsx @@ -19,6 +19,7 @@ export interface AppOptions { parallel?: boolean worktreeBranch?: string | undefined noSplash?: boolean + attachments?: string[] | undefined } export interface AppProps { diff --git a/cli/src/ui/UI.tsx b/cli/src/ui/UI.tsx index 3c85d1291db..d3d300e769d 100644 --- a/cli/src/ui/UI.tsx +++ b/cli/src/ui/UI.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react" import { Box, Text } from "ink" import { useAtomValue, useSetAtom } from "jotai" import { isStreamingAtom, errorAtom, addMessageAtom, messageResetCounterAtom, yoloModeAtom } from "../state/atoms/ui.js" +import { processImagePaths } from "../media/images.js" import { setCIModeAtom } from "../state/atoms/ci.js" import { configValidationAtom } from "../state/atoms/config.js" import { taskResumedViaContinueOrSessionAtom } from "../state/atoms/extension.js" @@ -37,6 +38,7 @@ import { notificationsAtom } from "../state/atoms/notifications.js" import { workspacePathAtom } from "../state/atoms/shell.js" import { useTerminal } from "../state/hooks/useTerminal.js" import { exitRequestCounterAtom } from "../state/atoms/keyboard.js" +import { useWebviewMessage } from "../state/hooks/useWebviewMessage.js" // Initialize commands on module load initializeCommands() @@ -74,6 +76,9 @@ export const UI: React.FC = ({ options, onExit }) => { ...(options.ci !== undefined && { ciMode: options.ci }), }) + // Get sendMessage for sending initial prompt with attachments + const { sendMessage } = useWebviewMessage() + // Followup handler hook for automatic suggestion population useFollowupHandler() @@ -176,17 +181,55 @@ export const UI: React.FC = ({ options, onExit }) => { if (isCommandInput(trimmedPrompt)) { executeCommand(trimmedPrompt, onExit) } else { - sendUserMessage(trimmedPrompt) + // Check if there are CLI attachments to load + if (options.attachments && options.attachments.length > 0) { + // Async IIFE to load attachments and send message + ;(async () => { + logs.debug("Loading CLI attachments", "UI", { count: options.attachments!.length }) + const result = await processImagePaths(options.attachments!) + + // Check for any errors - if any attachment fails, abort the task + if (result.errors.length > 0) { + const errorMsg = result.errors + .map((e) => `Failed to load attachment "${e.path}": ${e.error}`) + .join("\n") + logs.error("Attachment loading failed, aborting task", "UI", { error: errorMsg }) + addMessage({ + id: `attach-err-${Date.now()}-${Math.random()}`, + type: "error", + content: errorMsg, + ts: Date.now(), + }) + // Exit in CI mode since we cannot proceed + if (options.ci) { + setTimeout(() => onExit(), 500) + } + return + } + + logs.debug("Sending prompt with attachments", "UI", { + textLength: trimmedPrompt.length, + imageCount: result.images.length, + }) + // Send message with loaded images directly using sendMessage + await sendMessage({ type: "newTask", text: trimmedPrompt, images: result.images }) + })() + } else { + sendUserMessage(trimmedPrompt) + } } } } }, [ options.prompt, + options.attachments, taskResumedViaSession, hasActiveTask, configValidation.valid, executeCommand, sendUserMessage, + sendMessage, + addMessage, onExit, ]) From f16edd3dbd75009a30b3fae1690a4c222e9995d8 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 12 Jan 2026 15:07:47 +0100 Subject: [PATCH 4/8] Address PR comments --- cli/src/__tests__/attach-flag.test.ts | 163 ++++++++++---------------- cli/src/index.ts | 28 ++--- cli/src/ui/UI.tsx | 5 +- cli/src/validation/attachments.ts | 69 +++++++++++ 4 files changed, 139 insertions(+), 126 deletions(-) create mode 100644 cli/src/validation/attachments.ts diff --git a/cli/src/__tests__/attach-flag.test.ts b/cli/src/__tests__/attach-flag.test.ts index c6aea32bebc..52845637fcb 100644 --- a/cli/src/__tests__/attach-flag.test.ts +++ b/cli/src/__tests__/attach-flag.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" import { existsSync } from "fs" -import { extname } from "node:path" +import { validateAttachmentExists, validateAttachmentFormat, validateAttachments } from "../validation/attachments.js" +import { SUPPORTED_IMAGE_EXTENSIONS } from "../media/images.js" // Mock fs.existsSync vi.mock("fs", () => ({ @@ -11,11 +12,9 @@ vi.mock("fs", () => ({ * Tests for the --attach flag behavior. * * The --attach flag allows users to attach files (currently images) to CLI prompts - * in auto/yolo mode or json-io mode. + * in auto mode. */ describe("CLI --attach flag", () => { - const supportedExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".tiff"] - beforeEach(() => { vi.clearAllMocks() }) @@ -55,82 +54,53 @@ describe("CLI --attach flag", () => { describe("Mode validation", () => { /** - * Mirrors the validation logic from cli/src/index.ts + * Validates that --attach requires --auto flag. + * This mirrors the validation logic from cli/src/index.ts */ - function validateAttachRequiresAutoOrJsonIo(options: { attach?: string[]; auto?: boolean; jsonIo?: boolean }): { + function validateAttachRequiresAuto(options: { attach?: string[]; auto?: boolean }): { valid: boolean error?: string } { const attachments = options.attach || [] if (attachments.length > 0) { - if (!options.auto && !options.jsonIo) { + if (!options.auto) { return { valid: false, - error: "Error: --attach option requires --auto or --json-io flag", + error: "Error: --attach option requires --auto flag", } } } return { valid: true } } - it("should reject --attach without --auto or --json-io", () => { - const result = validateAttachRequiresAutoOrJsonIo({ + it("should reject --attach without --auto", () => { + const result = validateAttachRequiresAuto({ attach: ["./screenshot.png"], }) expect(result.valid).toBe(false) - expect(result.error).toBe("Error: --attach option requires --auto or --json-io flag") + expect(result.error).toBe("Error: --attach option requires --auto flag") }) it("should accept --attach with --auto flag", () => { - const result = validateAttachRequiresAutoOrJsonIo({ + const result = validateAttachRequiresAuto({ attach: ["./screenshot.png"], auto: true, }) expect(result.valid).toBe(true) }) - it("should accept --attach with --json-io flag", () => { - const result = validateAttachRequiresAutoOrJsonIo({ - attach: ["./screenshot.png"], - jsonIo: true, - }) - expect(result.valid).toBe(true) - }) - - it("should accept --attach with both --auto and --json-io flags", () => { - const result = validateAttachRequiresAutoOrJsonIo({ - attach: ["./screenshot.png"], - auto: true, - jsonIo: true, - }) - expect(result.valid).toBe(true) - }) - it("should accept when no attachments are provided", () => { - const result = validateAttachRequiresAutoOrJsonIo({}) + const result = validateAttachRequiresAuto({}) expect(result.valid).toBe(true) }) it("should accept when attachments array is empty", () => { - const result = validateAttachRequiresAutoOrJsonIo({ attach: [] }) + const result = validateAttachRequiresAuto({ attach: [] }) expect(result.valid).toBe(true) }) }) describe("File existence validation", () => { - /** - * Mirrors the file existence validation from cli/src/index.ts - */ - function validateAttachmentExists(attachPath: string): { valid: boolean; error?: string } { - if (!existsSync(attachPath)) { - return { - valid: false, - error: `Error: Attachment file not found: ${attachPath}`, - } - } - return { valid: true } - } - it("should error on non-existent attachment file", () => { vi.mocked(existsSync).mockReturnValue(false) @@ -148,25 +118,11 @@ describe("CLI --attach flag", () => { }) describe("File format validation", () => { - /** - * Mirrors the file format validation from cli/src/index.ts - */ - function validateAttachmentFormat(attachPath: string): { valid: boolean; error?: string } { - const ext = extname(attachPath).toLowerCase() - if (!supportedExtensions.includes(ext)) { - return { - valid: false, - error: `Error: Unsupported attachment format "${ext}". Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff. Other file types can be read using @path mentions or the read_file tool.`, - } - } - return { valid: true } - } - it("should error on unsupported file format with helpful message", () => { const result = validateAttachmentFormat("./document.pdf") expect(result.valid).toBe(false) expect(result.error).toContain('Unsupported attachment format ".pdf"') - expect(result.error).toContain("Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff") + expect(result.error).toContain("Currently supported:") expect(result.error).toContain("Other file types can be read using @path mentions or the read_file tool") }) @@ -220,59 +176,49 @@ describe("CLI --attach flag", () => { expect(validateAttachmentFormat("./image.GIF").valid).toBe(true) expect(validateAttachmentFormat("./image.TIFF").valid).toBe(true) }) + + it("should accept all supported image extensions", () => { + for (const ext of SUPPORTED_IMAGE_EXTENSIONS) { + const result = validateAttachmentFormat(`./image${ext}`) + expect(result.valid).toBe(true) + } + }) }) describe("Complete validation flow", () => { - /** - * Mirrors the complete validation flow from cli/src/index.ts - */ - function validateAttachments(options: { attach?: string[]; auto?: boolean; jsonIo?: boolean }): { - valid: boolean - errors: string[] - } { - const errors: string[] = [] - const attachments = options.attach || [] + it("should return valid for empty attachments array", () => { + const result = validateAttachments([]) + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) - if (attachments.length === 0) { - return { valid: true, errors: [] } - } + it("should validate multiple attachments", () => { + vi.mocked(existsSync).mockReturnValue(true) - // Validate mode requirement - if (!options.auto && !options.jsonIo) { - errors.push("Error: --attach option requires --auto or --json-io flag") - return { valid: false, errors } - } + const result = validateAttachments(["./image1.png", "./image2.jpg", "./image3.webp"]) - // Validate each attachment - for (const attachPath of attachments) { - // Check existence - if (!existsSync(attachPath)) { - errors.push(`Error: Attachment file not found: ${attachPath}`) - continue - } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) - // Check format - const ext = extname(attachPath).toLowerCase() - if (!supportedExtensions.includes(ext)) { - errors.push( - `Error: Unsupported attachment format "${ext}". Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff. Other file types can be read using @path mentions or the read_file tool.`, - ) - } - } + it("should report file not found error", () => { + vi.mocked(existsSync).mockReturnValue(false) - return { valid: errors.length === 0, errors } - } + const result = validateAttachments(["./missing.png"]) - it("should validate multiple attachments", () => { + expect(result.valid).toBe(false) + expect(result.errors.length).toBe(1) + expect(result.errors[0]).toContain("Attachment file not found") + }) + + it("should report unsupported format error", () => { vi.mocked(existsSync).mockReturnValue(true) - const result = validateAttachments({ - attach: ["./image1.png", "./image2.jpg", "./image3.webp"], - auto: true, - }) + const result = validateAttachments(["./document.pdf"]) - expect(result.valid).toBe(true) - expect(result.errors).toEqual([]) + expect(result.valid).toBe(false) + expect(result.errors.length).toBe(1) + expect(result.errors[0]).toContain("Unsupported attachment format") }) it("should report all validation errors", () => { @@ -280,15 +226,24 @@ describe("CLI --attach flag", () => { .mockReturnValueOnce(true) // first file exists .mockReturnValueOnce(false) // second file doesn't exist - const result = validateAttachments({ - attach: ["./invalid.pdf", "./missing.png"], - auto: true, - }) + const result = validateAttachments(["./invalid.pdf", "./missing.png"]) expect(result.valid).toBe(false) expect(result.errors.length).toBe(2) expect(result.errors[0]).toContain("Unsupported attachment format") expect(result.errors[1]).toContain("Attachment file not found") }) + + it("should skip format validation for non-existent files", () => { + vi.mocked(existsSync).mockReturnValue(false) + + // Even though the file has a valid extension, we should get a "not found" error + const result = validateAttachments(["./missing.png"]) + + expect(result.valid).toBe(false) + expect(result.errors.length).toBe(1) + expect(result.errors[0]).toContain("Attachment file not found") + expect(result.errors[0]).not.toContain("Unsupported attachment format") + }) }) }) diff --git a/cli/src/index.ts b/cli/src/index.ts index 7f1c48d9fc2..7815812ca48 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -6,7 +6,6 @@ loadEnvFile() import { Command } from "commander" import { existsSync } from "fs" -import { extname } from "node:path" import { CLI } from "./cli.js" import { DEFAULT_MODES, getAllModes } from "./constants/modes/defaults.js" import { getTelemetryService } from "./services/telemetry/index.js" @@ -19,6 +18,7 @@ import { envConfigExists, getMissingEnvVars } from "./config/env-config.js" import { getParallelModeParams } from "./parallel/parallel.js" import { DEBUG_MODES, DEBUG_FUNCTIONS } from "./debug/index.js" import { logs } from "./services/logs.js" +import { validateAttachments } from "./validation/attachments.js" // Log CLI location for debugging (visible in VS Code "Kilo-Code" output channel) logs.info(`CLI started from: ${import.meta.url}`) @@ -161,29 +161,17 @@ program // Validate attachments if specified const attachments: string[] = options.attach || [] if (attachments.length > 0) { - // Validate that --attach requires --auto or --json-io flag - if (!options.auto && !options.jsonIo) { - console.error("Error: --attach option requires --auto or --json-io flag") + if (!options.auto) { + console.error("Error: --attach option requires --auto flag") process.exit(1) } - // Validate each attachment - const supportedExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".tiff"] - for (const attachPath of attachments) { - // Check that file exists - if (!existsSync(attachPath)) { - console.error(`Error: Attachment file not found: ${attachPath}`) - process.exit(1) - } - - // Validate file extension - const ext = extname(attachPath).toLowerCase() - if (!supportedExtensions.includes(ext)) { - console.error( - `Error: Unsupported attachment format "${ext}". Currently supported: .png, .jpg, .jpeg, .webp, .gif, .tiff. Other file types can be read using @path mentions or the read_file tool.`, - ) - process.exit(1) + const validationResult = validateAttachments(attachments) + if (!validationResult.valid) { + for (const error of validationResult.errors) { + console.error(error) } + process.exit(1) } } diff --git a/cli/src/ui/UI.tsx b/cli/src/ui/UI.tsx index d3d300e769d..b80f8add3d6 100644 --- a/cli/src/ui/UI.tsx +++ b/cli/src/ui/UI.tsx @@ -194,11 +194,12 @@ export const UI: React.FC = ({ options, onExit }) => { .map((e) => `Failed to load attachment "${e.path}": ${e.error}`) .join("\n") logs.error("Attachment loading failed, aborting task", "UI", { error: errorMsg }) + const now = Date.now() addMessage({ - id: `attach-err-${Date.now()}-${Math.random()}`, + id: `attach-err-${now}-${Math.random()}`, type: "error", content: errorMsg, - ts: Date.now(), + ts: now, }) // Exit in CI mode since we cannot proceed if (options.ci) { diff --git a/cli/src/validation/attachments.ts b/cli/src/validation/attachments.ts new file mode 100644 index 00000000000..37532a8648c --- /dev/null +++ b/cli/src/validation/attachments.ts @@ -0,0 +1,69 @@ +import { existsSync } from "fs" +import { extname } from "node:path" +import { SUPPORTED_IMAGE_EXTENSIONS, SupportedImageExtension } from "../media/images.js" + +export interface AttachmentValidationResult { + valid: boolean + error?: string +} + +export interface AttachmentsValidationResult { + valid: boolean + errors: string[] +} + +/** + * Validates that an attachment file exists + */ +export function validateAttachmentExists(attachPath: string): AttachmentValidationResult { + if (!existsSync(attachPath)) { + return { + valid: false, + error: `Error: Attachment file not found: ${attachPath}`, + } + } + return { valid: true } +} + +/** + * Validates that an attachment has a supported format + */ +export function validateAttachmentFormat(attachPath: string): AttachmentValidationResult { + const ext = extname(attachPath).toLowerCase() + if (!SUPPORTED_IMAGE_EXTENSIONS.includes(ext as SupportedImageExtension)) { + return { + valid: false, + error: `Error: Unsupported attachment format "${ext}". Currently supported: ${SUPPORTED_IMAGE_EXTENSIONS.join(", ")}. Other file types can be read using @path mentions or the read_file tool.`, + } + } + return { valid: true } +} + +/** + * Validates all attachments for existence and format + */ +export function validateAttachments(attachments: string[]): AttachmentsValidationResult { + const errors: string[] = [] + + if (attachments.length === 0) { + return { valid: true, errors: [] } + } + + // Validate each attachment + for (const attachPath of attachments) { + // Check existence + const existsResult = validateAttachmentExists(attachPath) + if (!existsResult.valid && existsResult.error) { + errors.push(existsResult.error) + continue + } + + // Check format + const formatResult = validateAttachmentFormat(attachPath) + if (!formatResult.valid && formatResult.error) { + errors.push(formatResult.error) + } + } + + return { valid: errors.length === 0, errors } +} From 423eac09bda86a0e91231abf1903377aa5c76e72 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 12 Jan 2026 13:39:02 +0100 Subject: [PATCH 5/8] Create shaggy-walls-tie.md --- .changeset/shaggy-walls-tie.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaggy-walls-tie.md diff --git a/.changeset/shaggy-walls-tie.md b/.changeset/shaggy-walls-tie.md new file mode 100644 index 00000000000..3d032de9320 --- /dev/null +++ b/.changeset/shaggy-walls-tie.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Add --attach flag for file attachments in CLI From 386bfeb23c4d0c2509a35032d360b405d17eb399 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 12 Jan 2026 15:18:05 +0100 Subject: [PATCH 6/8] Update attach-flag.test.ts --- cli/src/__tests__/attach-flag.test.ts | 45 +++------------------------ 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/cli/src/__tests__/attach-flag.test.ts b/cli/src/__tests__/attach-flag.test.ts index 52845637fcb..89758e3c53a 100644 --- a/cli/src/__tests__/attach-flag.test.ts +++ b/cli/src/__tests__/attach-flag.test.ts @@ -138,51 +138,16 @@ describe("CLI --attach flag", () => { expect(result.error).toContain('Unsupported attachment format ".xyz"') }) - it("should accept .png format", () => { - const result = validateAttachmentFormat("./image.png") + it.each(SUPPORTED_IMAGE_EXTENSIONS)("should accept %s format", (ext) => { + const result = validateAttachmentFormat(`./image${ext}`) expect(result.valid).toBe(true) }) - it("should accept .jpg format", () => { - const result = validateAttachmentFormat("./photo.jpg") + it.each(SUPPORTED_IMAGE_EXTENSIONS)("should handle case-insensitive %s extension", (ext) => { + const upperExt = ext.toUpperCase() + const result = validateAttachmentFormat(`./image${upperExt}`) expect(result.valid).toBe(true) }) - - it("should accept .jpeg format", () => { - const result = validateAttachmentFormat("./photo.jpeg") - expect(result.valid).toBe(true) - }) - - it("should accept .webp format", () => { - const result = validateAttachmentFormat("./image.webp") - expect(result.valid).toBe(true) - }) - - it("should accept .gif format", () => { - const result = validateAttachmentFormat("./animation.gif") - expect(result.valid).toBe(true) - }) - - it("should accept .tiff format", () => { - const result = validateAttachmentFormat("./scan.tiff") - expect(result.valid).toBe(true) - }) - - it("should handle case-insensitive extensions", () => { - expect(validateAttachmentFormat("./image.PNG").valid).toBe(true) - expect(validateAttachmentFormat("./image.Jpg").valid).toBe(true) - expect(validateAttachmentFormat("./image.JPEG").valid).toBe(true) - expect(validateAttachmentFormat("./image.WebP").valid).toBe(true) - expect(validateAttachmentFormat("./image.GIF").valid).toBe(true) - expect(validateAttachmentFormat("./image.TIFF").valid).toBe(true) - }) - - it("should accept all supported image extensions", () => { - for (const ext of SUPPORTED_IMAGE_EXTENSIONS) { - const result = validateAttachmentFormat(`./image${ext}`) - expect(result.valid).toBe(true) - } - }) }) describe("Complete validation flow", () => { From cab9eecebafd6347e3decc4b193ee32b611c4889 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 12 Jan 2026 15:31:37 +0100 Subject: [PATCH 7/8] Refactor --attach flag accumulation logic --- cli/src/__tests__/attach-flag.test.ts | 24 ++++++++++++++++++------ cli/src/index.ts | 4 ++-- cli/src/validation/attachments.ts | 8 ++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/cli/src/__tests__/attach-flag.test.ts b/cli/src/__tests__/attach-flag.test.ts index 89758e3c53a..c4aa19727a4 100644 --- a/cli/src/__tests__/attach-flag.test.ts +++ b/cli/src/__tests__/attach-flag.test.ts @@ -1,6 +1,11 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" import { existsSync } from "fs" -import { validateAttachmentExists, validateAttachmentFormat, validateAttachments } from "../validation/attachments.js" +import { + accumulateAttachments, + validateAttachmentExists, + validateAttachmentFormat, + validateAttachments, +} from "../validation/attachments.js" import { SUPPORTED_IMAGE_EXTENSIONS } from "../media/images.js" // Mock fs.existsSync @@ -25,12 +30,9 @@ describe("CLI --attach flag", () => { describe("Flag accumulation", () => { /** - * Test the Commander.js accumulator function for --attach flag + * Tests the real accumulateAttachments function from validation/attachments.ts + * This function is used by Commander.js to accumulate --attach flags */ - function accumulateAttachments(value: string, previous: string[]): string[] { - return previous.concat([value]) - } - it("should accept a single --attach flag", () => { const result = accumulateAttachments("./screenshot.png", []) expect(result).toEqual(["./screenshot.png"]) @@ -50,6 +52,16 @@ describe("CLI --attach flag", () => { const defaultValue: string[] = [] expect(defaultValue).toEqual([]) }) + + it("should not mutate the previous array", () => { + const previous = ["./first.png"] + const result = accumulateAttachments("./second.png", previous) + + // Result should contain both + expect(result).toEqual(["./first.png", "./second.png"]) + // Original array should be unchanged + expect(previous).toEqual(["./first.png"]) + }) }) describe("Mode validation", () => { diff --git a/cli/src/index.ts b/cli/src/index.ts index 7815812ca48..bbfa138f6e1 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -18,7 +18,7 @@ import { envConfigExists, getMissingEnvVars } from "./config/env-config.js" import { getParallelModeParams } from "./parallel/parallel.js" import { DEBUG_MODES, DEBUG_FUNCTIONS } from "./debug/index.js" import { logs } from "./services/logs.js" -import { validateAttachments } from "./validation/attachments.js" +import { validateAttachments, accumulateAttachments } from "./validation/attachments.js" // Log CLI location for debugging (visible in VS Code "Kilo-Code" output channel) logs.info(`CLI started from: ${import.meta.url}`) @@ -56,7 +56,7 @@ program .option( "--attach ", "Attach a file to the prompt (can be repeated). Currently supports images: png, jpg, jpeg, webp, gif, tiff", - (value: string, previous: string[]) => previous.concat([value]), + accumulateAttachments, [] as string[], ) .argument("[prompt]", "The prompt or command to execute") diff --git a/cli/src/validation/attachments.ts b/cli/src/validation/attachments.ts index 37532a8648c..477a8394823 100644 --- a/cli/src/validation/attachments.ts +++ b/cli/src/validation/attachments.ts @@ -7,6 +7,14 @@ export interface AttachmentValidationResult { error?: string } +/** + * Commander.js accumulator function for --attach flag. + * Allows multiple --attach flags to accumulate into an array. + */ +export function accumulateAttachments(value: string, previous: string[]): string[] { + return previous.concat([value]) +} + export interface AttachmentsValidationResult { valid: boolean errors: string[] From e152239b605cc1777a0071601f4dc3fe69a130ae Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 12 Jan 2026 15:38:38 +0100 Subject: [PATCH 8/8] Refactor --attach flag validation logic --- cli/src/__tests__/attach-flag.test.ts | 21 +-------------------- cli/src/index.ts | 13 +++++++------ cli/src/validation/attachments.ts | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/cli/src/__tests__/attach-flag.test.ts b/cli/src/__tests__/attach-flag.test.ts index c4aa19727a4..f6c8507134c 100644 --- a/cli/src/__tests__/attach-flag.test.ts +++ b/cli/src/__tests__/attach-flag.test.ts @@ -5,6 +5,7 @@ import { validateAttachmentExists, validateAttachmentFormat, validateAttachments, + validateAttachRequiresAuto, } from "../validation/attachments.js" import { SUPPORTED_IMAGE_EXTENSIONS } from "../media/images.js" @@ -65,26 +66,6 @@ describe("CLI --attach flag", () => { }) describe("Mode validation", () => { - /** - * Validates that --attach requires --auto flag. - * This mirrors the validation logic from cli/src/index.ts - */ - function validateAttachRequiresAuto(options: { attach?: string[]; auto?: boolean }): { - valid: boolean - error?: string - } { - const attachments = options.attach || [] - if (attachments.length > 0) { - if (!options.auto) { - return { - valid: false, - error: "Error: --attach option requires --auto flag", - } - } - } - return { valid: true } - } - it("should reject --attach without --auto", () => { const result = validateAttachRequiresAuto({ attach: ["./screenshot.png"], diff --git a/cli/src/index.ts b/cli/src/index.ts index bbfa138f6e1..969ab96f1c1 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -18,7 +18,7 @@ import { envConfigExists, getMissingEnvVars } from "./config/env-config.js" import { getParallelModeParams } from "./parallel/parallel.js" import { DEBUG_MODES, DEBUG_FUNCTIONS } from "./debug/index.js" import { logs } from "./services/logs.js" -import { validateAttachments, accumulateAttachments } from "./validation/attachments.js" +import { validateAttachments, validateAttachRequiresAuto, accumulateAttachments } from "./validation/attachments.js" // Log CLI location for debugging (visible in VS Code "Kilo-Code" output channel) logs.info(`CLI started from: ${import.meta.url}`) @@ -160,12 +160,13 @@ program // Validate attachments if specified const attachments: string[] = options.attach || [] - if (attachments.length > 0) { - if (!options.auto) { - console.error("Error: --attach option requires --auto flag") - process.exit(1) - } + const attachRequiresAutoResult = validateAttachRequiresAuto({ attach: attachments, auto: options.auto }) + if (!attachRequiresAutoResult.valid) { + console.error(attachRequiresAutoResult.error) + process.exit(1) + } + if (attachments.length > 0) { const validationResult = validateAttachments(attachments) if (!validationResult.valid) { for (const error of validationResult.errors) { diff --git a/cli/src/validation/attachments.ts b/cli/src/validation/attachments.ts index 477a8394823..993b93dc179 100644 --- a/cli/src/validation/attachments.ts +++ b/cli/src/validation/attachments.ts @@ -7,6 +7,23 @@ export interface AttachmentValidationResult { error?: string } +/** + * Validates that --attach requires --auto flag. + * Attachments can only be used in autonomous mode. + */ +export function validateAttachRequiresAuto(options: { attach?: string[]; auto?: boolean }): AttachmentValidationResult { + const attachments = options.attach || [] + if (attachments.length > 0) { + if (!options.auto) { + return { + valid: false, + error: "Error: --attach option requires --auto flag", + } + } + } + return { valid: true } +} + /** * Commander.js accumulator function for --attach flag. * Allows multiple --attach flags to accumulate into an array.