From c1a43482e71f4104c858b58216d9eb7ea9998770 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Feb 2026 22:30:54 -0800 Subject: [PATCH] sdk lib (#9259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sdk lib * improve cline sdk api surface - better api design and messages * fix some types, fix session id retrieval, improve wording * hide controller from sdk surface completely --------- Co-authored-by: Max Paulus 🄪 --- cli/esbuild.mts | 51 +- cli/package.json | 14 +- cli/src/acp/ACPHostBridgeClientProvider.ts | 8 +- cli/src/acp/AcpAgent.ts | 26 +- cli/src/acp/index.ts | 5 - cli/src/agent/ClineAgent.ts | 77 ++- cli/src/agent/ClineSessionEmitter.ts | 2 +- cli/src/agent/messageTranslator.test.ts | 8 +- cli/src/agent/messageTranslator.ts | 4 +- cli/src/agent/public-types.ts | 254 ++++++++ cli/src/agent/types.ts | 219 +------ cli/src/exports.ts | 71 +++ cli/src/index.ts | 71 +-- cli/src/lib-import.test.ts | 9 + cli/src/utils/auth.ts | 64 ++ cli/src/utils/console.ts | 16 +- cli/tsconfig.lib.json | 20 + docs/cline-sdk/overview.md | 643 +++++++++++++++++++++ 18 files changed, 1212 insertions(+), 350 deletions(-) create mode 100644 cli/src/agent/public-types.ts create mode 100644 cli/src/exports.ts create mode 100644 cli/src/lib-import.test.ts create mode 100644 cli/src/utils/auth.ts create mode 100644 cli/tsconfig.lib.json create mode 100644 docs/cline-sdk/overview.md diff --git a/cli/esbuild.mts b/cli/esbuild.mts index 7de569d618..1f81020c05 100644 --- a/cli/esbuild.mts +++ b/cli/esbuild.mts @@ -208,8 +208,8 @@ if (production) { buildEnvVars["process.env.IS_DEV"] = "false" } -const config: esbuild.BuildOptions = { - entryPoints: [path.join(__dirname, "src", "index.ts")], +// Shared build options +const sharedOptions: Partial = { bundle: true, minify: production, sourcemap: !production, @@ -221,7 +221,6 @@ const config: esbuild.BuildOptions = { sourcesContent: false, platform: "node", target: "node20", - outfile: path.join(__dirname, "dist", "cli.mjs"), // These modules need to load files from the module directory at runtime external: [ "@grpc/reflection", @@ -237,6 +236,13 @@ const config: esbuild.BuildOptions = { "@vscode/ripgrep", // Uses __dirname to locate the binary ], supported: { "top-level-await": true }, +} + +// CLI executable configuration +const cliConfig: esbuild.BuildOptions = { + ...sharedOptions, + entryPoints: [path.join(__dirname, "src", "index.ts")], + outfile: path.join(__dirname, "dist", "cli.mjs"), banner: { js: `#!/usr/bin/env node // Suppress all Node.js warnings (deprecation, experimental, etc.) @@ -250,19 +256,44 @@ const __dirname = _dirname(__filename);`, }, } +// Library configuration for programmatic use +const libConfig: esbuild.BuildOptions = { + ...sharedOptions, + entryPoints: [path.join(__dirname, "src", "exports.ts")], + outfile: path.join(__dirname, "dist", "lib.mjs"), + banner: { + js: `// Cline Library - Programmatic API +import { createRequire as _createRequire } from 'module'; +import { fileURLToPath as _fileURLToPath } from 'url'; +import { dirname as _dirname } from 'path'; +const require = _createRequire(import.meta.url); +const __filename = _fileURLToPath(import.meta.url); +const __dirname = _dirname(__filename);`, + }, +} + async function main() { - const ctx = await esbuild.context(config) if (watch) { + // In watch mode, only watch the CLI (primary use case for development) + const ctx = await esbuild.context(cliConfig) await ctx.watch() console.log("[cli] Watching for changes...") } else { - await ctx.rebuild() - await ctx.dispose() + // Build both CLI and library + console.log("[cli esbuild] Building CLI executable...") + const cliCtx = await esbuild.context(cliConfig) + await cliCtx.rebuild() + await cliCtx.dispose() - // Make the output executable - const outfile = path.join(__dirname, "dist", "cli.mjs") - if (fs.existsSync(outfile)) { - fs.chmodSync(outfile, "755") + console.log("[cli esbuild] Building library bundle...") + const libCtx = await esbuild.context(libConfig) + await libCtx.rebuild() + await libCtx.dispose() + + // Make the CLI output executable + const cliOutfile = path.join(__dirname, "dist", "cli.mjs") + if (fs.existsSync(cliOutfile)) { + fs.chmodSync(cliOutfile, "755") } } } diff --git a/cli/package.json b/cli/package.json index d1eced8e13..666b85a490 100644 --- a/cli/package.json +++ b/cli/package.json @@ -2,10 +2,17 @@ "name": "cline", "version": "2.4.3", "description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more", - "main": "dist/cli.mjs", + "main": "dist/lib.mjs", + "types": "dist/lib.d.ts", "bin": { "cline": "./dist/cli.mjs" }, + "exports": { + ".": { + "import": "./dist/lib.mjs", + "types": "./dist/lib.d.ts" + } + }, "os": [ "darwin", "linux", @@ -23,8 +30,9 @@ "scripts": { "package:brew": "npx tsx ./scripts/update-brew-formula.mts", "package": "npm pack --pack-destination ./dist", - "build": "npm run typecheck && npx tsx esbuild.mts", - "build:production": "npm run typecheck && npx tsx esbuild.mts --production", + "build": "npm run typecheck && npx tsx esbuild.mts && npm run build:types", + "build:production": "npm run typecheck && npx tsx esbuild.mts --production && npm run build:types", + "build:types": "(npx tsc -p tsconfig.lib.json || true) && cp dist/types/cli/src/exports.d.ts dist/lib.d.ts && mkdir -p dist/agent && cp dist/types/cli/src/agent/ClineAgent.d.ts dist/types/cli/src/agent/ClineSessionEmitter.d.ts dist/types/cli/src/agent/public-types.d.ts dist/agent/ && rm -rf dist/types", "watch": "npx tsx esbuild.mts --watch", "dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink", "clean": "rimraf dist", diff --git a/cli/src/acp/ACPHostBridgeClientProvider.ts b/cli/src/acp/ACPHostBridgeClientProvider.ts index 5ed3acc284..9699358688 100644 --- a/cli/src/acp/ACPHostBridgeClientProvider.ts +++ b/cli/src/acp/ACPHostBridgeClientProvider.ts @@ -108,11 +108,7 @@ class ACPDiffServiceClient implements DiffServiceClientInterface { class ACPEnvServiceClient implements EnvServiceClientInterface { private readonly version: string - constructor( - _clientCapabilities: acp.ClientCapabilities | undefined, - _sessionIdResolver: SessionIdResolver, - version: string = "1.0.0", - ) { + constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) { this.version = version } @@ -402,7 +398,7 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider { clientCapabilities: acp.ClientCapabilities | undefined, sessionIdResolver: SessionIdResolver, cwdResolver: CwdResolver, - version: string = "1.0.0", + version: string, ) { this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver) this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version) diff --git a/cli/src/acp/AcpAgent.ts b/cli/src/acp/AcpAgent.ts index 202880067f..eab5d548a4 100644 --- a/cli/src/acp/AcpAgent.ts +++ b/cli/src/acp/AcpAgent.ts @@ -15,7 +15,7 @@ import type * as acp from "@agentclientprotocol/sdk" import { Logger } from "@/shared/services/Logger.js" import { ClineAgent } from "../agent/ClineAgent.js" -import type { AcpAgentOptions, SessionUpdateType } from "../agent/types.js" +import { type AcpAgentOptions, type SessionUpdateType } from "../agent/types.js" /** * ACP Agent wrapper that bridges stdio connection to ClineAgent. @@ -39,37 +39,21 @@ export class AcpAgent implements acp.Agent { this.clineAgent = new ClineAgent(options) // Wire up the permission handler to use the connection - this.clineAgent.setPermissionHandler(async (request, resolve) => { + this.clineAgent.setPermissionHandler(async (request) => { try { Logger.debug("[AcpAgent] Forwarding permission request to connection") - const response = await this.connection.requestPermission({ - sessionId: this.getCurrentSessionId() ?? "", + return await this.connection.requestPermission({ + sessionId: request.sessionId, toolCall: request.toolCall, options: request.options, }) - resolve(response) } catch (error) { Logger.debug("[AcpAgent] Error requesting permission:", error) - resolve({ outcome: "rejected" as unknown as acp.RequestPermissionOutcome }) + return { outcome: { outcome: "cancelled" } } } }) } - /** - * Get the current active session ID from the ClineAgent. - */ - private getCurrentSessionId(): string | undefined { - // Find the session that's currently processing - for (const [sessionId, session] of this.clineAgent.sessions) { - if (session.controller?.task) { - return sessionId - } - } - // Fall back to the first session if none is actively processing - const firstSession = this.clineAgent.sessions.keys().next() - return firstSession.done ? undefined : firstSession.value - } - /** * Subscribe to session events and forward them to the connection. */ diff --git a/cli/src/acp/index.ts b/cli/src/acp/index.ts index 09a3b17187..47458e6137 100644 --- a/cli/src/acp/index.ts +++ b/cli/src/acp/index.ts @@ -15,22 +15,18 @@ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { Logger } from "@/shared/services/Logger" -import { version as CLI_VERSION } from "../../../package.json" import { AcpAgent } from "./AcpAgent.js" import { nodeToWebReadable, nodeToWebWritable } from "./streamUtils.js" // Re-export classes for programmatic use export { ClineAgent } from "../agent/ClineAgent.js" export { ClineSessionEmitter } from "../agent/ClineSessionEmitter.js" -// Re-export types export type { AcpAgentOptions, AcpSessionState, - ClineAcpSession, ClineAgentOptions, ClineSessionEvents, PermissionHandler, - PermissionResolver, } from "../agent/types.js" export { AcpAgent } from "./AcpAgent.js" @@ -99,7 +95,6 @@ export async function runAcpMode(options: AcpModeOptions = {}): Promise { new AgentSideConnection((conn) => { agent = new AcpAgent(conn, { - version: CLI_VERSION, debug: Boolean(options.verbose), }) return agent diff --git a/cli/src/agent/ClineAgent.ts b/cli/src/agent/ClineAgent.ts index 560626ee18..d462ed175c 100644 --- a/cli/src/agent/ClineAgent.ts +++ b/cli/src/agent/ClineAgent.ts @@ -54,16 +54,19 @@ import { AuthService } from "@/services/auth/AuthService.js" import { Logger } from "@/shared/services/Logger.js" import type { Mode } from "@/shared/storage/types" import { openExternal } from "@/utils/env" +import { version as AGENT_VERSION } from "../../package.json" import { ACPDiffViewProvider } from "../acp/ACPDiffViewProvider.js" import { ACPHostBridgeClientProvider } from "../acp/ACPHostBridgeClientProvider.js" import { AcpTerminalManager } from "../acp/AcpTerminalManager.js" -import { isAuthConfigured } from "../index.js" +import { isAuthConfigured } from "../utils/auth" import { fetchOpenRouterModels, usesOpenRouterModels } from "../utils/openrouter-models" import { CliContextResult, initializeCliContext } from "../vscode-context.js" import { ClineSessionEmitter } from "./ClineSessionEmitter.js" import { translateMessage } from "./messageTranslator.js" import { handlePermissionResponse } from "./permissionHandler.js" -import type { AcpSessionState, ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./types.js" +import type { ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./public-types.js" +import { AcpSessionStatus } from "./public-types.js" +import { type AcpSessionState } from "./types.js" // Map providers to their static model lists and defaults (copied from ModelPicker.tsx) const providerModels: Record; defaultId: string }> = { @@ -104,7 +107,12 @@ function getModelList(provider: string): string[] { export class ClineAgent implements acp.Agent { private readonly options: ClineAgentOptions private readonly ctx: CliContextResult - readonly sessions: Map = new Map() + + /** Map of active sessions by session ID */ + public readonly sessions: Map = new Map() + + /** WeakMap to associate ClineAcpSession with its Controller without exposing it to consumers */ + readonly #sessionControllers = new WeakMap() /** Runtime state for active sessions */ private readonly sessionStates: Map = new Map() @@ -132,7 +140,7 @@ export class ClineAgent implements acp.Agent { constructor(options: ClineAgentOptions) { this.options = options - this.ctx = initializeCliContext() + this.ctx = initializeCliContext({ clineDir: options.clineDir }) } /** @@ -194,7 +202,7 @@ export class ClineAgent implements acp.Agent { }, agentInfo: { name: "cline", - version: this.options.version, + version: AGENT_VERSION, }, authMethods: [ { @@ -226,7 +234,7 @@ export class ClineAgent implements acp.Agent { clientCapabilities, () => this.currentActiveSessionId, () => this.sessions.get(this.currentActiveSessionId ?? "")?.cwd ?? process.cwd(), - this.options.version, + AGENT_VERSION, ) HostProvider.initialize( @@ -289,16 +297,16 @@ export class ClineAgent implements acp.Agent { mcpServers: params.mcpServers ?? [], createdAt: Date.now(), lastActivityAt: Date.now(), - controller, } + this.#sessionControllers.set(session, controller) + this.sessions.set(sessionId, session) // Initialize session state const sessionState: AcpSessionState = { sessionId, - isProcessing: false, - cancelled: false, + status: AcpSessionStatus.Idle, pendingToolCalls: new Map(), } @@ -435,11 +443,11 @@ export class ClineAgent implements acp.Agent { * * The prompt flow: * 1. Extract content from the ACP prompt (text, images, files) - * 2. Set up state broadcasting (subscribe to controller updates) - * 3. Initialize or continue task with Controller + * 2. Set up internal cline state subsription + * 3. Initialize or continue cline task * 4. Translate ClineMessages to ACP SessionUpdates * 5. Handle permission requests for tools/commands - * 6. Return when task completes, is cancelled, or needs user input + * 6. Return when cline task completes, is cancelled, or needs user input */ async prompt(params: acp.PromptRequest): Promise { const session = this.sessions.get(params.sessionId) @@ -449,11 +457,11 @@ export class ClineAgent implements acp.Agent { throw new Error(`Session not found: ${params.sessionId}`) } - if (sessionState.isProcessing) { + if (sessionState.status === AcpSessionStatus.Processing) { throw new Error(`Session ${params.sessionId} is already processing a prompt`) } - const controller = session.controller + const controller = this.#sessionControllers.get(session) if (!controller) { throw new Error("Controller not initialized for session. This is a bug in the ACP agent setup.") } @@ -464,8 +472,7 @@ export class ClineAgent implements acp.Agent { }) // Mark session as processing and set as current active session - sessionState.isProcessing = true - sessionState.cancelled = false + sessionState.status = AcpSessionStatus.Processing session.lastActivityAt = Date.now() this.currentActiveSessionId = params.sessionId @@ -586,7 +593,7 @@ export class ClineAgent implements acp.Agent { Logger.debug("[ClineAgent] Error during cleanup:", error) } } - sessionState.isProcessing = false + sessionState.status = AcpSessionStatus.Idle } } @@ -648,7 +655,13 @@ export class ClineAgent implements acp.Agent { permissionRequest: Omit, ): Promise { const session = this.sessions.get(sessionId) - const controller = session?.controller + + if (!session) { + Logger.debug("[ClineAgent] No session found for permission request") + return + } + + const controller = this.#sessionControllers.get(session) if (!controller?.task) { Logger.debug("[ClineAgent] No active task for permission request") @@ -829,7 +842,7 @@ export class ClineAgent implements acp.Agent { await this.emitSessionUpdate(sessionId, { sessionUpdate, - content: { type: "text", text: needsNewline ? "\n" + textDelta : textDelta }, + content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta }, }) } @@ -882,18 +895,22 @@ export class ClineAgent implements acp.Agent { */ async cancel(params: acp.CancelNotification): Promise { const session = this.sessions.get(params.sessionId) + if (!session) { + Logger.debug("[ClineAgent] cancel called for non-existent session:", params.sessionId) + return + } const sessionState = this.sessionStates.get(params.sessionId) Logger.debug("[ClineAgent] cancel called:", { sessionId: params.sessionId, - isProcessing: sessionState?.isProcessing, + status: sessionState?.status, }) if (sessionState) { - sessionState.cancelled = true + sessionState.status = AcpSessionStatus.Cancelled // If we have an active controller task, cancel it - const controller = session?.controller + const controller = this.#sessionControllers.get(session) if (controller?.task) { try { await controller.cancelTask() @@ -934,7 +951,7 @@ export class ClineAgent implements acp.Agent { session.lastActivityAt = Date.now() // Update Controller mode if active - const controller = session.controller + const controller = this.#sessionControllers.get(session) if (controller) { controller.stateManager.setGlobalState("mode", session.mode) @@ -1065,7 +1082,7 @@ export class ClineAgent implements acp.Agent { * @returns The permission response from the client */ protected async requestPermission( - _sessionId: string, + sessionId: string, toolCall: acp.ToolCallUpdate, options: acp.PermissionOption[], ): Promise { @@ -1080,17 +1097,15 @@ export class ClineAgent implements acp.Agent { return { outcome: "rejected" as unknown as acp.RequestPermissionOutcome } } - // Use the permission handler callback pattern - return new Promise((resolve) => { - this.permissionHandler!({ toolCall, options }, resolve) - }) + return await this.permissionHandler({ sessionId, toolCall, options }) } async shutdown(): Promise { for (const [sessionId, session] of this.sessions) { - await session.controller?.task?.abortTask() - await session.controller?.stateManager.flushPendingState() - await session.controller?.dispose() + const controller = this.#sessionControllers.get(session) + await controller?.task?.abortTask() + await controller?.stateManager.flushPendingState() + await controller?.dispose() this.sessions.delete(sessionId) this.sessionStates.delete(sessionId) } diff --git a/cli/src/agent/ClineSessionEmitter.ts b/cli/src/agent/ClineSessionEmitter.ts index ef9b6d32a7..a97efe285d 100644 --- a/cli/src/agent/ClineSessionEmitter.ts +++ b/cli/src/agent/ClineSessionEmitter.ts @@ -8,7 +8,7 @@ */ import { EventEmitter } from "events" -import type { ClineSessionEvents } from "./types.js" +import type { ClineSessionEvents } from "./public-types.js" /** * Type-safe EventEmitter for ClineAgent session events. diff --git a/cli/src/agent/messageTranslator.test.ts b/cli/src/agent/messageTranslator.test.ts index 8f2df83590..c2ecd98e6e 100644 --- a/cli/src/agent/messageTranslator.test.ts +++ b/cli/src/agent/messageTranslator.test.ts @@ -12,6 +12,7 @@ import type { ClineMessage } from "@shared/ExtensionMessage" import { beforeEach, describe, expect, it } from "vitest" import { createSessionState, translateMessage, translateMessages } from "./messageTranslator" import type { AcpSessionState } from "./types" +import { AcpSessionStatus } from "./types" // ============================================================================= // Test Helpers @@ -175,8 +176,7 @@ describe("createSessionState", () => { const state = createSessionState("my-session-123") expect(state.sessionId).toBe("my-session-123") - expect(state.isProcessing).toBe(false) - expect(state.cancelled).toBe(false) + expect(state.status).toBe(AcpSessionStatus.Idle) expect(state.pendingToolCalls).toBeInstanceOf(Map) expect(state.pendingToolCalls.size).toBe(0) expect(state.currentToolCallId).toBeUndefined() @@ -187,11 +187,11 @@ describe("createSessionState", () => { const state2 = createSessionState("session-2") // Modify state1 - state1.isProcessing = true + state1.status = AcpSessionStatus.Processing state1.pendingToolCalls.set("tool-1", {} as acp.ToolCall) // state2 should be unaffected - expect(state2.isProcessing).toBe(false) + expect(state2.status).toBe(AcpSessionStatus.Idle) expect(state2.pendingToolCalls.size).toBe(0) }) }) diff --git a/cli/src/agent/messageTranslator.ts b/cli/src/agent/messageTranslator.ts index 99953e3e47..3da28d502d 100644 --- a/cli/src/agent/messageTranslator.ts +++ b/cli/src/agent/messageTranslator.ts @@ -11,6 +11,7 @@ import type * as acp from "@agentclientprotocol/sdk" import type { ClineMessage, ClineSayBrowserAction, ClineSayTool } from "@shared/ExtensionMessage" import type { AcpSessionState, TranslatedMessage } from "./types.js" +import { AcpSessionStatus } from "./types.js" /** * Maps Cline tool types to ACP ToolKind values. @@ -1019,8 +1020,7 @@ export function translateMessages(messages: ClineMessage[], sessionState: AcpSes export function createSessionState(sessionId: string): AcpSessionState { return { sessionId, - isProcessing: false, - cancelled: false, + status: AcpSessionStatus.Idle, pendingToolCalls: new Map(), } } diff --git a/cli/src/agent/public-types.ts b/cli/src/agent/public-types.ts new file mode 100644 index 0000000000..61a603296a --- /dev/null +++ b/cli/src/agent/public-types.ts @@ -0,0 +1,254 @@ +/** + * Public types for the Cline library API. + * + * This file contains types that are safe to export to library consumers. + * It must NOT import any internal types (Controller, StateManager, etc.) + * to keep the generated declaration files clean. + * + * Internal-only extensions of these types live in ./types.ts. + */ + +import type * as acp from "@agentclientprotocol/sdk" + +// ============================================================ +// Session Update Type Utilities +// ============================================================ + +/** + * Different types of updates that can be sent during session processing. + * + * These updates provide real-time feedback about the agent's progress. + * + * See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output) + */ +export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"] + +/** + * Different types of update payloads that can be sent during session processing. + * + * Each update type has a corresponding payload structure defined in the ACP SessionUpdate union. + */ +export type SessionUpdatePayload = Omit< + Extract, + "sessionUpdate" +> + +// ============================================================ +// Permission Handler Callback Types +// ============================================================ + +/** + * Handler function for permission requests. + * Called when the agent needs permission for a tool call. + * The handler should present the request to the user and call resolve() with their response. + */ +export type PermissionHandler = (request: acp.RequestPermissionRequest) => Promise + +// ============================================================ +// Session Event Emitter Types +// ============================================================ + +/** + * Maps ACP SessionUpdate types to their event listener signatures. + * Uses the sessionUpdate discriminator to derive event names and payload types. + */ +export type ClineSessionEvents = { + [K in SessionUpdateType]: (payload: SessionUpdatePayload) => void +} & { + /** Error event for session-level errors (not part of ACP SessionUpdate) */ + error: (error: Error) => void +} + +// ============================================================ +// ClineAgent Options +// ============================================================ + +/** + * Options for creating a ClineAgent instance. + */ +export interface ClineAgentOptions { + /** Whether debug logging is enabled */ + debug?: boolean + /** Cline Config Directory (defaults to ~/.cline) */ + clineDir?: string +} + +/** + * Options for creating an ACP agent instance. + */ +export interface AcpAgentOptions { + /** Whether debug logging is enabled */ + debug?: boolean +} + +// ============================================================ +// Session Types +// ============================================================ +export type SessionID = string + +/** + * Extended session data stored by Cline for ACP sessions. + */ +export interface ClineAcpSession { + /** Unique session ID */ + sessionId: SessionID + /** Working directory for the session */ + cwd: string + /** Current mode (plan/act) */ + mode: "plan" | "act" + /** MCP servers passed from the client */ + mcpServers: acp.McpServer[] + /** Timestamp when session was created */ + createdAt: number + /** Timestamp of last activity */ + lastActivityAt: number + /** Whether this session was loaded from history (needs resume on first prompt) */ + isLoadedFromHistory?: boolean + /** Model ID override for plan mode (format: "provider/modelId") */ + planModeModelId?: string + /** Model ID override for act mode (format: "provider/modelId") */ + actModeModelId?: string +} + +/** + * Lifecycle status of an ACP session. + * + * Represents the state machine: + * Idle → Processing → Idle (normal completion) + * Idle → Processing → Cancelled (cancellation, then back to Idle on next prompt) + */ +export enum AcpSessionStatus { + /** Session is idle, waiting for a prompt */ + Idle = "idle", + /** Session is actively processing a prompt */ + Processing = "processing", + /** Session processing was cancelled */ + Cancelled = "cancelled", +} + +/** + * State tracking for an active ACP session within Cline. + */ +export interface AcpSessionState { + /** Session ID */ + sessionId: SessionID + /** Current lifecycle status of the session */ + status: AcpSessionStatus + /** Current tool call ID being executed (if any) */ + currentToolCallId?: string + /** Accumulated tool calls for permission batching */ + pendingToolCalls: Map +} + +// ============================================================ +// Agent Capabilities +// ============================================================ + +/** + * Cline-specific agent capabilities extending the ACP base capabilities. + */ +export interface ClineAgentCapabilities { + /** Support for loading sessions from disk */ + loadSession: boolean + /** Prompt capabilities for the agent */ + promptCapabilities: { + /** Support for image inputs */ + image: boolean + /** Support for audio inputs */ + audio: boolean + /** Support for embedded context (file resources) */ + embeddedContext: boolean + } + /** MCP server passthrough capabilities */ + mcpCapabilities: { + /** Support for HTTP MCP servers */ + http: boolean + /** Support for SSE MCP servers */ + sse: boolean + } +} + +/** + * Cline agent info for ACP initialization response. + */ +export interface ClineAgentInfo { + name: "cline" + title: "Cline" + version: string +} + +// ============================================================ +// Permission Options +// ============================================================ + +/** + * Permission option as presented to the ACP client. + */ +export interface ClinePermissionOption { + kind: acp.PermissionOptionKind + name: string + optionId: string +} + +// ============================================================ +// Message Translation +// ============================================================ + +/** + * Result of translating a Cline message to ACP session update(s). + * A single Cline message may produce multiple ACP updates. + */ +export interface TranslatedMessage { + /** The session updates to send */ + updates: acp.SessionUpdate[] + /** Whether this message requires a permission request */ + requiresPermission?: boolean + /** Permission request details if required */ + permissionRequest?: Omit + /** The toolCallId that was created/used (for tracking across streaming updates) */ + toolCallId?: string +} + +// ============================================================ +// Re-exported ACP Types +// ============================================================ + +export type { + Agent, + AgentSideConnection, + AudioContent, + CancelNotification, + ClientCapabilities, + ContentBlock, + ImageContent, + InitializeRequest, + InitializeResponse, + LoadSessionRequest, + LoadSessionResponse, + McpServer, + ModelInfo, + NewSessionRequest, + NewSessionResponse, + PermissionOption, + PermissionOptionKind, + PromptRequest, + PromptResponse, + RequestPermissionRequest, + RequestPermissionResponse, + SessionConfigOption, + SessionModelState, + SessionNotification, + SessionUpdate, + SetSessionConfigOptionRequest, + SetSessionConfigOptionResponse, + SetSessionModelRequest, + SetSessionModelResponse, + SetSessionModeRequest, + SetSessionModeResponse, + StopReason, + TextContent, + ToolCall, + ToolCallStatus, + ToolCallUpdate, + ToolKind, +} from "@agentclientprotocol/sdk" diff --git a/cli/src/agent/types.ts b/cli/src/agent/types.ts index b22de33922..90440635c7 100644 --- a/cli/src/agent/types.ts +++ b/cli/src/agent/types.ts @@ -1,76 +1,13 @@ /** - * Custom types and extensions for ACP integration with Cline CLI. + * Internal types for ACP integration with Cline CLI. * - * This file extends the base ACP types with Cline-specific functionality. + * This file re-exports all public types from ./public-types.ts and adds + * internal-only Types that reference core modules (Controller, etc.). + * + * Library consumers should never import from this file directly — they + * get the public types via the library entrypoint (exports.ts). */ -import type * as acp from "@agentclientprotocol/sdk" -import type { Controller } from "@/core/controller" - -// ============================================================ -// Session Update Type Utilities -// ============================================================ - -/** - * Extract the sessionUpdate discriminator value from a SessionUpdate variant. - */ -export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"] - -/** - * Extract the payload type for a given sessionUpdate discriminator value. - * This removes the `sessionUpdate` discriminator field from the type. - */ -export type SessionUpdatePayload = Omit< - Extract, - "sessionUpdate" -> - -// ============================================================ -// Permission Handler Callback Types -// ============================================================ - -/** - * Callback to resolve a permission request with the user's response. - */ -export type PermissionResolver = (response: acp.RequestPermissionResponse) => void - -/** - * Handler function for permission requests. - * Called when the agent needs permission for a tool call. - * The handler should present the request to the user and call resolve() with their response. - */ -export type PermissionHandler = (request: Omit, resolve: PermissionResolver) => void - -// ============================================================ -// Session Event Emitter Types -// ============================================================ - -/** - * Maps ACP SessionUpdate types to their event listener signatures. - * Uses the sessionUpdate discriminator to derive event names and payload types. - */ -export type ClineSessionEvents = { - [K in SessionUpdateType]: (payload: SessionUpdatePayload) => void -} & { - /** Error event for session-level errors (not part of ACP SessionUpdate) */ - error: (error: Error) => void -} - -// ============================================================ -// ClineAgent Options (decoupled from connection) -// ============================================================ - -/** - * Options for creating a ClineAgent instance (decoupled from connection). - */ -export interface ClineAgentOptions { - /** CLI version string */ - version: string - /** Whether debug logging is enabled */ - debug?: boolean -} - -// Re-export common ACP types for convenience export type { Agent, AgentSideConnection, @@ -114,134 +51,18 @@ export type { WriteTextFileResponse, } from "@agentclientprotocol/sdk" -/** - * Cline-specific agent capabilities extending the ACP base capabilities. - */ -export interface ClineAgentCapabilities { - /** Support for loading sessions from disk */ - loadSession: boolean - /** Prompt capabilities for the agent */ - promptCapabilities: { - /** Support for image inputs */ - image: boolean - /** Support for audio inputs */ - audio: boolean - /** Support for embedded context (file resources) */ - embeddedContext: boolean - } - /** MCP server passthrough capabilities */ - mcpCapabilities: { - /** Support for HTTP MCP servers */ - http: boolean - /** Support for SSE MCP servers */ - sse: boolean - } -} +export type { + AcpAgentOptions, + AcpSessionState, + ClineAgentCapabilities, + ClineAgentInfo, + ClineAgentOptions, + ClinePermissionOption, + ClineSessionEvents, + PermissionHandler, + SessionUpdatePayload, + SessionUpdateType, + TranslatedMessage, +} from "./public-types.js" -/** - * Cline agent info for ACP initialization response. - */ -export interface ClineAgentInfo { - name: "cline" - title: "Cline" - version: string -} - -/** - * Extended session data stored by Cline for ACP sessions. - * Maps to Cline's task history structure. - */ -export interface ClineAcpSession { - /** Unique session/task ID */ - sessionId: string - /** Working directory for the session */ - cwd: string - /** Current mode (plan/act) */ - mode: "plan" | "act" - /** MCP servers passed from the client */ - mcpServers: acp.McpServer[] - /** Timestamp when session was created */ - createdAt: number - /** Timestamp of last activity */ - lastActivityAt: number - /** Whether this session was loaded from history (needs resume on first prompt) */ - isLoadedFromHistory?: boolean - /** Controller instance for this session (manages task execution) */ - controller?: Controller - /** Model ID override for plan mode (format: "provider/modelId") */ - planModeModelId?: string - /** Model ID override for act mode (format: "provider/modelId") */ - actModeModelId?: string -} - -/** - * Permission option as presented to the ACP client. - */ -export interface ClinePermissionOption { - kind: acp.PermissionOptionKind - name: string - optionId: string -} - -/** - * Mapping of Cline message types to their ACP session update equivalents. - */ -export type ClineToAcpUpdateMapping = { - /** Text messages from the agent */ - text: "agent_message_chunk" - /** Reasoning/thinking from the agent */ - reasoning: "agent_thought_chunk" - /** Markdown content from the agent */ - markdown: "agent_message_chunk" - /** Tool execution */ - tool: "tool_call" - /** Command execution */ - command: "tool_call" - /** Command output */ - command_output: "tool_call_update" - /** Task completion */ - completion_result: "end_turn" - /** Error messages */ - error: "tool_call_update" | "error" -} - -/** - * Options for creating an ACP agent instance. - */ -export interface AcpAgentOptions { - /** CLI version string */ - version: string - /** Whether debug logging is enabled */ - debug?: boolean -} - -/** - * Result of translating a Cline message to ACP session update(s). - * A single Cline message may produce multiple ACP updates. - */ -export interface TranslatedMessage { - /** The session updates to send */ - updates: acp.SessionUpdate[] - /** Whether this message requires a permission request */ - requiresPermission?: boolean - /** Permission request details if required */ - permissionRequest?: Omit - /** The toolCallId that was created/used (for tracking across streaming updates) */ - toolCallId?: string -} - -/** - * State tracking for an active ACP session within Cline. - */ -export interface AcpSessionState { - /** Session ID */ - sessionId: string - /** Whether the session is currently processing a prompt */ - isProcessing: boolean - /** Current tool call ID being executed (if any) */ - currentToolCallId?: string - /** Whether the session has been cancelled */ - cancelled: boolean - /** Accumulated tool calls for permission batching */ - pendingToolCalls: Map -} +export { AcpSessionStatus } from "./public-types.js" diff --git a/cli/src/exports.ts b/cli/src/exports.ts new file mode 100644 index 0000000000..4cd4561aff --- /dev/null +++ b/cli/src/exports.ts @@ -0,0 +1,71 @@ +/** + * Cline Library Exports + * + * This file exports the public API for programmatic use of Cline. + * Use these classes and types to embed Cline into your applications. + * + * @example + * ```typescript + * import { ClineAgent } from "cline" + * + * const agent = new ClineAgent() + * await agent.initialize({ clientCapabilities: {} }) + * const session = await agent.newSession({ cwd: process.cwd() }) + * ``` + * @module cline + */ + +export { ClineAgent } from "./agent/ClineAgent.js" +export { ClineSessionEmitter } from "./agent/ClineSessionEmitter.js" +export type { + AcpAgentOptions, + AcpSessionState, + AcpSessionStatus, + Agent, + AgentSideConnection, + AudioContent, + CancelNotification, + ClientCapabilities, + ClineAcpSession, + ClineAgentCapabilities, + ClineAgentInfo, + ClineAgentOptions, + ClinePermissionOption, + ClineSessionEvents, + ContentBlock, + ImageContent, + InitializeRequest, + InitializeResponse, + LoadSessionRequest, + LoadSessionResponse, + McpServer, + ModelInfo, + NewSessionRequest, + NewSessionResponse, + PermissionHandler, + PermissionOption, + PermissionOptionKind, + PromptRequest, + PromptResponse, + RequestPermissionRequest, + RequestPermissionResponse, + SessionConfigOption, + SessionModelState, + SessionNotification, + SessionUpdate, + SessionUpdatePayload, + SessionUpdateType, + SetSessionConfigOptionRequest, + SetSessionConfigOptionResponse, + SetSessionModelRequest, + SetSessionModelResponse, + SetSessionModeRequest, + SetSessionModeResponse, + StopReason, + TextContent, + ToolCall, + ToolCallStatus, + ToolCallUpdate, + ToolKind, + TranslatedMessage, +} from "./agent/public-types.js" diff --git a/cli/src/index.ts b/cli/src/index.ts index b428aa7c21..37179d82f6 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -20,7 +20,7 @@ import { PostHogClientProvider } from "@/services/telemetry/providers/posthog/Po import { HistoryItem } from "@/shared/HistoryItem" import { Logger } from "@/shared/services/Logger" import { Session } from "@/shared/services/Session" -import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage" +import { getProviderModelIdKey } from "@/shared/storage" import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@/shared/storage/types" import { version as CLI_VERSION } from "../package.json" import { runAcpMode } from "./acp/index.js" @@ -29,7 +29,8 @@ import { checkRawModeSupport } from "./context/StdinContext" import { createCliHostBridgeProvider } from "./controllers" import { CliCommentReviewController } from "./controllers/CliCommentReviewController" import { CliWebviewProvider } from "./controllers/CliWebviewProvider" -import { restoreConsole } from "./utils/console" +import { isAuthConfigured } from "./utils/auth" +import { restoreConsole, suppressConsoleUnlessVerbose } from "./utils/console" import { printInfo, printWarning } from "./utils/display" import { selectOutputMode } from "./utils/mode-selection" import { parseImagesFromInput, processImagePaths } from "./utils/parser" @@ -42,6 +43,10 @@ import { autoUpdateOnStartup, checkForUpdates } from "./utils/update" import { initializeCliContext } from "./vscode-context" import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim" +// CLI-only behavior: suppress console output unless verbose mode is enabled. +// Kept explicit here so importing the library bundle does not mutate global console methods. +suppressConsoleUnlessVerbose() + /** * Common options shared between runTask and resumeTask */ @@ -790,68 +795,6 @@ devCommand await openExternal(CLI_LOG_FILE) }) -/** - * Check if the user has completed onboarding (has any provider configured). - * - * Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach. - * If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials - * and sets the flag accordingly. - */ -export async function isAuthConfigured(): Promise { - const stateManager = StateManager.get() - - // Check welcomeViewCompleted first - this is the single source of truth - const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted") - if (welcomeViewCompleted !== undefined) { - return welcomeViewCompleted - } - - // welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials - // This mirrors the extension's migrateWelcomeViewCompleted behavior - const hasAnyAuth = await checkAnyProviderConfigured() - - // Set welcomeViewCompleted based on what we found - stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth) - await stateManager.flushPendingState() - - return hasAnyAuth -} - -/** - * Check if ANY provider has valid credentials configured. - * Used for migration when welcomeViewCompleted is undefined. - */ -async function checkAnyProviderConfigured(): Promise { - const stateManager = StateManager.get() - const config = stateManager.getApiConfiguration() as Record - - // Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config) - if (config["clineApiKey"] || config["cline:clineAccountId"]) return true - - // Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config) - if (config["openai-codex-oauth-credentials"]) return true - - // Check all BYO provider API keys (loaded into config from secrets) - for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) { - // Skip cline - already checked above with the correct key - if (provider === "cline") continue - - const fields = Array.isArray(keyField) ? keyField : [keyField] - for (const field of fields) { - if (config[field]) return true - } - } - - // Check provider-specific settings that indicate configuration - // (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio) - if (config.awsRegion) return true - if (config.vertexProjectId) return true - if (config.ollamaBaseUrl) return true - if (config.lmStudioBaseUrl) return true - - return false -} - /** * Validate that a task exists in history * @returns The task history item if found, null otherwise diff --git a/cli/src/lib-import.test.ts b/cli/src/lib-import.test.ts new file mode 100644 index 0000000000..69f42055d6 --- /dev/null +++ b/cli/src/lib-import.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest" + +describe("library import side effects", () => { + it("importing library exports must not mutate console.log", async () => { + const originalConsoleLog = console.log + await import("./exports") + expect(console.log).toBe(originalConsoleLog) + }, 10000) +}) diff --git a/cli/src/utils/auth.ts b/cli/src/utils/auth.ts new file mode 100644 index 0000000000..4274c6cda4 --- /dev/null +++ b/cli/src/utils/auth.ts @@ -0,0 +1,64 @@ +import { StateManager } from "@/core/storage/StateManager" +import { ProviderToApiKeyMap } from "@/shared/storage" + +/** + * Check if the user has completed onboarding (has any provider configured). + * + * Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach. + * If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials + * and sets the flag accordingly. + */ +export async function isAuthConfigured(): Promise { + const stateManager = StateManager.get() + + // Check welcomeViewCompleted first - this is the single source of truth + const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted") + if (welcomeViewCompleted !== undefined) { + return welcomeViewCompleted + } + + // welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials + // This mirrors the extension's migrateWelcomeViewCompleted behavior + const hasAnyAuth = await checkAnyProviderConfigured() + + // Set welcomeViewCompleted based on what we found + stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth) + await stateManager.flushPendingState() + + return hasAnyAuth +} + +/** + * Check if ANY provider has valid credentials configured. + * Used for migration when welcomeViewCompleted is undefined. + */ +export async function checkAnyProviderConfigured(): Promise { + const stateManager = StateManager.get() + const config = stateManager.getApiConfiguration() as Record + + // Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config) + if (config["clineApiKey"] || config["cline:clineAccountId"]) return true + + // Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config) + if (config["openai-codex-oauth-credentials"]) return true + + // Check all BYO provider API keys (loaded into config from secrets) + for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) { + // Skip cline - already checked above with the correct key + if (provider === "cline") continue + + const fields = Array.isArray(keyField) ? keyField : [keyField] + for (const field of fields) { + if (config[field]) return true + } + } + + // Check provider-specific settings that indicate configuration + // (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio) + if (config.awsRegion) return true + if (config.vertexProjectId) return true + if (config.ollamaBaseUrl) return true + if (config.lmStudioBaseUrl) return true + + return false +} diff --git a/cli/src/utils/console.ts b/cli/src/utils/console.ts index 2fcb59adb4..25d7d7551b 100644 --- a/cli/src/utils/console.ts +++ b/cli/src/utils/console.ts @@ -12,11 +12,19 @@ export const originalConsoleWarn = console.warn.bind(console) export const originalConsoleInfo = console.info.bind(console) export const originalConsoleDebug = console.debug.bind(console) -// Check for verbose flag early (before commander parses) -const isVerbose = process.argv.includes("-v") || process.argv.includes("--verbose") +/** + * Suppress console output unless verbose mode is enabled. + * + * This is intentionally opt-in and should only be called by the CLI entrypoint. + * Library consumers should not have their global console methods mutated as a + * side effect of importing the library bundle. + */ +export function suppressConsoleUnlessVerbose(argv: string[] = process.argv) { + const isVerbose = argv.includes("-v") || argv.includes("--verbose") + if (isVerbose) { + return + } -// Suppress console output unless verbose mode -if (!isVerbose) { console.log = () => {} console.warn = () => {} console.error = () => {} diff --git a/cli/tsconfig.lib.json b/cli/tsconfig.lib.json new file mode 100644 index 0000000000..151cac6b02 --- /dev/null +++ b/cli/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "noCheck": true, + "noResolve": true, + "outDir": "dist/types" + }, + "include": [ + "src/exports.ts", + "src/agent/public-types.ts", + "src/agent/ClineAgent.ts", + "src/agent/ClineSessionEmitter.ts", + "src/agent/types.ts", + "src/agent/messageTranslator.ts", + "src/agent/permissionHandler.ts" + ] +} diff --git a/docs/cline-sdk/overview.md b/docs/cline-sdk/overview.md new file mode 100644 index 0000000000..9f335f5c13 --- /dev/null +++ b/docs/cline-sdk/overview.md @@ -0,0 +1,643 @@ +# Cline SDK + +The Cline SDK lets you embed Cline as a programmable coding agent in your Node.js applications. It exposes the same capabilities as the Cline CLI and VS Code extension — file editing, command execution, browser use, MCP servers — through a TypeScript API that conforms to the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/protocol/schema). + +## Installation + +```bash +npm install cline +``` + +If you want direct ACP type imports as well: + +```bash +npm install @agentclientprotocol/sdk +``` + +Requires Node.js 20+. + +## Quick Start + +```typescript +import { ClineAgent } from "cline" + +const agent = new ClineAgent({ version: "1.0.0" }) + +// 1. Initialize — negotiates capabilities +await agent.initialize({ + protocolVersion: 1, + clientCapabilities: {}, +}) + +// 2. Authenticate (if using Cline-hosted models) +await agent.authenticate({ methodId: "cline-oauth" }) + +// 3. Create a session +const { sessionId } = await agent.newSession({ + cwd: process.cwd(), + mcpServers: [], +}) + +// 4. Subscribe to streaming output +const emitter = agent.emitterForSession(sessionId) + +emitter.on("agent_message_chunk", (payload) => { + process.stdout.write(payload.content.text) +}) + +emitter.on("tool_call", (payload) => { + console.log(`[tool] ${payload.title}`) +}) + +emitter.on("error", (err) => { + console.error("[session error]", err) +}) + +// 5. Send a prompt and wait for completion +const { stopReason } = await agent.prompt({ + sessionId, + prompt: [{ type: "text", text: "Create a hello world Express server" }], +}) + +console.log("Done:", stopReason) + +// 6. Clean up +await agent.shutdown() +``` + +## Core Concepts + +### Agent Lifecycle + +The SDK follows the ACP lifecycle: + +``` +initialize() → authenticate() → newSession() → prompt() ⇄ events → shutdown() +``` + +| Step | Method | Purpose | +|------|--------|---------| +| Init | `initialize()` | Exchange protocol version and capabilities | +| Auth | `authenticate()` | OAuth flow for Cline or OpenAI Codex accounts | +| Session | `newSession()` | Create an isolated conversation context | +| Prompt | `prompt()` | Send user messages; blocks until the turn ends | +| Cancel | `cancel()` | Abort an in-progress prompt turn | +| Mode | `setSessionMode()` | Switch between `"plan"` and `"act"` modes | +| Model | `unstable_setSessionModel()` | Change the backing LLM (experimental) | +| Shutdown | `shutdown()` | Abort all tasks, flush state, release resources | + +### Sessions + +A session is an independent conversation with its own task history, working directory, and MCP server connections. You can run multiple sessions concurrently. + +```typescript +const { sessionId, modes, models } = await agent.newSession({ + cwd: "/path/to/project", + mcpServers: [ + { name: "my-server", command: "npx", args: ["-y", "my-mcp-server"] }, + ], +}) +``` + +The response includes: +- `sessionId` — use this in all subsequent calls +- `modes` — available modes (`plan`, `act`) and the current mode +- `models` — available models and the current model ID + +Access session metadata via the read-only `sessions` map: + +```typescript +const session = agent.sessions.get(sessionId) +// { sessionId, cwd, mode, mcpServers, createdAt, lastActivityAt, ... } +``` + +### Prompting + +`prompt()` sends a user message and blocks until the agent finishes its turn. While the prompt is processing, the agent streams output via session events. + +```typescript +const response = await agent.prompt({ + sessionId, + prompt: [ + { type: "text", text: "Refactor the auth module to use JWT" }, + ], +}) +``` + +The prompt array accepts multiple content blocks: + +```typescript +// Text + image + file context +await agent.prompt({ + sessionId, + prompt: [ + { type: "text", text: "What's in this screenshot?" }, + { type: "image", data: base64ImageData, mimeType: "image/png" }, + { + type: "resource", + resource: { + uri: "file:///path/to/relevant-file.ts", + mimeType: "text/plain", + text: fileContents, + }, + }, + ], +}) +``` + +#### Content Block Types + +| Type | Fields | Description | +|------|--------|-------------| +| `TextContent` | `{ type: "text", text: string }` | Plain text message | +| `ImageContent` | `{ type: "image", mimeType: string, data: string }` | Base64-encoded image | +| `EmbeddedResource` | `{ type: "resource", resource: { uri: string, mimeType?: string, text?: string, blob?: string } }` | File or resource context | + +#### Stop Reasons + +`prompt()` resolves with a `stopReason`: + +| Value | Meaning | +|-------|---------| +| `"end_turn"` | Agent finished normally (completed task or waiting for user input) | +| `"cancelled"` | You called `cancel()` during the turn | +| `"error"` | An unrecoverable error occurred | +| `"max_tokens"` | Context window exhausted | + +### Streaming Events + +Subscribe to real-time output via `ClineSessionEmitter`. Each session has its own emitter. + +```typescript +const emitter = agent.emitterForSession(sessionId) +``` + +#### Event Types + +All events correspond to [ACP `SessionUpdate` types](https://agentclientprotocol.com/protocol/schema#SessionUpdate): + +| Event | Payload | Description | +|-------|---------|-------------| +| `agent_message_chunk` | `{ content: ContentBlock }` | Streamed text from the agent | +| `agent_thought_chunk` | `{ content: ContentBlock }` | Internal reasoning / chain-of-thought | +| `tool_call` | `ToolCall` | New tool invocation (file edit, command, etc.) | +| `tool_call_update` | `ToolCallUpdate` | Progress/result update for an existing tool call | +| `plan` | `{ entries: PlanEntry[] }` | Agent's execution plan | +| `available_commands_update` | `{ availableCommands: AvailableCommand[] }` | Slash commands the agent supports | +| `current_mode_update` | `{ currentModeId: string }` | Mode changed (plan/act) | +| `user_message_chunk` | `{ content: ContentBlock }` | User message chunks (for multi-turn) | +| `config_option_update` | `{ configOptions: SessionConfigOption[] }` | Configuration changed | +| `session_info_update` | Session metadata | Session metadata changed | +| `error` | `Error` | Session-level error (not an ACP update) | + +```typescript +emitter.on("agent_message_chunk", (payload) => { + // payload.content is a ContentBlock — usually { type: "text", text: "..." } + process.stdout.write(payload.content.text) +}) + +emitter.on("agent_thought_chunk", (payload) => { + console.log("[thinking]", payload.content.text) +}) + +emitter.on("tool_call", (payload) => { + console.log(`[${payload.kind}] ${payload.title} (${payload.status})`) +}) + +emitter.on("tool_call_update", (payload) => { + console.log(` → ${payload.toolCallId}: ${payload.status}`) +}) + +emitter.on("error", (err) => { + console.error("Session error:", err) +}) +``` + +The emitter supports `on`, `once`, `off`, and `removeAllListeners`. + +### Permission Handling + +When the agent wants to execute a tool (edit a file, run a command, etc.), it requests permission. You **must** set a permission handler or all tool calls will be auto-rejected. + +```typescript +agent.setPermissionHandler((request, resolve) => { + // request.toolCall — details about what the agent wants to do + // request.options — available choices (allow_once, reject_once, etc.) + + console.log(`Permission requested: ${request.toolCall.title}`) + console.log("Options:", request.options.map(o => `${o.optionId} (${o.kind})`)) + + // Auto-approve everything: + const allowOption = request.options.find(o => o.kind === "allow_once") + if (allowOption) { + resolve({ outcome: { outcome: "selected", optionId: allowOption.optionId } }) + } else { + resolve({ outcome: { outcome: "rejected" } }) + } +}) +``` + +#### Permission Options + +Each permission request includes an array of `PermissionOption` objects: + +| `kind` | Meaning | +|--------|---------| +| `allow_once` | Approve this single operation | +| `allow_always` | Approve and remember for future operations | +| `reject_once` | Deny this single operation | +| `reject_always` | Deny and remember for future operations | + +**Important:** If no permission handler is set, all tool calls are rejected for safety. + +### Modes + +Cline supports two modes: + +- **`plan`** — The agent gathers information and creates a plan without executing actions +- **`act`** — The agent executes actions (file edits, commands, etc.) + +```typescript +// Switch to plan mode +await agent.setSessionMode({ sessionId, modeId: "plan" }) + +// Switch back to act mode +await agent.setSessionMode({ sessionId, modeId: "act" }) +``` + +The current mode is returned in `newSession()` and emitted via `current_mode_update` events. + +### Model Selection + +Change the backing model with `unstable_setSessionModel()`. The model ID format is `"provider/modelId"`. + +```typescript +await agent.unstable_setSessionModel({ + sessionId, + modelId: "anthropic/claude-sonnet-4-20250514", +}) +``` + +This sets the model for both plan and act modes. Available providers include `anthropic`, `openai-native`, `gemini`, `bedrock`, `deepseek`, `mistral`, `groq`, `xai`, and others. + +> **Note:** This API is experimental and may change. + +### Authentication + +The SDK supports two OAuth flows: + +```typescript +// Cline account (uses browser OAuth) +await agent.authenticate({ methodId: "cline-oauth" }) + +// OpenAI Codex / ChatGPT subscription +await agent.authenticate({ methodId: "openai-codex-oauth" }) +``` + +Both methods open a browser window for the OAuth flow and block until authentication completes (5-minute timeout for Cline OAuth). + +For BYO (bring-your-own) API key providers, configure the key through the state manager before creating a session. The `authenticate()` call is not needed for BYO providers. + +### Cancellation + +Cancel an in-progress prompt turn: + +```typescript +await agent.cancel({ sessionId }) +``` + +The pending `prompt()` call will resolve with `{ stopReason: "cancelled" }`. + +## API Reference + +### Constructor + +```typescript +new ClineAgent(options: ClineAgentOptions) +``` + +```typescript +interface ClineAgentOptions { + /** Version string for your application (required) */ + version: string + /** Enable debug logging (default: false) */ + debug?: boolean + /** Custom Cline config directory (default: ~/.cline) */ + clineDir?: string +} +``` + +The `clineDir` option lets you isolate configuration and task history per-application: + +```typescript +const agent = new ClineAgent({ + version: "1.0.0", + clineDir: "/tmp/my-app-cline", +}) +``` + +### Methods + +#### `initialize(params): Promise` + +Initialize the agent and negotiate protocol capabilities. + +```typescript +const response = await agent.initialize({ + clientCapabilities: {}, + protocolVersion: 1, +}) + +// Response includes: +{ + protocolVersion: "0.9.0", + agentCapabilities: { + loadSession: true, + promptCapabilities: { image: true, audio: false, embeddedContext: true }, + mcpCapabilities: { http: true, sse: false } + }, + agentInfo: { name: "cline", version: "2.2.3" }, + authMethods: [ + { id: "cline-oauth", name: "Sign in with Cline", description: "..." }, + { id: "openai-codex-oauth", name: "Sign in with ChatGPT", description: "..." } + ] +} +``` + +#### `newSession(params): Promise` + +Create a new conversation session. + +```typescript +const session = await agent.newSession({ + cwd: "/path/to/project", + mcpServers: [ + { + type: "stdio", + name: "filesystem", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], + env: {}, + }, + ], +}) + +// Response includes: +{ + sessionId: "uuid-string", + modes: { + availableModes: [ + { id: "plan", name: "Plan", description: "Gather information and create a detailed plan" }, + { id: "act", name: "Act", description: "Execute actions to accomplish the task" } + ], + currentModeId: "act" + }, + models: { + currentModelId: "anthropic/claude-sonnet-4-5-20241022", + availableModels: [{ modelId: "anthropic/claude-3-5-sonnet-20241022", name: "..." }] + } +} +``` + +> **Note:** `newSession()` may throw an auth-required error if credentials are not configured yet. + +#### `prompt(params): Promise` + +Send a user prompt to the agent. This is the main method for interacting with Cline. Blocks until the agent finishes its turn. + +```typescript +const response = await agent.prompt({ + sessionId: session.sessionId, + prompt: [ + { type: "text", text: "Create a function that adds two numbers" }, + ], +}) + +// Response: { stopReason: "end_turn" | "max_tokens" | "cancelled" | "error" } +``` + +#### `cancel(params): Promise` + +Cancel an ongoing prompt operation. + +```typescript +await agent.cancel({ sessionId: session.sessionId }) +``` + +#### `setSessionMode(params): Promise` + +Switch between plan and act modes. + +```typescript +await agent.setSessionMode({ sessionId, modeId: "plan" }) +``` + +#### `unstable_setSessionModel(params): Promise` + +Change the model for the session. Model ID format: `"provider/modelId"`. + +```typescript +await agent.unstable_setSessionModel({ + sessionId, + modelId: "anthropic/claude-sonnet-4-20250514", +}) +``` + +#### `authenticate(params): Promise` + +Authenticate with a provider. Opens a browser window for OAuth flow. + +```typescript +await agent.authenticate({ methodId: "cline-oauth" }) +``` + +#### `shutdown(): Promise` + +Clean up all resources. Call this when done. + +```typescript +await agent.shutdown() +``` + +#### `setPermissionHandler(handler)` + +Set a callback to handle tool permission requests. + +```typescript +agent.setPermissionHandler((request, resolve) => { + resolve({ outcome: { outcome: "selected", optionId: "allow_once" } }) +}) +``` + +#### `emitterForSession(sessionId): ClineSessionEmitter` + +Get the typed event emitter for a session. + +```typescript +const emitter = agent.emitterForSession(session.sessionId) +``` + +#### `sessions` (read-only Map) + +Access active sessions: + +```typescript +for (const [sessionId, session] of agent.sessions) { + console.log(sessionId, session.cwd, session.mode) +} +``` + +## Full Example: Auto-Approve Agent + +```typescript +import { ClineAgent } from "cline" + +async function runTask(task: string, cwd: string) { + const agent = new ClineAgent({ version: "1.0.0" }) + + await agent.initialize({ + protocolVersion: 1, + clientCapabilities: {}, + }) + + const { sessionId } = await agent.newSession({ cwd, mcpServers: [] }) + + // Auto-approve all tool calls + agent.setPermissionHandler((request, resolve) => { + const allow = request.options.find(o => o.kind === "allow_once") + resolve({ + outcome: allow + ? { outcome: "selected", optionId: allow.optionId } + : { outcome: "rejected" }, + }) + }) + + // Collect output + const output: string[] = [] + const emitter = agent.emitterForSession(sessionId) + + emitter.on("agent_message_chunk", (p) => { + if (p.content.type === "text") output.push(p.content.text) + }) + + emitter.on("tool_call", (p) => { + console.log(`[tool] ${p.title}`) + }) + + const { stopReason } = await agent.prompt({ + sessionId, + prompt: [{ type: "text", text: task }], + }) + + console.log("\n--- Agent Output ---") + console.log(output.join("")) + console.log(`\nStop reason: ${stopReason}`) + + await agent.shutdown() +} + +runTask("Create a README.md for this project", process.cwd()) +``` + +## Full Example: Interactive Permission Flow + +```typescript +import { ClineAgent, type PermissionHandler } from "cline" +import * as readline from "readline" + +const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) +const ask = (q: string) => new Promise((res) => rl.question(q, res)) + +const interactivePermissions: PermissionHandler = async (request, resolve) => { + console.log(`\nāš ļø Permission: ${request.toolCall.title}`) + + for (const [i, opt] of request.options.entries()) { + console.log(` ${i + 1}. [${opt.kind}] ${opt.name}`) + } + + const choice = await ask("Choose (number): ") + const idx = parseInt(choice, 10) - 1 + const selected = request.options[idx] + + if (selected) { + resolve({ outcome: { outcome: "selected", optionId: selected.optionId } }) + } else { + resolve({ outcome: { outcome: "rejected" } }) + } +} + +async function main() { + const agent = new ClineAgent({ version: "1.0.0" }) + await agent.initialize({ protocolVersion: 1, clientCapabilities: {} }) + + const { sessionId } = await agent.newSession({ + cwd: process.cwd(), + mcpServers: [], + }) + + agent.setPermissionHandler(interactivePermissions) + + const emitter = agent.emitterForSession(sessionId) + emitter.on("agent_message_chunk", (p) => { + if (p.content.type === "text") process.stdout.write(p.content.text) + }) + + // Multi-turn conversation + while (true) { + const userInput = await ask("\n> ") + if (userInput === "exit") break + + const { stopReason } = await agent.prompt({ + sessionId, + prompt: [{ type: "text", text: userInput }], + }) + + console.log(`\n[${stopReason}]`) + } + + await agent.shutdown() + rl.close() +} + +main() +``` + +## Exported Types + +All types are re-exported from the `cline` package. Key types: + +| Type | Description | +|------|-------------| +| `ClineAgent` | Main agent class | +| `ClineSessionEmitter` | Typed event emitter for session events | +| `ClineAgentOptions` | Constructor options | +| `ClineAcpSession` | Session metadata (read-only) | +| `ClineSessionEvents` | Event name → handler signature map | +| `PermissionHandler` | `(request, resolve) => void` callback | +| `PermissionResolver` | `(response) => void` callback | +| `SessionUpdate` | Union of all session update types | +| `SessionUpdateType` | Discriminator values (`"agent_message_chunk"`, `"tool_call"`, etc.) | +| `ToolCall` | Tool call details (id, title, kind, status, content) | +| `ToolCallUpdate` | Partial update to an existing tool call | +| `ToolCallStatus` | `"pending" \| "in_progress" \| "completed" \| "failed"` | +| `ToolKind` | `"read" \| "edit" \| "delete" \| "execute" \| "search" \| ...` | +| `StopReason` | `"end_turn" \| "cancelled" \| "error" \| "max_tokens" \| ...` | +| `ContentBlock` | `TextContent \| ImageContent \| AudioContent \| ...` | +| `McpServer` | MCP server configuration (stdio, http) | +| `PromptRequest` / `PromptResponse` | Prompt call types | +| `NewSessionRequest` / `NewSessionResponse` | Session creation types | +| `InitializeRequest` / `InitializeResponse` | Initialization types | + +See the [ACP Schema](https://agentclientprotocol.com/protocol/schema) for the full type definitions. + +## Relationship to ACP + +The Cline SDK implements the [Agent Client Protocol](https://agentclientprotocol.com) `Agent` interface. The key difference from a standard ACP stdio agent is that the SDK uses an **event emitter pattern** instead of a transport connection: + +| ACP Stdio (via `AcpAgent`) | SDK (via `ClineAgent`) | +|-----------------------------|------------------------| +| Session updates sent over JSON-RPC stdio | Session updates emitted via `ClineSessionEmitter` | +| Permissions requested via `connection.requestPermission()` | Permissions requested via `setPermissionHandler()` callback | +| Single process, single connection | Embeddable, multiple concurrent sessions | + +If you need stdio-based ACP communication (e.g., for IDE integration), use the `cline` CLI binary directly. The SDK is for embedding Cline in your own Node.js processes.