sdk/apps/cli/DEVELOPMENT.md
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.
Install these before starting:
@opentui/core package includes a Zig-compiled native binary that builds from source on install. Without Zig, bun install will fail for OpenTUI packages.Verify your setup:
bun --version # should be >= 1.0.0
zig version # any recent stable release
node --version # should be >= 22
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
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.
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.
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)
| 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 |
OpenTUI is a native terminal UI core written in Zig with TypeScript bindings. Compared to the previous terminal renderer, OpenTUI provides:
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.
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
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.
index.tsxThe 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.
run-interactive.tsThis file is the bridge between the SDK and the TUI. It:
SessionManager via createCliCore()onSubmit, onAbort, onModelChange, etc.)The TUI never talks to the SDK directly. All communication flows through the callback props defined in TuiProps (see types.ts).
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
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, turnStartTimeEventBridgeContext - SDK event subscription:
subscribeToEvents prop once via useEffectSDK (AgentLoop)
--> AgentEvent emitted
--> subscribeToAgentEvents() fires
--> UIEventEmitter.emit("agent", event)
--> EventBridgeProvider receives event
--> useAgentEventHandlers processes event
--> SessionContext.entries updated
--> React re-renders affected components
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 }
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.
components/input-bar.tsx - Text input with submit handling:
<textarea> with key={inputKey} for resetref callback wires node.onSubmit (React reconciler pattern)components/chat-entry.tsx - Renders a single ChatEntry based on its kind:
<markdown>)<diff>)<code>)components/status-bar.tsx - Bottom status display:
components/tool-output.tsx - Rich tool result rendering:
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 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 rendererStyling 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.
# 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
bun run dev
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.
.tsx file in src/tui/components/// @jsxImportSource @opentui/react<box>, <text>, etc.) for layoutChoiceContext<T> propsuseDialogKeyboard for keyboard handlingresolve(value) to return a result, dismiss() to cancelconst result = await dialog.choice<T>({ content: ... })dialog.choice(), not inside the dialogroot.tsx (in the slash command processing section)components/dialogs/help-dialog.tsxhooks/use-autocomplete.ts# Run with React DevTools (requires react-devtools-core@7)
DEV=true bun run dev
# In another terminal
npx react-devtools@7
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/core - Native renderer and built-in elements@opentui/react - React reconciler (createRoot, hooks)@opentui-ui/dialog - Dialog/modal systemopentui-spinner - Spinner componentThe 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 (sdk/apps/cli/.cline/skills/publish-cli/SKILL.md).
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.
Agent for chat/task execution.rules in runtime config (or systemPrompt when fully prebuilt by the caller).Y / N replies./api/webhooks/gchat; configure the Google Chat App URL as <base-url>/api/webhooks/gchat.node:http server helper rather than Bun.serve./api/webhooks/whatsapp; configure the Meta callback URL as <base-url>/api/webhooks/whatsapp.cline uses a pino-backed adapter that targets the core BasicLogger contract:
logger directly into local @cline/core sessions.ChatStartSessionRequest.logger; the runtime reconstructs the same pino settings and injects them into core.clientId, clientType, clientApp) through RuntimeLoggerConfig.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:
<CLINE_DATA_DIR>/globalState.json and <CLINE_DATA_DIR>/secrets.json<CLINE_DATA_DIR>/settings/providers.json (or CLINE_PROVIDER_SETTINGS_PATH)providers.json are never overwrittenproviders.jsontokenSource: "migration"Custom provider registry notes:
<CLINE_DATA_DIR>/settings/providers.json.providers.json can 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.<CLINE_DATA_DIR>/settings/models.json (or alongside CLINE_PROVIDER_SETTINGS_PATH).models.json stores model lists by provider ID and is loaded by the runtime provider actions.models extend an existing provider; entries with provider metadata register or override a custom provider.