Back to Cline

CLI Development Guide

sdk/apps/cli/DEVELOPMENT.md

3.83.018.4 KB
Original Source

CLI Development Guide

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:

  1. Bun (v1.0.0+) - Package manager, runtime, and bundler
  2. Zig - Required by OpenTUI's native core. The @opentui/core package includes a Zig-compiled native binary that builds from source on install. Without Zig, bun install will fail for OpenTUI packages.
  3. Node.js 22+ - Required for some build tooling and test infrastructure

Verify your setup:

bash
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:

bash
# 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:

bash
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:

bash
# 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:

bash
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:

bash
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

LayerTechnologyPurpose
RuntimeBunPackage management, script execution, bundling
LanguageTypeScript (strict)All source code
CLI FrameworkCommander.jsArgument parsing, subcommands
TUI RendererOpenTUI (@opentui/core)Native terminal rendering engine (Zig + C ABI)
TUI ComponentsOpenTUI React (@opentui/react)React 19 reconciler for declarative terminal UI
TUI Dialogs@opentui-ui/dialogModal dialog system (model picker, tool approval, etc.)
Linter/FormatterBiomeCode quality and formatting
TestingVitestUnit and E2E tests
LoggingPinoRuntime 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:

tsx
// @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():

tsx
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:

  1. Creates a SessionManager via createCliCore()
  2. Sets up event subscriptions (agent events, pending prompts, team events)
  3. Passes callbacks to the TUI as props (onSubmit, onAbort, onModelChange, etc.)
  4. 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 conversation
  • isRunning / abortRequested - Agent execution state
  • mode (plan/act), autoApproveAll, hasSubmitted
  • lastTotalTokens, lastTotalCost, turnStartTime

EventBridgeContext - SDK event subscription:

  • Subscribes to subscribeToEvents prop 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:

typescript
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:

tsx
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> with key={inputKey} for reset
  • ref callback wires node.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:

tsx
<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

bash
# 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

bash
bun run dev

Testing onboarding flow

Use a temporary config directory to simulate a fresh install:

bash
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

  1. Create a .tsx file in src/tui/components/
  2. Add the JSX pragma at the top: // @jsxImportSource @opentui/react
  3. Use OpenTUI elements (<box>, <text>, etc.) for layout
  4. Import and use in the parent view or root

Adding a new dialog

  1. Create a content component that receives ChoiceContext<T> props
  2. Use useDialogKeyboard for keyboard handling
  3. Call resolve(value) to return a result, dismiss() to cancel
  4. Open it from a hook or view: const result = await dialog.choice<T>({ content: ... })
  5. Fetch any async data before calling dialog.choice(), not inside the dialog

Adding a new slash command

  1. Define the command handler in root.tsx (in the slash command processing section)
  2. Add the command to the help dialog in components/dialogs/help-dialog.tsx
  3. Add autocomplete entry in hooks/use-autocomplete.ts

Debugging the TUI

bash
# 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

bash
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

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 (sdk/apps/cli/.cline/skills/publish-cli/SKILL.md).

From the apps/cli workspace:

bash
# 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 Agent for 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 rules in runtime config (or systemPrompt when 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 / N replies.
  • 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:http server helper rather than Bun.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 logger directly into local @cline/core sessions.
  • Hub-backed sessions include a serialized logger payload in ChatStartSessionRequest.logger; the runtime reconstructs the same pino settings and injects them into core.
  • Hosts can attach stable runtime logger bindings (for example 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:

  • Source files: <CLINE_DATA_DIR>/globalState.json and <CLINE_DATA_DIR>/secrets.json
  • Target file: <CLINE_DATA_DIR>/settings/providers.json (or CLINE_PROVIDER_SETTINGS_PATH)
  • Existing providers in providers.json are 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.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.
  • User-added OpenAI-compatible provider model catalogs are persisted in <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.
  • Entries with only models extend an existing provider; entries with provider metadata register or override a custom provider.