CLI Development Guide
June 4, 2026 ยท View on GitHub
This guide covers everything you need to build and run the Cline CLI locally after cloning the repository. It includes setup instructions, a tech stack overview, and a walkthrough of the TUI architecture.
For CLI command reference and usage, see DOC.md and README.md.
Prerequisites
Install these before starting:
- Bun (v1.0.0+) - Package manager, runtime, and bundler
- Zig - Required by OpenTUI's native core. The
@opentui/corepackage includes a Zig-compiled native binary that builds from source on install. Without Zig,bun installwill fail for OpenTUI packages. - Node.js 22+ - Required for some build tooling and test infrastructure
Verify your setup:
bun --version # should be >= 1.0.0
zig version # any recent stable release
node --version # should be >= 22
First-Time Setup
From the repository root:
# Install all workspace dependencies (including native OpenTUI build)
bun install
# Build the SDK packages and CLI
bun run build
# Run the CLI in dev mode (interactive)
bun run cli
That last command is a shortcut for cd apps/cli && bun run dev, which runs:
CLINE_BUILD_ENV=development bun --conditions=development ./src/index.ts
Linking for Global Access
To use the CLI from anywhere on your system, first build the SDK packages, then link:
# From the repo root -- build all workspace packages
bun run build:sdk
# Then link the CLI binary
cd apps/cli
bun link
The build:sdk step is required because bun link runs without the --conditions=development flag, so Bun resolves workspace packages (@cline/llms, @cline/core, etc.) via their package.json exports which point to dist/. Without the build, those dist files don't exist and you'll get "Cannot find module" errors.
After linking, you can run cline from any directory:
cline # interactive mode
cline "prompt" # single-prompt mode
cline auth # authenticate a provider
If you prefer to skip the build step, use bun run dev from apps/cli/ instead -- it passes --conditions=development which resolves packages directly from source.
Rebuilding After SDK Changes
If you modify any package in packages/ (shared, llms, agents, core, etc.), rebuild the SDK:
bun run build:sdk
If you're using bun run dev, you don't need to rebuild after every SDK change -- dev mode resolves packages from source. But if you're using the linked cline binary, you do need to rebuild for changes to take effect.
Monorepo Structure
cline-sdk/
packages/ # SDK packages (published to npm)
shared/ # Contracts, schemas, path helpers, runtime utilities
llms/ # Provider settings, model catalogs, AI SDK handlers
agents/ # Stateless agent loop, tool orchestration, hooks
scheduler/ # Scheduled execution, concurrency control
core/ # Stateful orchestration, sessions, hub, storage, config
enterprise/ # Internal enterprise integrations (not published)
apps/
cli/ # This package - CLI host and TUI
code/ # Tauri + Next.js desktop app
vscode/ # VS Code extension
desktop/ # Desktop application
examples/ # Sample integrations
biome.json # Linter and formatter config (Biome)
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | Bun | Package management, script execution, bundling |
| Language | TypeScript (strict) | All source code |
| CLI Framework | Commander.js | Argument parsing, subcommands |
| TUI Renderer | OpenTUI (@opentui/core) | Native terminal rendering engine (Zig + C ABI) |
| TUI Components | OpenTUI React (@opentui/react) | React 19 reconciler for declarative terminal UI |
| TUI Dialogs | @opentui-ui/dialog | Modal dialog system (model picker, tool approval, etc.) |
| Linter/Formatter | Biome | Code quality and formatting |
| Testing | Vitest | Unit and E2E tests |
| Logging | Pino | Runtime file logging |
Why OpenTUI?
OpenTUI is a native terminal UI core written in Zig with TypeScript bindings. Compared to the previous terminal renderer, OpenTUI provides:
- Native diff rendering with syntax highlighting
- Streaming markdown rendering
- Scrollable content areas
- Mouse interaction (click, hover, drag-to-select, scroll)
- Built-in clipboard support (OSC52)
- Higher performance through native rendering
OpenTUI exposes a C ABI from its Zig core. The @opentui/core package provides TypeScript bindings, and @opentui/react provides a React reconciler so you can write terminal UIs with JSX.
CLI Source Structure
apps/cli/src/
index.ts # Entry point (shebang, signal handling)
main.ts # CLI command definitions, argument parsing
runtime/
run-interactive.ts # Interactive mode runtime (session lifecycle, event wiring)
run-agent.ts # Single-prompt runtime
session-events.ts # Event bridge types and pub/sub
active-runtime.ts # Abort registry
tool-policies.ts # Auto-approve toggle logic
prompt.ts # System prompt and user input assembly
defaults.ts # Default config values
tui/ # Terminal UI (OpenTUI + React)
index.tsx # Renderer entry point
root.tsx # Provider tree, view routing, global keyboard
types.ts # ChatEntry union, TuiProps, shared constants
interactive-config.ts # Config data loading
interactive-welcome.ts # Welcome line, slash command resolution
components/ # Reusable UI components
contexts/ # React context providers
hooks/ # Custom React hooks
views/ # Full-screen view components
utils/ # TUI-specific utilities
session/ # Session state management
commands/ # CLI subcommands (auth, config, history, etc.)
connectors/ # Chat adapter bridges (Telegram, Slack, etc.)
utils/ # Shared utilities
wizards/ # Interactive setup flows
logging/ # Pino logger adapter
TUI Architecture
The TUI lives at src/tui/ and uses React with OpenTUI's reconciler. Every .tsx file in this directory uses a per-file JSX pragma:
// @jsxImportSource @opentui/react
This tells TypeScript to use OpenTUI's JSX runtime instead of React DOM. The tsconfig.json sets jsxImportSource: "@opentui/react" globally, but the per-file pragma makes the intent explicit and avoids conflicts with any non-TUI React code.
Entry Point: index.tsx
The TUI boots through renderOpenTui():
const renderer = await createCliRenderer({
exitOnCtrlC: false, // We handle Ctrl+C ourselves
autoFocus: false, // Prevents click-anywhere from stealing focus
enableMouseMovement: true,
});
const root = createRoot(renderer);
root.render(<Root {...props} />);
The renderer returns destroy() and waitUntilExit() methods. The runtime calls destroy() on exit and awaits waitUntilExit() for cleanup.
Runtime Bridge: run-interactive.ts
This file is the bridge between the SDK and the TUI. It:
- Creates a
SessionManagerviacreateCliCore() - Sets up event subscriptions (agent events, pending prompts, team events)
- Passes callbacks to the TUI as props (
onSubmit,onAbort,onModelChange, etc.) - Manages session lifecycle (start, stop, restart, resume, compact)
The TUI never talks to the SDK directly. All communication flows through the callback props defined in TuiProps (see types.ts).
Component Tree
Root (root.tsx)
DialogProvider # Modal dialog system
SessionProvider # Chat entries, running state, mode
EventBridgeProvider # Subscribes to SDK events
View Router
HomeView # Welcome screen (before first prompt)
ChatView # Message list + input bar + status
OnboardingView # First-run provider setup
ConfigView (dialog) # Settings browser
HistoryView (dialog) # Session history
Context Providers
Each context owns a slice of state. Components subscribe only to what they need.
SessionContext - Core chat state:
entries: ChatEntry[]- All messages in the conversationisRunning/abortRequested- Agent execution statemode(plan/act),autoApproveAll,hasSubmittedlastTotalTokens,lastTotalCost,turnStartTime
EventBridgeContext - SDK event subscription:
- Subscribes to
subscribeToEventsprop once via useEffect - Forwards agent events to session context handlers via stable refs
- Handles pending prompts, team events
Event Flow
SDK (AgentLoop)
--> AgentEvent emitted
--> subscribeToAgentEvents() fires
--> UIEventEmitter.emit("agent", event)
--> EventBridgeProvider receives event
--> useAgentEventHandlers processes event
--> SessionContext.entries updated
--> React re-renders affected components
ChatEntry Type
All messages in the conversation are represented as a discriminated union:
type ChatEntry =
| { kind: "user"; text: string }
| { kind: "assistant_text"; text: string; streaming: boolean }
| { kind: "reasoning"; text: string; streaming: boolean }
| { kind: "tool_call"; toolName: string; inputSummary: string; ... }
| { kind: "error"; text: string }
| { kind: "status"; text: string }
| { kind: "team"; text: string }
| { kind: "user_submitted"; text: string; delivery?: "queue" | "steer" }
| { kind: "done"; tokens: number; cost: number; elapsed: string; iterations: number }
Dialog System
Dialogs use @opentui-ui/dialog. The pattern:
import { useDialog } from "@opentui-ui/dialog/react";
const dialog = useDialog();
const result = await dialog.choice<string>({
style: { maxHeight: termHeight - 2 },
content: (ctx) => <MyDialogContent {...ctx} />,
});
Dialog content components receive resolve and dismiss callbacks through the context. They use useDialogKeyboard for keyboard handling scoped to the dialog.
Important gotcha: async data loading inside a dialog (via useEffect/useState) causes layout gaps between flex children in OpenTUI. Always fetch data before opening the dialog and pass it as props.
Key Components
components/input-bar.tsx - Text input with submit handling:
- Uncontrolled
<textarea>withkey={inputKey}for reset refcallback wiresnode.onSubmit(React reconciler pattern)- Supports newlines (Shift+Enter) and autocomplete integration
components/chat-entry.tsx - Renders a single ChatEntry based on its kind:
- Markdown rendering for assistant text (
<markdown>) - Diff rendering for file edits (
<diff>) - Code highlighting for file reads (
<code>) - Spinner for streaming states
components/status-bar.tsx - Bottom status display:
- Model name, context bar, token/cost
- Plan/Act mode indicator
- Workspace, branch, auto-approve state
components/tool-output.tsx - Rich tool result rendering:
- Unified diffs with syntax highlighting
- Expandable/collapsible output sections
- File read with line numbers
views/home-view.tsx - Welcome screen with animated robot and centered input
views/chat-view.tsx - Main conversation view (scrollbox + input + status)
views/onboarding-view.tsx - First-run provider/model setup wizard
OpenTUI Elements
OpenTUI provides these built-in elements (used like HTML tags in JSX):
<box>- Flexbox container (like<div>)<text>- Text display (like<span>)<span>- Inline text modifier (for coloring nested text)<scrollbox>- Scrollable container<textarea>- Multi-line text input<input>- Single-line text input<select>- List selection<code>- Syntax-highlighted code block<diff>- Unified/split diff viewer<markdown>- Streaming markdown renderer
Styling uses named terminal colors as props:
<text fg="cyan">colored text</text>
<box backgroundColor="gray" paddingX={1}>padded box</box>
Layout follows flexbox conventions: flexDirection, flexGrow, flexShrink, gap, padding, margin, etc.
Testing
# Unit tests
bun run test:unit
# E2E tests
bun run test:e2e
bun run test:e2e:interactive
# TUI-specific E2E tests (uses @microsoft/tui-test)
bun run test:e2e:cli:tui
# Type checking
bun run typecheck
# Lint and format
cd ../.. && bun run fix # auto-fix from repo root
Common Development Tasks
Running in interactive mode
bun run dev
Testing onboarding flow
Use a temporary config directory to simulate a fresh install:
bun run dev -- --interactive --config /tmp/cline-test
Or set CLINE_FORCE_ONBOARDING=1 to force the onboarding view regardless of existing config.
Adding a new TUI component
- Create a
.tsxfile insrc/tui/components/ - Add the JSX pragma at the top:
// @jsxImportSource @opentui/react - Use OpenTUI elements (
<box>,<text>, etc.) for layout - Import and use in the parent view or root
Adding a new dialog
- Create a content component that receives
ChoiceContext<T>props - Use
useDialogKeyboardfor keyboard handling - Call
resolve(value)to return a result,dismiss()to cancel - Open it from a hook or view:
const result = await dialog.choice<T>({ content: ... }) - Fetch any async data before calling
dialog.choice(), not inside the dialog
Adding a new slash command
- Define the command handler in
root.tsx(in the slash command processing section) - Add the command to the help dialog in
components/dialogs/help-dialog.tsx - Add autocomplete entry in
hooks/use-autocomplete.ts
Debugging the TUI
# Run with React DevTools (requires react-devtools-core@7)
DEV=true bun run dev
# In another terminal
npx react-devtools@7
Debugging the CLI process
cd apps/cli
CLINE_BUILD_ENV=development bun --conditions=development --inspect-brk=6499 ./src/index.ts
Then attach VS Code or Chrome DevTools to ws://127.0.0.1:6499.
OpenTUI Resources
- OpenTUI docs: https://opentui.com/docs/getting-started
- Repository: https://github.com/anomalyco/opentui
- Packages used by CLI:
@opentui/core- Native renderer and built-in elements@opentui/react- React reconciler (createRoot, hooks)@opentui-ui/dialog- Dialog/modal systemopentui-spinner- Spinner component
Publishing
The CLI is published as the cline wrapper package on npm with platform-specific binaries under @cline/cli-*. The release flow lives in the publish-cli skill (.cline/skills/publish-cli/SKILL.md at the repo root).
From the apps/cli workspace:
# Dry run for checking package size and build output
bun publish --dry-run
# Publish to npm (version bump required first)
bun run release
See DISTRIBUTION.md for details on how the CLI is packaged.
Runtime ownership
- CLI renders runtime events and handles terminal UX.
- Core owns agent creation, runtime composition, and session message persistence.
- CLI does not directly instantiate
Agentfor chat/task execution. - CLI does not perform direct file/db message persistence in run/interactive paths.
- CLI owns the user-instruction watcher (rules/workflows/skills) because prompt assembly uses rule context before session start; the watcher is disposed on all exit paths.
- RPC runtime uses the same prompt resolver and accepts optional
rulesin runtime config (orsystemPromptwhen fully prebuilt by the caller).
Connector runtime behavior
- Telegram final assistant replies are sent through Telegram entity payloads with raw-text fallback; Google Chat and WhatsApp use the shared connector runtime formatting path.
- Assistant text streams incrementally into chat surfaces that use the shared runtime streaming path; Telegram sends final assistant replies after the turn completes.
- Tool activity is summarized as compact start/error messages with short argument previews.
- Required tool approvals are posted back into the chat thread and accept
Y/Nreplies. - Google Chat serves its webhook at
/api/webhooks/gchat; configure the Google Chat App URL as<base-url>/api/webhooks/gchat. - Webhook-based connectors are hosted through a shared CLI
node:httpserver helper rather thanBun.serve. - WhatsApp serves its webhook at
/api/webhooks/whatsapp; configure the Meta callback URL as<base-url>/api/webhooks/whatsapp.
Logging adapter
cline uses a pino-backed adapter that targets the core BasicLogger contract:
- CLI runtime passes
loggerdirectly into local@cline/coresessions. - Hub-backed sessions include a serialized logger payload in
ChatStartSessionRequest.logger; the runtime reconstructs the samepinosettings and injects them into core. - Hosts can attach stable runtime logger bindings (for example
clientId,clientType,clientApp) throughRuntimeLoggerConfig.bindings.
After login, OAuth credentials are persisted with auth.expiresAt, and @cline/core refreshes these tokens automatically during session turns. Provider auth and model settings should be changed through cline auth, the interactive config UI, or core provider-settings APIs rather than editing provider settings files directly.
On startup, cline also attempts a legacy settings import:
- Source files:
<CLINE_DATA_DIR>/globalState.jsonand<CLINE_DATA_DIR>/secrets.json - Target file:
<CLINE_DATA_DIR>/settings/providers.json(orCLINE_PROVIDER_SETTINGS_PATH) - Existing providers in
providers.jsonare never overwritten - Missing providers discovered in legacy files are merged into
providers.json - Migrated provider entries are annotated with
tokenSource: "migration"
Custom provider registry notes:
- Provider runtime settings continue to persist in
<CLINE_DATA_DIR>/settings/providers.json. - Providers in
providers.jsoncan opt into the OpenAI Responses API with"protocol": "openai-responses"; this routes the runtime through the OpenAI client while keeping the user-defined provider ID, base URL, and model catalog. - User-added OpenAI-compatible provider model catalogs are persisted in
<CLINE_DATA_DIR>/settings/models.json(or alongsideCLINE_PROVIDER_SETTINGS_PATH). models.jsonstores model lists by provider ID and is loaded by the runtime provider actions.- Entries with only
modelsextend an existing provider; entries withprovidermetadata register or override a custom provider.