Files
2026-07-13 12:52:40 +08:00

477 lines
18 KiB
Markdown

# 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](./DOC.md) and [README.md](./README.md).
## Prerequisites
Install these before starting:
1. [Bun](https://bun.sh) (v1.0.0+) - Package manager, runtime, and bundler
2. [Zig](https://ziglang.org/download/) - 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
| 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:
```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
- 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 system
- `opentui-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:
```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](./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.