import { execFileSync, spawn } from "node:child_process"; import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { basename, dirname, extname, join } from "node:path"; import type { ClineAccountActionRequest, ProviderCapability, ProviderClient, ProviderProtocol, SaveProviderSettingsActionRequest, } from "@cline/core"; import { addLocalProvider, ClineAccountService, createLocalHubScheduleRuntimeHandlers, createUserInstructionConfigService, discoverPluginModulePaths, ensureCustomProvidersLoaded, ensureHubServer, executeClineAccountAction, getCoreBuiltinToolCatalog, getLocalProviderModels, HubScheduleCommandService, HubScheduleService, listHookConfigFiles, listLocalProviders, listPluginTools, loginAndSaveLocalProviderOAuthCredentials, markLocalProviderEnabled, normalizeOAuthProvider, ProviderSettingsManager, readGlobalSettings, resolveLocalClineAuthToken, resolvePluginConfigSearchPaths, resolveSessionBackend, resolveAgentConfigSearchPaths as resolveSharedAgentConfigSearchPaths, SqliteSessionStore, saveLocalProviderSettings, sendHubCommand, setAutoUpdateEnabledGlobally, setDisabledPlugin, setDisabledTools, setTelemetryOptOutGlobally, toggleDisabledTool, updateMcpSettingsFileSync, } from "@cline/core"; import { getClineEnvironmentConfig } from "@cline/shared"; import { connectorChannelsPayload, startConnectorChannel, stopConnectorChannel, } from "./connectors"; import { broadcastEvent, resolveSidecarAskQuestion } from "./context"; import { installMarketplaceEntryForDesktopCommand, listMarketplaceInstalledEntries, uninstallLocalPrimitive, uninstallMarketplaceEntryForDesktopCommand, } from "./marketplace"; import { findArtifactUnderDir, readSessionManifest, resolveMcpSettingsPath, rootSessionIdFrom, sessionLogPath, sharedSessionDataDir, } from "./paths"; import { readSessionHooks } from "./session-data/artifacts"; import { normalizeSessionTitle } from "./session-data/common"; import { discoverChatSessions } from "./session-data/discovery"; import { readSessionMessages } from "./session-data/messages"; import { searchWorkspaceFiles } from "./session-data/search"; import type { ChatSessionCommandRequest, JsonRecord, SidecarContext, } from "./types"; function readProviderSettingsUpdate( args: Record | undefined, ): Partial> { return args?.settings && typeof args.settings === "object" ? (args.settings as Partial< Omit >) : {}; } // --------------------------------------------------------------------------- // MCP settings helpers // --------------------------------------------------------------------------- function readMcpServersResponse(): JsonRecord { const settingsPath = resolveMcpSettingsPath(); if (!existsSync(settingsPath)) { return { settingsPath, hasSettingsFile: false, servers: [] }; } const parsed = JSON.parse(readFileSync(settingsPath, "utf8")) as JsonRecord; const servers = parsed.mcpServers as JsonRecord | undefined; const entries = Object.entries(servers ?? {}).map(([name, body]) => { const record = body as JsonRecord; const transport = record.transport && typeof record.transport === "object" ? (record.transport as JsonRecord) : undefined; const transportType = String( transport?.type ?? record.transportType ?? record.type ?? "stdio", ).trim(); return { name, transportType, disabled: record.disabled === true, command: typeof transport?.command === "string" ? transport.command : typeof record.command === "string" ? record.command : undefined, args: Array.isArray(transport?.args) ? transport.args : Array.isArray(record.args) ? record.args : undefined, cwd: typeof transport?.cwd === "string" ? transport.cwd : typeof record.cwd === "string" ? record.cwd : undefined, env: transport?.env && typeof transport.env === "object" ? transport.env : record.env && typeof record.env === "object" ? record.env : undefined, url: typeof transport?.url === "string" ? transport.url : typeof record.url === "string" ? record.url : undefined, headers: transport?.headers && typeof transport.headers === "object" ? transport.headers : record.headers && typeof record.headers === "object" ? record.headers : undefined, metadata: record.metadata, }; }); return { settingsPath, hasSettingsFile: true, servers: entries }; } function writeMcpServersMap(servers: JsonRecord): void { updateMcpSettingsFileSync(resolveMcpSettingsPath(), (settings) => { settings.mcpServers = servers; }); } function ensureMcpSettingsFile(): string { const path = resolveMcpSettingsPath(); if (!existsSync(path)) { writeMcpServersMap({}); } return path; } function removePathIfExists( path: string, options?: { recursive?: boolean }, ): boolean { if (!path || !existsSync(path)) { return false; } rmSync(path, { force: true, recursive: options?.recursive === true, }); return true; } async function listSessionsFromSidecarManager( ctx: SidecarContext, limit: number, ): Promise { const max = Math.max(1, Math.floor(limit)); if (ctx.sessionManager) { return await ctx.sessionManager.list(max, { hydrate: false }); } const byId = new Map(); const store = new SqliteSessionStore(); const mergeSessionRecord = ( sessionId: string, record: JsonRecord, ): JsonRecord => { const persisted = store.get(sessionId) as unknown as JsonRecord | undefined; const metadata = record.metadata && typeof record.metadata === "object" ? (record.metadata as JsonRecord) : undefined; return { ...(persisted ?? {}), ...record, sessionId, provider: record.provider ?? persisted?.provider ?? "", model: record.model ?? persisted?.model ?? "", cwd: record.cwd ?? persisted?.cwd ?? "", workspaceRoot: record.workspaceRoot ?? persisted?.workspaceRoot ?? persisted?.cwd ?? "", prompt: record.prompt ?? persisted?.prompt ?? metadata?.prompt, parentSessionId: record.parentSessionId ?? persisted?.parentSessionId ?? metadata?.parentSessionId, parentAgentId: record.parentAgentId ?? persisted?.parentAgentId ?? metadata?.parentAgentId, agentId: record.agentId ?? persisted?.agentId ?? metadata?.agentId, conversationId: record.conversationId ?? persisted?.conversationId ?? metadata?.conversationId, isSubagent: record.isSubagent ?? persisted?.isSubagent ?? false, startedAt: record.startedAt ?? persisted?.startedAt ?? record.createdAt, updatedAt: record.updatedAt ?? persisted?.updatedAt, metadata: { ...((persisted?.metadata && typeof persisted.metadata === "object" ? persisted.metadata : {}) as JsonRecord), ...(metadata ?? {}), }, }; }; if (ctx.hubClient) { try { const reply = await ctx.hubClient.command("session.list", { limit: max }); const sessions = Array.isArray(reply.payload?.sessions) ? reply.payload.sessions : []; for (const item of sessions) { if (!item || typeof item !== "object") continue; const record = item as JsonRecord; const sessionId = String(record.sessionId ?? "").trim(); if (sessionId) byId.set(sessionId, mergeSessionRecord(sessionId, record)); } } catch { // Fall through to the local SQLite index. } } if (byId.size === 0) { for (const session of store.list(max)) { byId.set(session.sessionId, session as unknown as JsonRecord); } } for (const [sessionId, session] of ctx.liveSessions.entries()) { const existing = byId.get(sessionId); byId.set(sessionId, { ...(existing ?? {}), sessionId, status: session.status, provider: session.config.provider ?? existing?.provider ?? "", model: session.config.model ?? existing?.model ?? "", cwd: session.config.cwd ?? existing?.cwd ?? "", workspaceRoot: session.config.workspaceRoot ?? existing?.workspaceRoot ?? existing?.cwd ?? "", prompt: session.prompt ?? existing?.prompt, startedAt: existing?.startedAt ?? new Date(session.startedAt).toISOString(), endedAt: session.endedAt !== undefined ? new Date(session.endedAt).toISOString() : existing?.endedAt, metadata: { ...((existing?.metadata && typeof existing.metadata === "object" ? existing.metadata : {}) as JsonRecord), ...(session.title ? { title: session.title } : {}), }, }); } return Array.from(byId.values()) .sort((left, right) => { const leftTime = Date.parse( String(left.updatedAt ?? left.startedAt ?? ""), ); const rightTime = Date.parse( String(right.updatedAt ?? right.startedAt ?? ""), ); return ( (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime) ); }) .slice(0, max); } // --------------------------------------------------------------------------- // Git helpers // --------------------------------------------------------------------------- function listGitBranches( ctx: SidecarContext, cwd?: string, ): { current?: string; branches?: string[] } { const targetCwd = cwd?.trim() || ctx.workspaceRoot; const current = (() => { try { return execFileSync("git", ["branch", "--show-current"], { cwd: targetCwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); } catch { return ""; } })(); try { const stdout = execFileSync( "git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], { cwd: targetCwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }, ); const branches = stdout .split("\n") .map((v) => v.trim()) .filter(Boolean); return { current: current || undefined, branches }; } catch { return { current: current || undefined, branches: [] }; } } // --------------------------------------------------------------------------- // Routine schedule helpers (in-process via shared hub server) // --------------------------------------------------------------------------- function toPositiveInt(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isFinite(value)) return undefined; const rounded = Math.trunc(value); return rounded > 0 ? rounded : undefined; } function asTrimmedString(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } async function handleRoutineScheduleCommand( command: string, args?: Record, ): Promise { let useLocalScheduleService = false; try { if (!useLocalScheduleService) { await ensureHubServer({ runtimeHandlers: createLocalHubScheduleRuntimeHandlers(), }); } } catch { useLocalScheduleService = true; } const clientCommand = async ( hubCommand: string, payload?: Record, ) => { const reply = useLocalScheduleService ? await localRoutineScheduleCommand(hubCommand, payload) : await sendHubCommand( {}, { clientId: "code-sidecar-routines", command: hubCommand as never, payload, }, ); if (!reply.ok) { throw new Error( reply.error?.message ?? `hub command failed: ${hubCommand}`, ); } return (reply.payload ?? {}) as Record; }; try { if (command === "list_routine_schedules") { const [schedules, activeExecutions, upcomingRuns] = await Promise.all([ clientCommand("schedule.list", { limit: toPositiveInt(args?.limit) ?? 200, }), clientCommand("schedule.active"), clientCommand("schedule.upcoming", { limit: 30 }), ]); return { schedules: schedules.schedules ?? [], activeExecutions: activeExecutions.executions ?? [], upcomingRuns: upcomingRuns.runs ?? [], }; } if (command === "create_routine_schedule") { const name = asTrimmedString(args?.name); const cronPattern = asTrimmedString(args?.cron_pattern); const prompt = asTrimmedString(args?.prompt); const workspaceRoot = asTrimmedString(args?.workspace_root); if (!name || !cronPattern || !prompt || !workspaceRoot) { throw new Error( "createSchedule requires name, cron_pattern, prompt, and workspace_root", ); } const created = await clientCommand("schedule.create", { name, cronPattern, prompt, modelSelection: { providerId: asTrimmedString(args?.provider) ?? "cline", modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex", }, mode: args?.mode === "plan" ? "plan" : "act", workspaceRoot, cwd: asTrimmedString(args?.cwd), systemPrompt: asTrimmedString(args?.system_prompt), maxIterations: toPositiveInt(args?.max_iterations), timeoutSeconds: toPositiveInt(args?.timeout_seconds), maxParallel: toPositiveInt(args?.max_parallel) ?? 1, enabled: args?.enabled !== false, tags: Array.isArray(args?.tags) && args.tags.length > 0 ? (args.tags as string[]) .map((v: string) => v.trim()) .filter((v: string) => v.length > 0) : undefined, }); return { schedule: created.schedule ?? null }; } const scheduleId = asTrimmedString(args?.schedule_id); if (!scheduleId) throw new Error(`${command} requires schedule_id`); if (command === "pause_routine_schedule") { const reply = await clientCommand("schedule.disable", { scheduleId }); return { schedule: reply.schedule ?? null }; } if (command === "resume_routine_schedule") { const reply = await clientCommand("schedule.enable", { scheduleId }); return { schedule: reply.schedule ?? null }; } if (command === "trigger_routine_schedule") { const reply = await clientCommand("schedule.trigger", { scheduleId }); return { execution: reply.execution ?? null }; } if (command === "delete_routine_schedule") { const reply = await clientCommand("schedule.delete", { scheduleId }); return { deleted: reply.deleted === true }; } throw new Error(`unsupported routine schedule command: ${command}`); } finally { } } let localRoutineScheduleService: HubScheduleService | undefined; let localRoutineScheduleCommands: HubScheduleCommandService | undefined; function getLocalRoutineScheduleCommands(): HubScheduleCommandService { if (!localRoutineScheduleService || !localRoutineScheduleCommands) { localRoutineScheduleService = new HubScheduleService({ runtimeHandlers: createLocalHubScheduleRuntimeHandlers(), }); localRoutineScheduleCommands = new HubScheduleCommandService( localRoutineScheduleService, ); } return localRoutineScheduleCommands; } async function localRoutineScheduleCommand( command: string, payload?: Record, ) { return await getLocalRoutineScheduleCommands().handleCommand({ version: "v1", clientId: "code-sidecar-routines-local", command: command as never, payload, }); } // --------------------------------------------------------------------------- // User instruction config listing through the core config service. // --------------------------------------------------------------------------- function resolveAgentConfigSearchPaths(workspaceRoot?: string): string[] { return resolveSharedAgentConfigSearchPaths(workspaceRoot); } async function listUserInstructionConfigs( workspaceRoot: string, ): Promise { const warnings: string[] = []; const loadUserInstructionSnapshot = async ( type: "rule" | "skill" | "workflow", ): Promise => { const items: unknown[] = []; const service = createUserInstructionConfigService({ skills: { workspacePath: workspaceRoot }, rules: { workspacePath: workspaceRoot }, workflows: { workspacePath: workspaceRoot }, }); try { await service.start(); for (const record of service.listRecords(type)) { const item = record.item as unknown as JsonRecord; if (item.disabled === true) continue; items.push({ id: record.id, name: item.name ?? record.id, instructions: item.instructions, path: record.filePath, }); } } catch (error) { const message = error instanceof Error ? error.message : String(error); warnings.push(`${type}: ${message}`); } finally { service.stop(); } return items; }; const loadAgents = (): unknown[] => { const agentsById = new Map(); const directories = resolveAgentConfigSearchPaths(workspaceRoot).filter( (d) => existsSync(d), ); for (const directory of directories) { try { for (const entry of readdirSync(directory, { withFileTypes: true })) { if (!entry.isFile()) continue; const ext = extname(entry.name).toLowerCase(); if (ext !== ".yml" && ext !== ".yaml") continue; const filePath = join(directory, entry.name); const raw = readFileSync(filePath, "utf8"); const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); const fm = fmMatch?.[1] ?? ""; const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m); const parsedName = nameMatch?.[1]?.replace(/^["']|["']$/g, "").trim(); const name = parsedName && parsedName.length > 0 ? parsedName : basename(entry.name, ext); const id = name.toLowerCase(); if (!agentsById.has(id)) { agentsById.set(id, { name, path: filePath }); } } } catch { // best-effort } } return [...agentsById.values()].sort((a, b) => a.name.localeCompare(b.name), ); }; const loadHooks = (): unknown[] => { try { return listHookConfigFiles(workspaceRoot); } catch (error) { const message = error instanceof Error ? error.message : String(error); warnings.push(`hooks: ${message}`); return []; } }; const loadPlugins = (): Array<{ name: string; path: string; enabled: boolean; }> => { const disabledPlugins = new Set(readGlobalSettings().disabledPlugins ?? []); const pluginsByPath = new Map< string, { name: string; path: string; enabled: boolean } >(); const directories = resolvePluginConfigSearchPaths(workspaceRoot).filter( (d) => existsSync(d), ); for (const directory of directories) { try { for (const filePath of discoverPluginModulePaths(directory)) { if (pluginsByPath.has(filePath)) { continue; } pluginsByPath.set(filePath, { name: basename(filePath, extname(filePath)), path: filePath, enabled: !disabledPlugins.has(filePath), }); } } catch { // best-effort } } return [...pluginsByPath.values()].sort((a, b) => a.name.localeCompare(b.name), ); }; const [rules, workflows, skills, pluginTools] = await Promise.all([ loadUserInstructionSnapshot("rule"), loadUserInstructionSnapshot("workflow"), loadUserInstructionSnapshot("skill"), listPluginTools({ workspacePath: workspaceRoot, cwd: workspaceRoot, }), ]); const disabledTools = new Set(readGlobalSettings().disabledTools ?? []); const builtinToolCatalog = getCoreBuiltinToolCatalog({ disabledToolIds: disabledTools, }); return { workspaceRoot, rules, workflows, skills, agents: loadAgents(), plugins: loadPlugins(), tools: [ ...builtinToolCatalog.map((tool) => ({ id: tool.id, name: tool.id, description: tool.description, enabled: tool.defaultEnabled && !tool.headlessToolNames.some((name) => disabledTools.has(name)), source: "builtin", headlessToolNames: tool.headlessToolNames, })), ...pluginTools.map((tool) => ({ id: `${tool.pluginName}:${tool.name}:${tool.path}`, name: tool.name, description: tool.description, enabled: tool.enabled, source: tool.source, path: tool.path, pluginName: tool.pluginName, })), ], hooks: loadHooks(), mcp: readMcpServersResponse(), warnings, }; } // --------------------------------------------------------------------------- // Native OS commands // --------------------------------------------------------------------------- function pickWorkspaceDirectory(): string | null { const platform = process.platform; if (platform === "darwin") { try { const result = execFileSync( "osascript", [ "-e", 'set theFolder to choose folder with prompt "Select workspace directory"', "-e", "return POSIX path of theFolder", ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ).trim(); return result || null; } catch { return null; } } // Linux — try zenity try { const result = execFileSync( "zenity", ["--file-selection", "--directory", "--title=Select workspace directory"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ).trim(); return result || null; } catch { return null; } } function openFileInEditor(filePath: string): void { const platform = process.platform; const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open"; const cmdArgs = platform === "win32" ? ["/c", "start", "", filePath] : [filePath]; const child = spawn(cmd, cmdArgs, { stdio: "ignore", detached: true }); child.unref(); } // --------------------------------------------------------------------------- // Main command router // --------------------------------------------------------------------------- export async function handleCommand( ctx: SidecarContext, command: string, args?: Record, ): Promise { // ── Chat session commands ────────────────────────────────────────── if (command === "chat_session_command") { const { handleChatSessionCommand } = await import("./chat-session"); return await handleChatSessionCommand( ctx, (args?.request as ChatSessionCommandRequest | undefined) ?? (args as ChatSessionCommandRequest), ); } // ── Session data reading ────────────────────────────────────────── if (command === "read_session_messages") { return await readSessionMessages( ctx, String(args?.sessionId ?? ""), typeof args?.maxMessages === "number" ? args.maxMessages : 800, ); } if (command === "read_session_hooks") { return await readSessionHooks( String(args?.sessionId ?? ""), typeof args?.limit === "number" ? args.limit : 300, ); } // ── Process context ─────────────────────────────────────────────── if (command === "get_process_context") { return { workspaceRoot: ctx.workspaceRoot, cwd: ctx.workspaceRoot }; } if (command === "get_chat_ws_endpoint") { return ""; } // ── Tool approvals (in-memory) ──────────────────────────────────── if (command === "poll_tool_approvals") { const sessionId = String(args?.sessionId ?? "").trim(); return Array.from(ctx.pendingApprovals.values()) .filter((a) => a.item.sessionId === sessionId) .map((a) => a.item); } if (command === "respond_tool_approval") { const sessionId = String(args?.sessionId ?? "").trim(); const requestId = String(args?.requestId ?? "").trim(); if (!sessionId || !requestId) { throw new Error("sessionId and requestId are required"); } const pending = ctx.pendingApprovals.get(requestId); if (pending) { pending.resolve({ approved: Boolean(args?.approved), ...(typeof args?.reason === "string" && args.reason.trim().length > 0 ? { reason: args.reason.trim() } : {}), }); } ctx.pendingApprovals.delete(requestId); const remaining = Array.from(ctx.pendingApprovals.values()) .filter((a) => a.item.sessionId === sessionId) .map((a) => a.item); broadcastEvent(ctx, "tool_approval_state", { sessionId, items: remaining, }); return true; } if (command === "respond_ask_question") { const requestId = String(args?.requestId ?? "").trim(); if (!requestId) { throw new Error("requestId is required"); } const answer = String(args?.answer ?? "").trim(); const resolved = resolveSidecarAskQuestion(ctx, requestId, answer); if (!resolved) { throw new Error(`unknown ask question request: ${requestId}`); } broadcastEvent(ctx, "ask_question_answered", { requestId }); return true; } // ── Session discovery ───────────────────────────────────────────── if (command === "list_chat_sessions") { return discoverChatSessions( ctx, typeof args?.limit === "number" ? args.limit : 300, ); } if (command === "list_cli_sessions") { return await listSessionsFromSidecarManager( ctx, typeof args?.limit === "number" ? args.limit : 300, ); } if (command === "list_discovered_sessions") { return await listSessionsFromSidecarManager( ctx, typeof args?.limit === "number" ? args.limit : 300, ); } if (command === "update_chat_session_title") { const sessionId = String(args?.sessionId ?? "").trim(); if (!sessionId) throw new Error("session id is required"); const title = normalizeSessionTitle(String(args?.title ?? "")); const backend = await resolveSessionBackend({ backendMode: "local" }); const result = await backend.updateSession({ sessionId, title }); if (!result.updated) throw new Error(`Session ${sessionId} not found`); const liveSession = ctx.liveSessions.get(sessionId); if (liveSession) liveSession.title = title; return true; } if (command === "delete_chat_session" || command === "delete_cli_session") { const sessionId = String(args?.sessionId ?? args?.session_id ?? "").trim(); if (!sessionId) throw new Error("session id is required"); console.error( `[sidecar:delete] request command=${command} sessionId=${sessionId}`, ); const store = new SqliteSessionStore(); const row = store.get(sessionId); const manifest = readSessionManifest(sessionId); let deleted = false; let deleteError: Error | null = null; try { if (ctx.sessionManager) { deleted = await ctx.sessionManager.delete(sessionId); } else { const backend = await resolveSessionBackend({ backendMode: "local" }); const deleteSession = ( backend as { deleteSession: ( sessionId: string, cascade?: boolean, ) => Promise; } ).deleteSession.bind(backend); const deleteResult = await deleteSession(sessionId, true); deleted = typeof deleteResult === "boolean" ? deleteResult : deleteResult.deleted; } } catch (error) { deleteError = error instanceof Error ? error : new Error(String(error)); } if (store.delete(sessionId, true)) { deleted = true; } ctx.liveSessions.delete(sessionId); const directoryCandidates = new Set([ join(sharedSessionDataDir(), sessionId), ]); for (const path of [ row?.messagesPath, typeof manifest?.messages_path === "string" ? manifest.messages_path : null, ]) { if (typeof path === "string" && path.trim().length > 0) { directoryCandidates.add(dirname(path)); } } for (const path of [sessionLogPath(sessionId)]) { if (removePathIfExists(path, { recursive: true })) { deleted = true; } } for (const dir of directoryCandidates) { if (removePathIfExists(dir, { recursive: true })) { deleted = true; } } for (const path of [ row?.messagesPath, typeof manifest?.messages_path === "string" ? manifest.messages_path : null, join(sharedSessionDataDir(), sessionId, `${sessionId}.json`), ].filter((v): v is string => typeof v === "string" && v.length > 0)) { if (removePathIfExists(path)) { deleted = true; } } for (const suffix of ["messages.json"]) { const fileName = `${sessionId}.${suffix}`; const found = findArtifactUnderDir( join(sharedSessionDataDir(), rootSessionIdFrom(sessionId)), fileName, 4, ); if (found && removePathIfExists(found)) { deleted = true; } } if (!deleted && deleteError) { console.error( `[sidecar:delete] failed sessionId=${sessionId} error=${deleteError.message}`, ); throw deleteError; } console.error( `[sidecar:delete] result sessionId=${sessionId} deleted=${deleted}`, ); if (deleted) { broadcastEvent(ctx, "session_deleted", { sessionId, command, deleted: true, }); } return deleted; } // ── Workspace file search ───────────────────────────────────────── if (command === "search_workspace_files") { return await searchWorkspaceFiles(ctx, args); } // ── Cline account ────────────────────────────────────────────────── if (command === "cline_account") { const operation = String(args?.operation ?? "").trim(); if (!operation) throw new Error("operation is required"); const manager = new ProviderSettingsManager(); const settings = manager.getProviderSettings("cline"); const accountService = new ClineAccountService({ apiBaseUrl: settings?.baseUrl?.trim() || getClineEnvironmentConfig().apiBaseUrl, getAuthToken: async () => resolveLocalClineAuthToken(settings), }); return await executeClineAccountAction( args as ClineAccountActionRequest, accountService, ); } // ── Provider management ──────────────────────────────────────────── if (command === "list_provider_catalog") { const manager = new ProviderSettingsManager(); await ensureCustomProvidersLoaded(manager); return await listLocalProviders(manager, { isClinePassEnabled: true }); } if (command === "list_provider_models") { const manager = new ProviderSettingsManager(); return await getLocalProviderModels( String(args?.provider ?? ""), manager.getProviderConfig(String(args?.provider ?? "").trim()), ); } if (command === "save_provider_settings") { const manager = new ProviderSettingsManager(); return saveLocalProviderSettings(manager, { ...readProviderSettingsUpdate(args), providerId: String(args?.provider ?? ""), enabled: typeof args?.enabled === "boolean" ? args.enabled : undefined, apiKey: typeof args?.api_key === "string" ? args.api_key : undefined, baseUrl: typeof args?.base_url === "string" ? args.base_url : undefined, }); } if (command === "add_provider") { const manager = new ProviderSettingsManager(); await ensureCustomProvidersLoaded(manager); return await addLocalProvider(manager, { providerId: String(args?.provider_id ?? ""), name: String(args?.name ?? ""), baseUrl: String(args?.base_url ?? ""), apiKey: typeof args?.api_key === "string" ? args.api_key : undefined, headers: args?.headers && typeof args.headers === "object" ? (args.headers as Record) : undefined, timeoutMs: typeof args?.timeout_ms === "number" ? args.timeout_ms : undefined, models: Array.isArray(args?.models) ? (args.models as string[]) : undefined, defaultModelId: typeof args?.default_model_id === "string" ? args.default_model_id : undefined, modelsSourceUrl: typeof args?.models_source_url === "string" ? args.models_source_url : undefined, protocol: typeof args?.protocol === "string" ? (args.protocol as ProviderProtocol) : undefined, client: typeof args?.client === "string" ? (args.client as ProviderClient) : undefined, capabilities: Array.isArray(args?.capabilities) ? (args.capabilities as ProviderCapability[]) : undefined, }); } if (command === "run_provider_oauth_login") { const providerId = normalizeOAuthProvider(String(args?.provider ?? "")); const manager = new ProviderSettingsManager(); const saved = await loginAndSaveLocalProviderOAuthCredentials( manager, providerId, (url) => { const platform = process.platform; const spawned = platform === "darwin" ? spawn("open", [url], { stdio: "ignore", detached: true }) : platform === "win32" ? spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, }) : spawn("xdg-open", [url], { stdio: "ignore", detached: true, }); spawned.unref(); }, ); if (saved.provider !== providerId) { markLocalProviderEnabled(manager, providerId, { tokenSource: "oauth" }); } return { provider: providerId, accessToken: saved.auth?.accessToken ?? saved.apiKey ?? "", }; } // ── Global settings ──────────────────────────────────────────────── if (command === "get_global_settings") { return readGlobalSettings(); } if (command === "set_telemetry_opt_out") { if (typeof args?.telemetry_opt_out !== "boolean") { throw new Error("telemetry_opt_out must be a boolean"); } setTelemetryOptOutGlobally(args.telemetry_opt_out); return readGlobalSettings(); } if (command === "set_auto_update_enabled") { if (typeof args?.auto_update_enabled !== "boolean") { throw new Error("auto_update_enabled must be a boolean"); } setAutoUpdateEnabledGlobally(args.auto_update_enabled); return readGlobalSettings(); } // ── Connector channels ───────────────────────────────────────────── if (command === "list_connector_channels") { return connectorChannelsPayload(); } if (command === "start_connector_channel") { return await startConnectorChannel(ctx.workspaceRoot, args); } if (command === "stop_connector_channel") { return await stopConnectorChannel(ctx.workspaceRoot, args); } // ── MCP server management ───────────────────────────────────────── if (command === "list_mcp_servers") { return readMcpServersResponse(); } if (command === "set_mcp_server_disabled") { const path = ensureMcpSettingsFile(); updateMcpSettingsFileSync(path, (settings) => { const servers = ((settings.mcpServers as JsonRecord | undefined) ?? {}) as JsonRecord; const name = String(args?.name ?? "").trim(); const current = servers[name]; if (!current || typeof current !== "object") { throw new Error(`unknown MCP server: ${name}`); } servers[name] = { ...(current as JsonRecord), disabled: Boolean(args?.disabled), }; settings.mcpServers = servers; }); return readMcpServersResponse(); } if (command === "upsert_mcp_server") { const input = args?.input && typeof args.input === "object" ? (args.input as JsonRecord) : (args as JsonRecord); const name = String(input.name ?? "").trim(); if (!name) throw new Error("server name is required"); const previousName = String( input.previousName ?? input.previous_name ?? "", ).trim(); const transportType = String( input.transportType ?? input.transport_type ?? "", ).trim(); const next: JsonRecord = transportType === "stdio" ? { transport: { type: "stdio", command: input.command, args: input.args, cwd: input.cwd, env: input.env, }, disabled: Boolean(input.disabled), metadata: input.metadata, } : { transport: { type: transportType, url: input.url, headers: input.headers, }, disabled: Boolean(input.disabled), metadata: input.metadata, }; const path = ensureMcpSettingsFile(); updateMcpSettingsFileSync(path, (settings) => { const servers = ((settings.mcpServers as JsonRecord | undefined) ?? {}) as JsonRecord; if (previousName && previousName !== name) { delete servers[previousName]; } servers[name] = next; settings.mcpServers = servers; }); return readMcpServersResponse(); } if (command === "delete_mcp_server") { const path = ensureMcpSettingsFile(); updateMcpSettingsFileSync(path, (settings) => { const servers = ((settings.mcpServers as JsonRecord | undefined) ?? {}) as JsonRecord; delete servers[String(args?.name ?? "")]; settings.mcpServers = servers; }); return readMcpServersResponse(); } if (command === "ensure_mcp_settings_file") { return ensureMcpSettingsFile(); } // ── Git operations ───────────────────────────────────────────────── if (command === "get_git_branch") { const branches = listGitBranches( ctx, typeof args?.cwd === "string" ? args.cwd : undefined, ); return { branch: branches.current }; } if (command === "list_git_branches") { return listGitBranches( ctx, typeof args?.cwd === "string" ? args.cwd : undefined, ); } if (command === "checkout_git_branch") { const cwd = typeof args?.cwd === "string" ? args.cwd : undefined; const branch = String(args?.branch ?? "").trim(); if (!branch) throw new Error("branch is required"); execFileSync("git", ["checkout", branch], { cwd: cwd?.trim() || ctx.workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); return { branch }; } // ── Routine schedules ───────────────────────────────────────────── if ( command === "list_routine_schedules" || command === "create_routine_schedule" || command === "pause_routine_schedule" || command === "resume_routine_schedule" || command === "trigger_routine_schedule" || command === "delete_routine_schedule" ) { return await handleRoutineScheduleCommand(command, args); } // ── User instruction configs ────────────────────────────────────── if (command === "list_user_instruction_configs") { return await listUserInstructionConfigs(ctx.workspaceRoot); } if (command === "list_marketplace_installed_entries") { return listMarketplaceInstalledEntries( args, await listUserInstructionConfigs(ctx.workspaceRoot), ); } if (command === "install_marketplace_entry") { const result = await installMarketplaceEntryForDesktopCommand(args); return result; } if (command === "uninstall_marketplace_entry") { const result = await uninstallMarketplaceEntryForDesktopCommand(args); return result; } if (command === "uninstall_local_primitive") { const result = await uninstallLocalPrimitive(args, { workspaceRoot: ctx.workspaceRoot, }); return result; } if (command === "toggle_disabled_plugin_tool") { const toolName = String(args?.name ?? "").trim(); if (!toolName) { throw new Error("tool name is required"); } toggleDisabledTool(toolName); return await listUserInstructionConfigs(ctx.workspaceRoot); } if (command === "set_tool_disabled") { const rawNames = Array.isArray(args?.names) ? args.names : [args?.name]; const toolNames = rawNames .map((name) => String(name ?? "").trim()) .filter(Boolean); if (toolNames.length === 0) { throw new Error("tool name is required"); } setDisabledTools(toolNames, args?.disabled === true); return await listUserInstructionConfigs(ctx.workspaceRoot); } if (command === "set_plugin_disabled") { const pluginPath = String(args?.path ?? "").trim(); if (!pluginPath) { throw new Error("plugin path is required"); } setDisabledPlugin(pluginPath, args?.disabled === true); return await listUserInstructionConfigs(ctx.workspaceRoot); } // ── Native OS commands ──────────────────────────────────────────── if (command === "pick_workspace_directory") { return pickWorkspaceDirectory(); } if (command === "open_mcp_settings_file") { const path = ensureMcpSettingsFile(); openFileInEditor(path); return path; } throw new Error(`unsupported desktop command: ${command}`); }