Files
cline/apps/examples/desktop-app/sidecar/commands.ts
T
Tomás Barreiro 0c027d2731 Centralize OAuth management to the SDK (#11260)
* Centralize OAuth management to the SDK

* Update mock

* Cleanup TUI cline-account logic

* clean save credentails

* Remove unused code

* Reduce mocks

* use normalizeStoredAccessToken
2026-06-09 23:56:26 +02:00

1199 lines
37 KiB
TypeScript

import { execFileSync, spawn } from "node:child_process";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} 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,
normalizeOAuthProvider,
ProviderSettingsManager,
readGlobalSettings,
resolveLocalClineAuthToken,
resolvePluginConfigSearchPaths,
resolveSessionBackend,
resolveAgentConfigSearchPaths as resolveSharedAgentConfigSearchPaths,
SqliteSessionStore,
saveLocalProviderSettings,
sendHubCommand,
setDisabledPlugin,
setDisabledTools,
toggleDisabledTool,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
import { broadcastEvent, resolveSidecarAskQuestion } from "./context";
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<string, unknown> | undefined,
): Partial<Omit<SaveProviderSettingsActionRequest, "action" | "providerId">> {
return args?.settings && typeof args.settings === "object"
? (args.settings as Partial<
Omit<SaveProviderSettingsActionRequest, "action" | "providerId">
>)
: {};
}
// ---------------------------------------------------------------------------
// 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 {
const path = resolveMcpSettingsPath();
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`);
}
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<unknown> {
const max = Math.max(1, Math.floor(limit));
if (ctx.sessionManager) {
return await ctx.sessionManager.list(max, { hydrate: false });
}
const byId = new Map<string, JsonRecord>();
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<string, unknown>,
): Promise<unknown> {
let useLocalScheduleService = false;
try {
if (!useLocalScheduleService) {
await ensureHubServer({
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
});
}
} catch {
useLocalScheduleService = true;
}
const clientCommand = async (
hubCommand: string,
payload?: Record<string, unknown>,
) => {
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<string, unknown>;
};
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<string, unknown>,
) {
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<JsonRecord> {
const warnings: string[] = [];
const loadUserInstructionSnapshot = async (
type: "rule" | "skill" | "workflow",
): Promise<unknown[]> => {
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<string, { name: string; path: string }>();
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<string, unknown>,
): Promise<unknown> {
// ── 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<boolean | { deleted: boolean }>;
}
).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<string>([
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);
}
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<string, string>)
: 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();
},
);
return {
provider: providerId,
accessToken: saved.auth?.accessToken ?? saved.apiKey ?? "",
};
}
// ── MCP server management ─────────────────────────────────────────
if (command === "list_mcp_servers") {
return readMcpServersResponse();
}
if (command === "set_mcp_server_disabled") {
const path = ensureMcpSettingsFile();
const parsed = JSON.parse(readFileSync(path, "utf8")) as JsonRecord;
const servers = (parsed.mcpServers as JsonRecord | undefined) ?? {};
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),
};
writeMcpServersMap(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();
const parsed = JSON.parse(readFileSync(path, "utf8")) as JsonRecord;
const servers = (parsed.mcpServers as JsonRecord | undefined) ?? {};
if (previousName && previousName !== name) {
delete servers[previousName];
}
servers[name] = next;
writeMcpServersMap(servers);
return readMcpServersResponse();
}
if (command === "delete_mcp_server") {
const path = ensureMcpSettingsFile();
const parsed = JSON.parse(readFileSync(path, "utf8")) as JsonRecord;
const servers = (parsed.mcpServers as JsonRecord | undefined) ?? {};
delete servers[String(args?.name ?? "")];
writeMcpServersMap(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 === "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}`);
}