Compare commits

...
Author SHA1 Message Date
abeatrix 91127ffdb9 Update logger 2026-01-16 22:34:20 -08:00
abeatrix 11f76f0218 add cline sign in 2026-01-16 22:08:03 -08:00
abeatrix 74f1b9c75d **feat(cli): allow switching mode and specifying model for tasks**
Added `--switch` (`-s`) and `--model` (`-m`) options to the CLI.
Updated `runTask` signature and logic to set global state for mode (plan/act) and the corresponding API model ID.
This enables users to run tasks in different modes or with a specific model directly from the command line.
2026-01-16 18:46:55 -08:00
abeatrix 942c78119e working prototype
npm install:all
cd cli-ts
npm run link
clinedev auth
2026-01-16 18:36:47 -08:00
abeatrix 3130c3c96e replace console.log with Logger 2026-01-14 13:08:50 -08:00
abeatrix f0225bc20d fileeditprovider 2026-01-14 13:08:28 -08:00
abeatrix 1bf158245c prototype: Cline CLI with Typescript
TODO:
- remove usage of console.log across codebase and replace them with Logger
2026-01-13 16:55:36 -08:00
54 changed files with 4625 additions and 309 deletions
+183
View File
@@ -0,0 +1,183 @@
# Cline CLI (TypeScript)
A TypeScript CLI implementation of Cline that reuses the core TypeScript codebase. This allows you to run Cline tasks directly from the terminal while sharing the same underlying functionality as the VS Code extension.
## Features
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
- **Task History**: Access your task history from the command line
- **Configurable**: Use custom configuration directories and working directories
## Prerequisites
- Node.js 20.x or later
- npm or yarn
- The parent Cline project dependencies installed
## Installation
From the repository root:
```bash
# Install root dependencies first
npm install
# Build the CLI
npm run compile-cli-ts
```
Or install the CLI globally:
```bash
cd cli-ts
npm install
npm run build
npm link
```
## Usage
### Run a Task
```bash
# Run a task with a prompt
cline-ts task "Create a hello world function in Python"
# Or use the shorthand
cline-ts t "Create a hello world function"
# Run directly without the 'task' command
cline-ts "Create a hello world function"
```
### Options
```bash
# Show verbose output (including reasoning)
cline-ts task -v "Your prompt"
# Specify working directory
cline-ts task -c /path/to/project "Your prompt"
# Use custom config directory
cline-ts task --config ~/.my-cline "Your prompt"
```
### View Task History
```bash
# List recent tasks
cline-ts history
# Show more tasks
cline-ts history -n 20
```
### Show Configuration
```bash
cline-ts config
```
## Development
```bash
# Build and link the package to your terminal
npm run link
# Set your provider (No Cline provider support yet)
clinedev auth
# Run a task
clinedev task "Tell me about this codebase"
```
### Build
```bash
# Development build with source maps
npm run build
# Production build (minified)
npm run build:production
```
### Watch Mode
```bash
npm run watch
```
### Type Checking
```bash
npm run typecheck
```
## Architecture
The CLI reuses the core Cline TypeScript codebase:
- **Controller** (`@core/controller`): Manages task lifecycle and state
- **Task** (`@core/task`): Executes Cline tasks using the AI API
- **StateManager** (`@core/storage`): Handles persistent state storage
CLI-specific implementations:
- `cli-host-bridge.ts`: CLI implementations of host bridge services
- `cli-webview-provider.ts`: WebviewProvider that outputs to terminal
- `cli-diff-provider.ts`: DiffViewProvider for terminal diff display
- `vscode-context.ts`: Mock VSCode extension context
- `display.ts`: Terminal output formatting utilities
## Configuration
The CLI stores its data in `~/.cline/data/` by default:
- `globalState.json`: Global settings and state
- `secrets.json`: API keys and secrets
- `workspace/`: Workspace-specific state
- `tasks/`: Task history and conversation data
Override with the `--config` option or `CLINE_DIR` environment variable.
## Comparison with Go CLI
This TypeScript CLI differs from the Go CLI (`cli/` directory):
| Feature | Go CLI | TypeScript CLI |
|---------|--------|----------------|
| Language | Go | TypeScript |
| Core sharing | Uses gRPC to communicate | Direct imports |
| Startup time | Fast | Moderate |
| Dependencies | Standalone binary | Requires Node.js |
| Best for | Production deployment | Development, debugging |
Choose the TypeScript CLI when you need to debug or modify the core Cline logic. Choose the Go CLI for production deployment with faster startup.
## Troubleshooting
### Build Errors
If you encounter build errors, ensure you've:
1. Run `npm install` in the repository root
2. Run `npm run protos` to generate proto files
3. Have all peer dependencies installed
### Missing Dependencies
The CLI imports from the parent project. If you see import errors:
```bash
cd .. # Go to repository root
npm install
npm run protos
```
### Permission Denied
Make the CLI executable:
```bash
chmod +x dist/cli.js
```
+214
View File
@@ -0,0 +1,214 @@
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.resolve(__dirname, "..")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* Plugin to resolve path aliases from the parent project
* @type {import('esbuild').Plugin}
*/
const aliasResolverPlugin = {
name: "alias-resolver",
setup(build) {
const aliases = {
"@": path.resolve(rootDir, "src"),
"@core": path.resolve(rootDir, "src/core"),
"@integrations": path.resolve(rootDir, "src/integrations"),
"@services": path.resolve(rootDir, "src/services"),
"@shared": path.resolve(rootDir, "src/shared"),
"@utils": path.resolve(rootDir, "src/utils"),
"@packages": path.resolve(rootDir, "src/packages"),
"@hosts": path.resolve(rootDir, "src/hosts"),
"@generated": path.resolve(rootDir, "src/generated"),
"@api": path.resolve(rootDir, "src/core/api"),
}
// For each alias entry, create a resolver
Object.entries(aliases).forEach(([alias, aliasPath]) => {
const aliasRegex = new RegExp(`^${alias}($|/.*)`)
build.onResolve({ filter: aliasRegex }, (args) => {
const importPath = args.path.replace(alias, aliasPath)
// First, check if the path exists as is
if (fs.existsSync(importPath)) {
const stats = fs.statSync(importPath)
if (stats.isDirectory()) {
// If it's a directory, try to find index files
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const indexFile = path.join(importPath, `index${ext}`)
if (fs.existsSync(indexFile)) {
return { path: indexFile }
}
}
} else {
// It's a file that exists, so return it
return { path: importPath }
}
}
// If the path doesn't exist, try appending extensions
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const pathWithExtension = `${importPath}${ext}`
if (fs.existsSync(pathWithExtension)) {
return { path: pathWithExtension }
}
}
// If nothing worked, return the original path and let esbuild handle the error
return { path: importPath }
})
})
},
}
/**
* Plugin to redirect vscode imports to our shim
* @type {import('esbuild').Plugin}
*/
const vscodeStubPlugin = {
name: "vscode-stub",
setup(build) {
// Redirect 'vscode' imports to our shim
build.onResolve({ filter: /^vscode$/ }, (args) => {
return { path: path.join(__dirname, "src", "vscode-shim.ts") }
})
},
}
const esbuildProblemMatcherPlugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[cli-ts] Build started...")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
if (location) {
console.error(` ${location.file}:${location.line}:${location.column}:`)
}
})
console.log("[cli-ts] Build finished")
})
},
}
const copyWasmFiles = {
name: "copy-wasm-files",
setup(build) {
build.onEnd(() => {
const destDir = path.join(__dirname, "dist")
// Ensure dist directory exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
// tree sitter
const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
// Copy tree-sitter.wasm
const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
if (fs.existsSync(treeSitterWasm)) {
fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
}
// Copy language-specific WASM files
const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
const languages = [
"typescript",
"tsx",
"python",
"rust",
"javascript",
"go",
"cpp",
"c",
"c_sharp",
"ruby",
"java",
"php",
"swift",
"kotlin",
]
if (fs.existsSync(languageWasmDir)) {
languages.forEach((lang) => {
const filename = `tree-sitter-${lang}.wasm`
const sourcePath = path.join(languageWasmDir, filename)
if (fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, path.join(destDir, filename))
}
})
}
})
},
}
const buildEnvVars = {
"import.meta.url": "_importMetaUrl",
"process.env.IS_STANDALONE": JSON.stringify("true"),
"process.env.IS_CLI": JSON.stringify("true"),
}
if (production) {
buildEnvVars["process.env.IS_DEV"] = "false"
}
// Set the environment
if (process.env.CLINE_ENVIRONMENT) {
buildEnvVars["process.env.CLINE_ENVIRONMENT"] = JSON.stringify(process.env.CLINE_ENVIRONMENT)
}
const config = {
entryPoints: [path.join(__dirname, "src", "index.ts")],
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: buildEnvVars,
tsconfig: path.join(__dirname, "tsconfig.json"),
plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, esbuildProblemMatcherPlugin],
format: "cjs",
sourcesContent: false,
platform: "node",
target: "node20",
outfile: path.join(__dirname, "dist", "cli.cjs"),
// These modules need to load files from the module directory at runtime
external: ["@grpc/reflection", "grpc-health-check", "better-sqlite3"],
banner: {
js: `#!/usr/bin/env node
const _importMetaUrl=require('url').pathToFileURL(__filename)`,
},
}
async function main() {
const ctx = await esbuild.context(config)
if (watch) {
await ctx.watch()
console.log("[cli-ts] Watching for changes...")
} else {
await ctx.rebuild()
await ctx.dispose()
// Make the output executable
const outfile = path.join(__dirname, "dist", "cli.cjs")
if (fs.existsSync(outfile)) {
fs.chmodSync(outfile, "755")
}
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
+1449
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@cline/cli",
"version": "1.0.0",
"description": "Cline CLI - TypeScript implementation that reuses core Cline functionality",
"main": "dist/cli.cjs",
"bin": {
"clinedev": "./dist/cli.cjs"
},
"type": "module",
"scripts": {
"build": "node esbuild.mjs",
"build:production": "node esbuild.mjs --production",
"watch": "node esbuild.mjs --watch",
"dev": "npm run watch",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit",
"link": "npm run build && npm link"
},
"keywords": [
"cline",
"cli",
"ai",
"coding-assistant"
],
"author": "Cline Bot Inc.",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "20.x",
"@types/prompts": "^2.4.9",
"esbuild": "^0.25.0",
"rimraf": "^6.0.1",
"typescript": "^5.4.5"
},
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"ora": "^8.0.1",
"ink": "^5.0.1",
"ink-spinner": "^5.0.0",
"prompts": "^2.4.2"
}
}
+337
View File
@@ -0,0 +1,337 @@
/**
* CLI Authentication Handler
*
* Provides interactive and quick-setup authentication modes for the Cline CLI.
* Supports both interactive menus and command-line flag-based configuration.
*/
import prompts from "prompts"
import { StateManager } from "@/core/storage/StateManager"
import { AuthHandler } from "@/hosts/external/AuthHandler"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { StandaloneTerminalManager } from "@/integrations/terminal/standalone/StandaloneTerminalManager"
import { AuthService } from "@/services/auth/AuthService"
import { ErrorService } from "@/services/error/ErrorService"
import { initializeDistinctId } from "@/services/logging/distinctId"
import { API_PROVIDERS_LIST } from "@/shared/api"
import { createCliHostBridgeProvider } from "./cli-host-bridge"
import { CliWebviewProvider } from "./cli-webview-provider"
import { print, printError, printInfo, printSuccess, separator } from "./display"
import { initializeCliContext } from "./vscode-context"
/**
* Options for the auth command
*/
export interface AuthOptions {
provider?: string
apikey?: string
modelid?: string
baseurl?: string
verbose?: boolean
cwd?: string
config?: string
}
/**
* Run authentication flow
* Routes to either interactive mode or quick setup based on provided flags
*/
export async function runAuth(options: AuthOptions): Promise<void> {
try {
// Initialize services
const { extensionContext, EXTENSION_DIR, DATA_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: options.cwd || process.cwd(),
})
await ErrorService.initialize()
// Setup minimal host provider
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR, options.cwd || process.cwd())
// Initialize state manager
await StateManager.initialize(extensionContext)
// Create a webview provider to get a controller instance for auth operations
const webview = HostProvider.get().createWebviewProvider() as CliWebviewProvider
const controller = webview.controller
// Initialize telemetry distinct ID (needed by various services)
await initializeDistinctId(extensionContext)
// Initialize AuthService with the controller
AuthService.getInstance(controller)
// Check if flags are provided for quick setup
if (options.provider || options.apikey || options.modelid || options.baseurl) {
await handleQuickSetup(options)
} else {
// No flags - show interactive menu
await handleInteractiveAuth()
}
} catch (error) {
printError(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
/**
* Setup the host provider for CLI mode
*/
function setupHostProvider(extensionContext: any, extensionDir: string, dataDir: string, workspacePath: string) {
AuthHandler.getInstance().setEnabled(true)
const createWebview = () => new CliWebviewProvider(extensionContext)
const createDiffView = () => new FileEditProvider()
const createCommentReview = () => {
throw new Error("CommentReview not available in auth mode")
}
const createTerminalManager = () => new StandaloneTerminalManager()
const getCallbackUrl = async (): Promise<string> => {
const url = await AuthHandler.getInstance().getCallbackUrl()
return url
}
const getBinaryLocation = async (name: string): Promise<string> => {
const path = await import("path")
return path.join(process.cwd(), name)
}
const logToChannel = (_message: string) => {
// Silent in auth mode
}
HostProvider.initialize(
createWebview,
createDiffView,
createCommentReview,
createTerminalManager,
createCliHostBridgeProvider(workspacePath),
logToChannel,
getCallbackUrl,
getBinaryLocation,
extensionDir,
dataDir,
)
}
/**
* Handle quick setup mode using command-line flags
*/
async function handleQuickSetup(options: AuthOptions): Promise<void> {
// Validate required parameters
if (!options.provider || !options.apikey || !options.modelid) {
printError("Quick setup requires --provider, --apikey, and --modelid flags")
printInfo("Usage: cline auth --provider <provider> --apikey <key> --modelid <model> [--baseurl <url>]")
printInfo("\nExamples:")
printInfo(" cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5")
printInfo(" cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929")
printInfo(" cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1")
process.exit(1)
}
printInfo("🔐 Configuring provider...")
print(separator())
try {
const normalizedProvider = options.provider.toLowerCase().trim()
// Sort by alphabetical order for display
const sortedProviders = API_PROVIDERS_LIST.slice().sort()
if (!sortedProviders.includes(normalizedProvider)) {
throw new Error(`Invalid provider '${options.provider}'. Supported providers: ${sortedProviders.join(", ")}`)
}
// Check for bedrock
if (normalizedProvider === "bedrock") {
throw new Error(
"Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth",
)
}
// Validate baseurl is only for OpenAI
if (options.baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
throw new Error("Base URL is only supported for OpenAI and OpenAI-compatible providers")
}
// Save configuration to StateManager
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: normalizedProvider,
planModeApiProvider: normalizedProvider,
actModeApiModelId: options.modelid,
planModeApiModelId: options.modelid,
apiKey: options.apikey,
}
if (options.baseurl) {
config.openAiBaseUrl = options.baseurl
}
await stateManager.setApiConfiguration(config)
printSuccess(`Provider: ${normalizedProvider}`)
printSuccess(`Model: ${options.modelid}`)
if (options.baseurl) {
printSuccess(`Base URL: ${options.baseurl}`)
}
printSuccess("API Key: Configured")
print(separator())
printSuccess("✓ Successfully configured authentication")
printInfo("You can now use Cline with this provider.")
printInfo("Run 'cline task \"<your prompt>\"' to begin a new task.")
} catch (error) {
printError(`Configuration failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
/**
* Handle interactive authentication menu
*/
async function handleInteractiveAuth(): Promise<void> {
try {
printInfo("🔐 Cline Authentication Menu")
print(separator())
printInfo("")
// Show main menu
const mainMenuResponse = await prompts({
type: "select",
name: "action",
message: "What would you like to do?",
choices: [
{ title: "Sign in to Cline", value: "cline_auth" },
{ title: "Configure BYO API provider", value: "configure_byo" },
{ title: "Exit", value: "exit" },
],
})
if (mainMenuResponse.action === "exit" || mainMenuResponse.action === undefined) {
printInfo("Exiting authentication wizard.")
return
}
if (mainMenuResponse.action === "cline_auth") {
await AuthService.getInstance().createAuthRequest()
}
if (mainMenuResponse.action === "configure_byo") {
await handleProviderSetupInteractive()
}
} catch (error) {
printError(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
/**
* Handle interactive BYO provider setup
*/
async function handleProviderSetupInteractive(): Promise<void> {
// Get current configuration to show which providers are already configured
const stateManager = StateManager.get()
const currentConfig = stateManager.getApiConfiguration()
const currentProvider = currentConfig.actModeApiProvider || currentConfig.planModeApiProvider
// Sort by alphabetical order for display
const sortedProviders = API_PROVIDERS_LIST.slice().sort()
const providerResponse = await prompts({
type: "select",
name: "provider",
message: "Select a provider:",
choices: sortedProviders.map((p) => ({
title: `${capitalize(p)}${currentProvider === p ? " (configured)" : ""}`,
value: p,
})),
})
if (providerResponse.provider === undefined) {
printInfo("Provider setup cancelled.")
return
}
let apiKey: string | undefined
if (providerResponse.provider === "cline") {
AuthService.getInstance().createAuthRequest()
} else {
const apiKeyResponse = await prompts({
type: "password",
name: "apikey",
message: "Enter your API key:",
})
if (apiKeyResponse.apikey === undefined) {
printInfo("Provider setup cancelled.")
return
}
apiKey = apiKeyResponse.apikey
}
const modelResponse = await prompts({
type: "text",
name: "modelid",
message: "Enter the model ID (e.g., gpt-4, claude-sonnet-4.5):",
})
if (modelResponse.modelid === undefined) {
printInfo("Provider setup cancelled.")
return
}
let baseUrl = ""
if (["openai", "openai-native"].includes(providerResponse.provider)) {
const baseUrlResponse = await prompts({
type: "text",
name: "baseurl",
message: "Enter base URL (optional, press Enter to skip):",
initial: "",
})
baseUrl = baseUrlResponse.baseurl || ""
}
// Save configuration to StateManager
try {
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: providerResponse.provider,
planModeApiProvider: providerResponse.provider,
actModeApiModelId: modelResponse.modelid,
planModeApiModelId: modelResponse.modelid,
}
if (apiKey) {
config.apiKey = apiKey
}
if (baseUrl) {
config.openAiBaseUrl = baseUrl
}
stateManager.setApiConfiguration(config)
print(separator())
printSuccess(`✓ Provider configured successfully`)
printInfo(`Provider: ${capitalize(providerResponse.provider)}`)
printInfo(`Model: ${modelResponse.modelid}`)
if (baseUrl) {
printInfo(`Base URL: ${baseUrl}`)
}
print(separator())
printInfo("You can now use Cline with this provider.")
printInfo("Run 'cline task \"<your prompt>\"' to begin a new task.")
} catch (error) {
printError(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
function capitalize(str: string): string {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
}
+38
View File
@@ -0,0 +1,38 @@
/**
* CLI-specific CommentReviewController implementation
* Handles code review comments in CLI mode
*/
import { CommentReviewController } from "@/integrations/editor/CommentReviewController"
import { print, style } from "./display"
export class CliCommentReviewController extends CommentReviewController {
private comments: Map<string, string[]> = new Map()
override async addComment(filePath: string, line: number, comment: string): Promise<void> {
const key = `${filePath}:${line}`
const existing = this.comments.get(key) || []
existing.push(comment)
this.comments.set(key, existing)
print(style.info(`💬 Comment on ${filePath}:${line}`))
print(style.dim(` ${comment}`))
}
override async clearComments(filePath?: string): Promise<void> {
if (filePath) {
// Clear comments for specific file
for (const key of this.comments.keys()) {
if (key.startsWith(filePath)) {
this.comments.delete(key)
}
}
} else {
this.comments.clear()
}
}
override dispose(): void {
this.comments.clear()
}
}
+259
View File
@@ -0,0 +1,259 @@
/**
* CLI-specific Host Bridge implementations
* These provide stub implementations for the host bridge interfaces that work in CLI mode
*/
import type {
DiffServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
WorkspaceServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import type { HostBridgeClientProvider, StreamingCallbacks } from "@hosts/host-provider-types"
import * as proto from "@shared/proto/index"
import { printError, printInfo, printWarning } from "./display"
/**
* CLI implementation of DiffService - handles diff operations for terminal
*/
export class CliDiffServiceClient implements DiffServiceClientInterface {
async openDiff(request: proto.host.OpenDiffRequest): Promise<proto.host.OpenDiffResponse> {
printInfo(`📝 Opening diff for: ${request.leftUri || request.rightUri}`)
return proto.host.OpenDiffResponse.create({})
}
async getDocumentText(request: proto.host.GetDocumentTextRequest): Promise<proto.host.GetDocumentTextResponse> {
// In CLI mode, we'd read from the file system directly
return proto.host.GetDocumentTextResponse.create({ text: "" })
}
async replaceText(request: proto.host.ReplaceTextRequest): Promise<proto.host.ReplaceTextResponse> {
printInfo(`✏️ Replacing text in document`)
return proto.host.ReplaceTextResponse.create({})
}
async scrollDiff(request: proto.host.ScrollDiffRequest): Promise<proto.host.ScrollDiffResponse> {
// No-op in CLI
return proto.host.ScrollDiffResponse.create({})
}
async truncateDocument(request: proto.host.TruncateDocumentRequest): Promise<proto.host.TruncateDocumentResponse> {
return proto.host.TruncateDocumentResponse.create({})
}
async saveDocument(request: proto.host.SaveDocumentRequest): Promise<proto.host.SaveDocumentResponse> {
printInfo(`💾 Saving document`)
return proto.host.SaveDocumentResponse.create({})
}
async closeAllDiffs(request: proto.host.CloseAllDiffsRequest): Promise<proto.host.CloseAllDiffsResponse> {
return proto.host.CloseAllDiffsResponse.create({})
}
async openMultiFileDiff(request: proto.host.OpenMultiFileDiffRequest): Promise<proto.host.OpenMultiFileDiffResponse> {
printInfo(`📝 Opening multi-file diff`)
return proto.host.OpenMultiFileDiffResponse.create({})
}
}
/**
* CLI implementation of EnvService - handles environment operations
*/
export class CliEnvServiceClient implements EnvServiceClientInterface {
private clipboardContent: string = ""
async clipboardWriteText(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
this.clipboardContent = request.value || ""
printInfo(`📋 Copied to clipboard`)
return proto.cline.Empty.create()
}
async clipboardReadText(request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
return proto.cline.String.create({ value: this.clipboardContent })
}
async getHostVersion(request: proto.cline.EmptyRequest): Promise<proto.host.GetHostVersionResponse> {
return proto.host.GetHostVersionResponse.create({
version: "1.0.0",
platform: "Cline CLI",
})
}
async getIdeRedirectUri(request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
// CLI doesn't have IDE redirect
return proto.cline.String.create({ value: "" })
}
async getTelemetrySettings(request: proto.cline.EmptyRequest): Promise<proto.host.GetTelemetrySettingsResponse> {
return proto.host.GetTelemetrySettingsResponse.create({
isTelemetryEnabled: false,
isCrashReporterEnabled: false,
})
}
subscribeToTelemetrySettings(
request: proto.cline.EmptyRequest,
callbacks: StreamingCallbacks<proto.host.TelemetrySettingsEvent>,
): () => void {
// Send initial settings
callbacks.onResponse(
proto.host.TelemetrySettingsEvent.create({
isTelemetryEnabled: false,
isCrashReporterEnabled: false,
}),
)
// Return unsubscribe function
return () => {}
}
async shutdown(request: proto.cline.EmptyRequest): Promise<proto.cline.Empty> {
printInfo("Shutting down...")
return proto.cline.Empty.create()
}
}
/**
* CLI implementation of WindowService - handles window/UI operations
*/
export class CliWindowServiceClient implements WindowServiceClientInterface {
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
printInfo(`📄 Opening file: ${request.path}`)
return proto.host.TextEditorInfo.create({
path: request.path,
})
}
async showOpenDialogue(request: proto.host.ShowOpenDialogueRequest): Promise<proto.host.SelectedResources> {
printWarning("Open dialog not available in CLI mode")
return proto.host.SelectedResources.create({ uris: [] })
}
async showMessage(request: proto.host.ShowMessageRequest): Promise<proto.host.SelectedResponse> {
const message = request.message || ""
const type = request.type
switch (type) {
case proto.host.ShowMessageType.ERROR:
printError(message)
break
case proto.host.ShowMessageType.WARNING:
printWarning(message)
break
case proto.host.ShowMessageType.INFORMATION:
default:
printInfo(message)
break
}
return proto.host.SelectedResponse.create({})
}
async showInputBox(request: proto.host.ShowInputBoxRequest): Promise<proto.host.ShowInputBoxResponse> {
// In CLI mode, we could use readline, but for now return empty
printWarning("Input box not available in CLI mode")
return proto.host.ShowInputBoxResponse.create({ value: "" })
}
async showSaveDialog(request: proto.host.ShowSaveDialogRequest): Promise<proto.host.ShowSaveDialogResponse> {
printWarning("Save dialog not available in CLI mode")
return proto.host.ShowSaveDialogResponse.create({ uri: "" })
}
async openFile(request: proto.host.OpenFileRequest): Promise<proto.host.OpenFileResponse> {
printInfo(`📂 Opening: ${request.path}`)
return proto.host.OpenFileResponse.create({})
}
async openSettings(request: proto.host.OpenSettingsRequest): Promise<proto.host.OpenSettingsResponse> {
printInfo("Settings can be configured in ~/.cline/data/globalState.json")
return proto.host.OpenSettingsResponse.create({})
}
async getOpenTabs(request: proto.host.GetOpenTabsRequest): Promise<proto.host.GetOpenTabsResponse> {
// CLI doesn't have tabs
return proto.host.GetOpenTabsResponse.create({ tabs: [] })
}
async getVisibleTabs(request: proto.host.GetVisibleTabsRequest): Promise<proto.host.GetVisibleTabsResponse> {
return proto.host.GetVisibleTabsResponse.create({ tabs: [] })
}
async getActiveEditor(request: proto.host.GetActiveEditorRequest): Promise<proto.host.GetActiveEditorResponse> {
return proto.host.GetActiveEditorResponse.create({})
}
}
/**
* CLI implementation of WorkspaceService - handles workspace operations
*/
export class CliWorkspaceServiceClient implements WorkspaceServiceClientInterface {
private workspacePath: string
constructor(workspacePath: string = process.cwd()) {
this.workspacePath = workspacePath
}
setWorkspacePath(path: string) {
this.workspacePath = path
}
async getWorkspacePaths(request: proto.host.GetWorkspacePathsRequest): Promise<proto.host.GetWorkspacePathsResponse> {
return proto.host.GetWorkspacePathsResponse.create({
paths: [this.workspacePath],
})
}
async saveOpenDocumentIfDirty(
request: proto.host.SaveOpenDocumentIfDirtyRequest,
): Promise<proto.host.SaveOpenDocumentIfDirtyResponse> {
return proto.host.SaveOpenDocumentIfDirtyResponse.create({})
}
async getDiagnostics(request: proto.host.GetDiagnosticsRequest): Promise<proto.host.GetDiagnosticsResponse> {
// In CLI mode, we could run linters here
return proto.host.GetDiagnosticsResponse.create({ diagnostics: [] })
}
async openProblemsPanel(request: proto.host.OpenProblemsPanelRequest): Promise<proto.host.OpenProblemsPanelResponse> {
printInfo("Run linters to see problems")
return proto.host.OpenProblemsPanelResponse.create({})
}
async openInFileExplorerPanel(
request: proto.host.OpenInFileExplorerPanelRequest,
): Promise<proto.host.OpenInFileExplorerPanelResponse> {
printInfo(`📁 ${request.path}`)
return proto.host.OpenInFileExplorerPanelResponse.create({})
}
async openClineSidebarPanel(
request: proto.host.OpenClineSidebarPanelRequest,
): Promise<proto.host.OpenClineSidebarPanelResponse> {
// No sidebar in CLI
return proto.host.OpenClineSidebarPanelResponse.create({})
}
async openTerminalPanel(request: proto.host.OpenTerminalRequest): Promise<proto.host.OpenTerminalResponse> {
printInfo("Terminal is already available in CLI mode")
return proto.host.OpenTerminalResponse.create({})
}
async executeCommandInTerminal(
request: proto.host.ExecuteCommandInTerminalRequest,
): Promise<proto.host.ExecuteCommandInTerminalResponse> {
printInfo(`⚙️ Executing: ${request.command}`)
return proto.host.ExecuteCommandInTerminalResponse.create({})
}
}
/**
* Create a CLI host bridge provider
*/
export function createCliHostBridgeProvider(workspacePath?: string): HostBridgeClientProvider {
return {
workspaceClient: new CliWorkspaceServiceClient(workspacePath),
envClient: new CliEnvServiceClient(),
windowClient: new CliWindowServiceClient(),
diffClient: new CliDiffServiceClient(),
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* CLI-specific WebviewProvider implementation
* Instead of rendering to a webview, this outputs to the terminal
*/
import type * as vscode from "vscode"
import { WebviewProvider } from "@/core/webview"
export class CliWebviewProvider extends WebviewProvider {
constructor(context: vscode.ExtensionContext) {
super(context)
}
override getWebviewUrl(path: string): string {
// CLI doesn't have webview URLs
return `file://${path}`
}
override getCspSource(): string {
return "'self'"
}
override isVisible(): boolean {
// CLI is always "visible"
return true
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Console management for CLI
*
* Captures original console methods BEFORE any core modules are imported,
* so CLI output works even when console.log is suppressed.
*/
// Capture original console methods immediately
export const originalConsoleLog = console.log.bind(console)
export const originalConsoleError = console.error.bind(console)
export const originalConsoleWarn = console.warn.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
if (!isVerbose) {
console.log = () => {}
console.warn = () => {}
}
/**
* Restore original console methods (for cleanup)
*/
export function restoreConsole() {
console.log = originalConsoleLog
console.error = originalConsoleError
console.warn = originalConsoleWarn
}
+460
View File
@@ -0,0 +1,460 @@
/**
* Terminal display utilities for rendering Cline messages in the CLI
*/
import type { ClineAsk, ClineMessage, ClineSay, ExtensionState } from "@shared/ExtensionMessage"
import { originalConsoleError, originalConsoleLog } from "./console"
// ANSI color codes for terminal output
const colors = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
italic: "\x1b[3m",
underline: "\x1b[4m",
// Foreground colors
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
// Bright foreground colors
brightBlack: "\x1b[90m",
brightRed: "\x1b[91m",
brightGreen: "\x1b[92m",
brightYellow: "\x1b[93m",
brightBlue: "\x1b[94m",
brightMagenta: "\x1b[95m",
brightCyan: "\x1b[96m",
brightWhite: "\x1b[97m",
// Background colors
bgBlack: "\x1b[40m",
bgRed: "\x1b[41m",
bgGreen: "\x1b[42m",
bgYellow: "\x1b[43m",
bgBlue: "\x1b[44m",
bgMagenta: "\x1b[45m",
bgCyan: "\x1b[46m",
bgWhite: "\x1b[47m",
}
export function colorize(text: string, ...colorCodes: string[]): string {
return colorCodes.join("") + text + colors.reset
}
// Helper functions for common color combinations
export const style = {
bold: (text: string) => colorize(text, colors.bold),
dim: (text: string) => colorize(text, colors.dim),
italic: (text: string) => colorize(text, colors.italic),
error: (text: string) => colorize(text, colors.red, colors.bold),
warning: (text: string) => colorize(text, colors.yellow),
success: (text: string) => colorize(text, colors.green),
info: (text: string) => colorize(text, colors.cyan),
// Message type colors
task: (text: string) => colorize(text, colors.brightWhite, colors.bold),
tool: (text: string) => colorize(text, colors.blue),
command: (text: string) => colorize(text, colors.magenta),
api: (text: string) => colorize(text, colors.brightBlack),
user: (text: string) => colorize(text, colors.green),
assistant: (text: string) => colorize(text, colors.cyan),
// Special formatting
path: (text: string) => colorize(text, colors.underline, colors.blue),
code: (text: string) => colorize(text, colors.bgBlack, colors.brightWhite),
}
/**
* Format a timestamp for display
*/
export function formatTimestamp(ts: number): string {
const date = new Date(ts)
return date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
}
/**
* Get a prefix icon for different message types
*/
function getMessageIcon(message: ClineMessage): string {
if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "❓"
case "command":
case "command_output":
return "⚙️ "
case "tool":
return "🔧"
case "completion_result":
return "✅"
case "api_req_failed":
return "❌"
case "resume_task":
case "resume_completed_task":
return "▶️ "
case "browser_action_launch":
return "🌐"
case "use_mcp_server":
return "🔌"
default:
return "❔"
}
} else {
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️ "
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️ "
default:
return " "
}
}
}
/**
* Format a ClineMessage for terminal display
*/
export function formatMessage(message: ClineMessage, verbose: boolean = false): string {
const icon = getMessageIcon(message)
const timestamp = formatTimestamp(message.ts)
const lines: string[] = []
const prefix = `${style.dim(timestamp)} ${icon}`
if (message.type === "ask") {
lines.push(formatAskMessage(message, prefix, verbose))
} else {
lines.push(formatSayMessage(message, prefix, verbose))
}
return lines.filter(Boolean).join("\n")
}
function formatAskMessage(message: ClineMessage, prefix: string, verbose: boolean): string {
const ask = message.ask as ClineAsk
switch (ask) {
case "followup": {
// Parse JSON question format
let question = message.text || ""
try {
const parsed = JSON.parse(message.text || "{}")
question = parsed.question || question
} catch {
// Fallback to raw text if not JSON
question = message.text || ""
}
return `${prefix} ${style.info("Question:")} ${question}`
}
case "command":
return `${prefix} ${style.command("Execute command?")} ${style.code(message.text || "")}`
case "tool":
return `${prefix} ${style.tool("Use tool?")} ${message.text || ""}`
case "completion_result":
return `${prefix} ${style.success("Task completed")} ${message.text ? `- ${message.text}` : ""}`
case "api_req_failed":
return `${prefix} ${style.error("API request failed")} ${message.text || ""}`
case "resume_task":
case "resume_completed_task":
return `${prefix} ${style.info("Resume task?")} ${message.text || ""}`
case "browser_action_launch":
return `${prefix} ${style.info("Launch browser?")} ${message.text || ""}`
case "use_mcp_server":
return `${prefix} ${style.info("Use MCP server?")} ${message.text || ""}`
case "plan_mode_respond":
return `${prefix} ${style.info("Plan mode response:")} ${message.text || ""}`
default:
return verbose ? `${prefix} [ASK:${ask}] ${message.text || ""}` : ""
}
}
function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolean): string {
const say = message.say as ClineSay
switch (say) {
case "task":
return `${prefix} ${style.task("Task:")} ${message.text || ""}`
case "text":
return `${prefix} ${style.assistant(message.text || "")}`
case "reasoning":
return `${prefix} ${style.dim("Thinking:")} ${style.italic(message.text || "")}`
case "error":
return `${prefix} ${style.error("Error:")} ${message.text || ""}`
case "completion_result":
return `${prefix} ${style.success("✓ Completed:")} ${message.text || ""}`
case "user_feedback":
return `${prefix} ${style.user("User:")} ${message.text || ""}`
case "command":
return `${prefix} ${style.command("Command:")} ${style.code(message.text || "")}`
case "command_output":
const output = message.text || ""
const truncated = output.length > 500 ? output.substring(0, 500) + "..." : output
return `${prefix} ${style.dim("Output:")} ${truncated}`
case "tool":
return `${prefix} ${style.tool("Tool:")} ${message.text || ""}`
case "browser_action":
case "browser_action_launch":
return `${prefix} ${style.info("Browser:")} ${message.text || ""}`
case "browser_action_result":
return `${prefix} ${style.dim("Browser result")} ${message.text ? `- ${message.text.substring(0, 100)}...` : ""}`
case "mcp_server_request_started":
return `${prefix} ${style.info("MCP request started")} ${message.text || ""}`
case "mcp_server_response":
return `${prefix} ${style.info("MCP response")} ${message.text ? message.text.substring(0, 200) : ""}`
case "api_req_started":
return verbose ? `${prefix} ${style.api("API request started")}` : `${message.text || ""}`
case "api_req_finished":
return verbose ? `${prefix} ${style.api("API request finished")}` : ""
case "checkpoint_created":
return `${prefix} ${style.success("Checkpoint created")} ${message.text || ""}`
case "info":
return `${prefix} ${style.info(message.text || "")}`
case "hook_status":
return `${prefix} ${style.dim("Hook:")} ${message.text || ""}`
case "task_progress":
return `${prefix} ${style.info("Progress:")} ${message.text || ""}`
default:
return verbose ? `${prefix} [SAY:${say}] ${message.text || ""}` : ""
}
}
/**
* Display a horizontal separator
*/
export function separator(char: string = "─", width: number = 60): string {
return style.dim(char.repeat(width))
}
/**
* Display the task header
*/
export function taskHeader(taskId: string, task?: string): string {
const lines = [
separator("═"),
style.bold(` Task: ${taskId}`),
task ? ` ${style.dim(task.substring(0, 80))}${task.length > 80 ? "..." : ""}` : "",
separator("═"),
]
return lines.filter(Boolean).join("\n")
}
/**
* Format the current state for display
*/
export function formatState(state: ExtensionState, verbose: boolean = false): string {
const lines: string[] = []
if (state.currentTaskItem) {
lines.push(taskHeader(state.currentTaskItem.id, state.currentTaskItem.task))
}
// Show messages
if (state.clineMessages && state.clineMessages.length > 0) {
const messagesToShow = verbose
? state.clineMessages
: state.clineMessages.filter((m) => {
// Filter out noisy messages in non-verbose mode
// if (m.say === "api_req_started" || m.say === "api_req_finished") return false
return true
})
for (const message of messagesToShow) {
const formatted = formatMessage(message, verbose)
if (formatted) {
lines.push(formatted)
}
}
}
return lines.join("\n")
}
/**
* Display a spinner with message
*/
export class Spinner {
private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
private frameIndex = 0
private interval: NodeJS.Timeout | null = null
private message: string = ""
start(message: string) {
this.message = message
this.interval = setInterval(() => {
const frame = this.frames[this.frameIndex]
process.stdout.write(`\r${style.info(frame)} ${this.message}`)
this.frameIndex = (this.frameIndex + 1) % this.frames.length
}, 80)
}
update(message: string) {
this.message = message
}
stop(finalMessage?: string) {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
if (finalMessage) {
process.stdout.write(`\r${style.success("✓")} ${finalMessage}\n`)
} else {
process.stdout.write("\r" + " ".repeat(this.message.length + 4) + "\r")
}
}
fail(message?: string) {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
if (message) {
process.stdout.write(`\r${style.error("✗")} ${message}\n`)
}
}
}
/**
* Clear the current line
*/
export function clearLine() {
process.stdout.write("\r\x1b[K")
}
/**
* Move cursor up n lines
*/
export function cursorUp(n: number = 1) {
process.stdout.write(`\x1b[${n}A`)
}
/**
* Print a message to stdout with newline
* Uses original console.log to work even when console is suppressed
*/
export function print(message: string) {
originalConsoleLog(message)
}
/**
* Print an error message to stderr
* Uses original console.error to work even when console is suppressed
*/
export function printError(message: string) {
originalConsoleError(style.error(message))
}
/**
* Print a success message
*/
export function printSuccess(message: string) {
originalConsoleLog(style.success(message))
}
/**
* Print an info message
*/
export function printInfo(message: string) {
originalConsoleLog(style.info(message))
}
/**
* Print a warning message
*/
export function printWarning(message: string) {
originalConsoleLog(style.warning(message))
}
/**
* Prompt user for input from stdin
*/
export async function promptUser(question: string): Promise<string> {
const readline = await import("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
rl.question(style.info(question) + " ", (answer: string) => {
rl.close()
resolve(answer.trim())
})
})
}
/**
* Prompt user for yes/no confirmation
*/
export async function promptConfirmation(question: string): Promise<boolean> {
const answer = await promptUser(`${question} ${style.dim("(y/n)")}`)
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"
}
+690
View File
@@ -0,0 +1,690 @@
/**
* Cline CLI - TypeScript implementation
*
* A command-line interface for Cline that reuses the core TypeScript codebase,
* allowing you to run Cline tasks directly from the terminal.
*/
import type { ClineAsk, ClineMessage, ExtensionState } from "@shared/ExtensionMessage"
import { Command } from "commander"
import { StateManager } from "@/core/storage/StateManager"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { StandaloneTerminalManager } from "@/integrations/terminal/standalone/StandaloneTerminalManager"
import { BannerService } from "@/services/banner/BannerService"
import { ErrorService } from "@/services/error/ErrorService"
import { initializeDistinctId } from "@/services/logging/distinctId"
import { runAuth } from "./cli-auth"
import { CliCommentReviewController } from "./cli-comment-review"
import { createCliHostBridgeProvider } from "./cli-host-bridge"
import { CliWebviewProvider } from "./cli-webview-provider"
// IMPORTANT: Import console module FIRST - it suppresses console.log before core imports
import { restoreConsole } from "./console"
import { print, printError, printInfo, printSuccess, promptConfirmation, promptUser, Spinner, separator, style } from "./display"
import { jsonParseSafe } from "./utils"
import { initializeCliContext } from "./vscode-context"
// Version from package.json
const VERSION = "0.0.0"
/**
* Setup the host provider for CLI mode
*/
function setupHostProvider(
extensionContext: any,
extensionDir: string,
dataDir: string,
workspacePath: string,
verbose: boolean = false,
) {
const createWebview = () => new CliWebviewProvider(extensionContext)
const createDiffView = () => new FileEditProvider()
const createCommentReview = () => new CliCommentReviewController()
const createTerminalManager = () => new StandaloneTerminalManager()
const getCallbackUrl = async (): Promise<string> => {
// CLI doesn't support OAuth callbacks
return ""
}
const getBinaryLocation = async (name: string): Promise<string> => {
const path = await import("path")
return path.join(process.cwd(), name)
}
// Only log in verbose mode to avoid cluttering the CLI output
const logToChannel = verbose
? (message: string) => printInfo(message)
: (_message: string) => {
// Silent in non-verbose mode
}
HostProvider.initialize(
createWebview,
createDiffView,
createCommentReview,
createTerminalManager,
createCliHostBridgeProvider(workspacePath),
logToChannel,
getCallbackUrl,
getBinaryLocation,
extensionDir,
dataDir,
)
}
/**
* State subscriber that streams text updates to the terminal
*/
class CliStateSubscriber {
private lastMessageTexts = new Map<number, string>() // Track message text by index for streaming
private processedAskMessages = new Set<number>() // Track which ask messages we've already prompted for
private processedSayMessages = new Set<number>() // Track which say messages we've already displayed
private verbose: boolean
private spinner: Spinner | null
private controller: any // Reference to the task controller
// private lastStreamedMsgTs = 0
constructor(verbose: boolean = false, controller?: any) {
this.verbose = verbose
this.spinner = null
this.controller = controller
}
onStateUpdate(state: ExtensionState) {
const messages = state.clineMessages || []
// Stream partial text updates only
for (let i = 0; i < messages.length; i++) {
const message = messages[i]
const currentText = message.text || ""
const lastText = this.lastMessageTexts.get(i) || ""
// Handle different message types
if (message.say === "text" && currentText !== lastText) {
// Stream text messages incrementally
const newContent = currentText.slice(lastText.length)
if (newContent) {
process.stdout.write(newContent)
}
this.lastMessageTexts.set(i, currentText)
} else if (message.say !== "text" && currentText !== lastText && lastText === "") {
// New non-text message (hasn't been seen before)
this.displayNewMessage(message)
this.lastMessageTexts.set(i, currentText)
}
if (
message.type === "say" &&
message.say === "tool" &&
message.partial === false &&
!this.processedSayMessages.has(message.ts)
) {
this.processedSayMessages.add(message.ts)
this.displayNewMessage(message)
}
// Check if this is a completed ask message and prompt for input
if (message.type === "ask" && message.partial === false && !this.processedAskMessages.has(i)) {
this.processedAskMessages.add(i)
// Handle ask message asynchronously without blocking state updates
this.handleAskMessage(message).catch((error) => {
if (this.verbose) {
printError(`Error handling ask message: ${error instanceof Error ? error.message : String(error)}`)
}
})
}
}
}
private async handleAskMessage(message: ClineMessage) {
if (!this.controller || !this.controller.task) {
return
}
const ask = message.ask as ClineAsk
try {
switch (ask) {
case "followup":
case "plan_mode_respond":
{
if (message.text) {
const parts = jsonParseSafe(message.text, {
response: undefined as string | undefined,
options: undefined as string[] | undefined,
selected: undefined as string | undefined,
question: undefined as string | undefined,
})
if (parts.response) {
print(style.assistant(`[${message.ask}] ${parts.response}`))
}
// Text input questions
if (parts.question) {
print(style.assistant(`[${message.ask}] ${parts.question}`))
const userText = await promptUser("Reply:")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse("messageResponse", userText)
}
} else if (parts.options && parts.options.length > 0) {
// Multiple choice options
printInfo("Options:")
parts.options.forEach((opt, idx) => {
printInfo(` ${idx + 1}. ${opt}`)
})
const choiceStr = await promptUser("Select an option (number):")
const choiceIdx = parseInt(choiceStr, 10) - 1
if (choiceIdx >= 0 && choiceIdx < parts.options.length) {
const selectedOption = parts.options[choiceIdx]
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse("optionSelected", selectedOption)
}
} else {
printError("Invalid option selected.")
}
}
}
}
break
case "act_mode_respond":
print(style.success(`[${message.ask}] ${message.text || ""}`))
break
case "command":
const approveCmd = await promptConfirmation("Execute this command?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(approveCmd ? "yesButtonClicked" : "noButtonClicked")
}
break
case "tool":
const approveTool = await promptConfirmation("Use this tool?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(approveTool ? "yesButtonClicked" : "noButtonClicked")
}
break
case "completion_result":
const confirmComplete = await promptConfirmation("Task complete?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(
confirmComplete ? "yesButtonClicked" : "noButtonClicked",
)
}
break
case "resume_task":
case "resume_completed_task":
const confirmResume = await promptConfirmation("Resume task?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(
confirmResume ? "yesButtonClicked" : "noButtonClicked",
)
}
break
case "browser_action_launch":
const confirmBrowser = await promptConfirmation("Launch browser?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(
confirmBrowser ? "yesButtonClicked" : "noButtonClicked",
)
}
break
case "use_mcp_server":
const confirmMcp = await promptConfirmation("Use MCP server?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(confirmMcp ? "yesButtonClicked" : "noButtonClicked")
}
break
// Silent asks that don't require user input
case "command_output":
case "api_req_failed":
case "mistake_limit_reached":
// These are informational - no response needed
break
default:
if (this.verbose) {
printInfo(`Ask type "${ask}" requires manual response`)
}
}
} catch (error) {
// Silently ignore errors when controller is being disposed
if (this.controller && this.verbose) {
printError(`Failed to handle user response: ${error instanceof Error ? error.message : String(error)}`)
}
}
}
private displayNewMessage(message: ClineMessage) {
// Stop spinner if running
if (this.spinner) {
const spinner = this.spinner
spinner.stop()
this.spinner = null
}
// Format and display based on message type
if (message.type === "say") {
switch (message.say) {
case "text":
// Text messages are streamed incrementally, so we don't need to display again
// Just ensure the spinner is stopped above
break
case "task":
print(separator("═"))
print(style.task(`📋 Task: ${message.text || ""}`))
print(separator("═"))
break
case "reasoning":
if (this.verbose) {
print(style.dim(`🧠 ${message.text || ""}`))
}
break
case "error":
printError(message.text || "An error occurred")
break
case "completion_result":
print(separator())
printSuccess(`${message.text || "Task completed"}`)
print(separator())
break
case "command":
print(style.command(`⚙️ Command: ${message.text || ""}`))
break
case "command_output":
if (message.text) {
const lines = message.text.split("\n")
const displayLines = lines.slice(0, 10)
for (const line of displayLines) {
print(style.dim(` ${line}`))
}
if (lines.length > 10) {
print(style.dim(` ... and ${lines.length - 10} more lines`))
}
}
break
case "tool":
print(style.tool(`🔧 ${message.text || ""}`))
break
case "api_req_started":
this.spinner = new Spinner()
if (message.text) {
// Parse the JSON API request info and format it nicely
// try {
// const apiInfo = JSON.parse(message.text)
// const details: string[] = []
// if (apiInfo.tokensIn) details.push(`in: ${apiInfo.tokensIn}`)
// if (apiInfo.tokensOut) details.push(`out: ${apiInfo.tokensOut}`)
// if (apiInfo.cost) details.push(`$${apiInfo.cost.toFixed(4)}`)
// const detailStr = details.length > 0 ? ` [${details.join(", ")}]` : ""
// printInfo(`API request${message.text}`)
// } catch {
// // Fallback if JSON parsing fails
// this.spinner.start("Thinking...")
// }
} else {
this.spinner.start("Thinking...")
}
break
case "api_req_finished":
this.spinner = null
break
case "info":
printInfo(message.text || "")
break
case "checkpoint_created":
if (this.verbose) {
print(style.dim(`💾 Checkpoint created`))
}
break
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup": {
// Parse JSON question format
let question = "Question"
try {
const parsed = JSON.parse(message.text || "{}")
question = parsed.question || question
} catch {
// Fallback to raw text if not JSON
question = message.text || question
}
print(style.info(`${question}`))
break
}
case "command":
print(style.warning(`⚙️ Execute command: ${message.text || ""}`))
break
case "tool":
print(style.info(`🔧 Use tool: ${message.text || ""}`))
break
case "completion_result":
printSuccess(`${message.text || "Task completed"}`)
break
case "api_req_failed":
printError(`${message.text || "API request failed"}`)
break
}
}
}
reset() {
this.lastMessageTexts.clear()
if (this.spinner) {
this.spinner.stop()
this.spinner = null
}
}
}
/**
* Run a task with the given prompt
*/
async function runTask(
prompt: string,
options: { mode?: string; model?: string; verbose?: boolean; cwd?: string; config?: string },
) {
const workspacePath = options.cwd || process.cwd()
if (options.mode) {
StateManager.get().setGlobalState("mode", options.mode === "plan" ? "plan" : "act")
}
if (options.model) {
const selectedMode = StateManager.get().getGlobalSettingsKey("mode") || "act"
StateManager.get().setGlobalState(selectedMode === "act" ? "actModeApiModelId" : "planModeApiModelId", options.model)
}
printInfo(`🚀 Starting Cline task...`)
printInfo(`📁 Working directory: ${workspacePath}`)
print(separator())
// Initialize context
const { extensionContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: workspacePath,
})
// Initialize ErrorService (required by BannerService)
await ErrorService.initialize()
// Setup host provider
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR, workspacePath, options.verbose)
// Initialize state manager
await StateManager.initialize(extensionContext)
// Create webview provider (which creates the controller)
const webview = HostProvider.get().createWebviewProvider() as CliWebviewProvider
const controller = webview.controller
// Initialize telemetry distinct ID (needed by various services)
await initializeDistinctId(extensionContext)
// Initialize BannerService (required by Controller.getStateToPostToWebview)
if (!BannerService.isInitialized()) {
BannerService.initialize(controller)
}
// Setup state subscriber
const subscriber = new CliStateSubscriber(options.verbose, controller)
// Override postStateToWebview to also update CLI
const originalPostState = controller.postStateToWebview.bind(controller)
controller.postStateToWebview = async () => {
await originalPostState()
const state = await controller.getStateToPostToWebview()
subscriber.onStateUpdate(state)
}
// Start the task
try {
const taskId = await controller.initTask(prompt)
printInfo(`Task ID: ${taskId}`)
// Wait for task completion by monitoring state updates
const result = await new Promise<{ success: boolean }>((resolve) => {
let completionTimeout: NodeJS.Timeout | null = null
const checkInterval: NodeJS.Timeout | null = null
// Wrap the original onStateUpdate to detect completion
const originalOnStateUpdate = subscriber.onStateUpdate.bind(subscriber)
subscriber.onStateUpdate = async (state: ExtensionState) => {
// Call original update
originalOnStateUpdate(state)
// Check for completion
const lastMessage = state.clineMessages[state.clineMessages.length - 1]
if (lastMessage) {
if (lastMessage.say === "completion_result" || lastMessage.ask === "completion_result") {
if (completionTimeout) clearTimeout(completionTimeout)
if (checkInterval) clearInterval(checkInterval)
// Add newline after streaming output
console.log()
resolve({ success: true })
} else if (lastMessage.say === "error" || lastMessage.ask === "api_req_failed") {
if (completionTimeout) clearTimeout(completionTimeout)
if (checkInterval) clearInterval(checkInterval)
resolve({ success: false })
}
}
}
// Also set up a fallback check every 500ms in case state updates are slow
// checkInterval = setInterval(async () => {
// const state = await controller.getStateToPostToWebview()
// const lastMessage = state.clineMessages[state.clineMessages.length - 1]
// if (lastMessage) {
// if (
// lastMessage.say === "completion_result" ||
// lastMessage.ask === "plan_mode_respond" ||
// lastMessage.ask === "completion_result"
// ) {
// if (checkInterval) clearInterval(checkInterval)
// if (completionTimeout) clearTimeout(completionTimeout)
// console.log()
// resolve({ success: true })
// } else if (lastMessage.say === "error" || lastMessage.ask === "api_req_failed") {
// if (checkInterval) clearInterval(checkInterval)
// if (completionTimeout) clearTimeout(completionTimeout)
// resolve({ success: false })
// }
// }
// }, 500)
// Safety timeout - resolve after 10 minutes
completionTimeout = setTimeout(
() => {
if (checkInterval) clearInterval(checkInterval)
resolve({ success: false })
},
10 * 60 * 1000,
)
})
if (result.success) {
printSuccess("Task completed!")
} else {
process.exit(1)
}
} catch (error) {
printError(`Task failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
} finally {
// Cleanup
restoreConsole()
await controller.dispose()
await ErrorService.get().dispose()
}
}
/**
* List task history
*/
async function listHistory(options: { config?: string; limit?: number }) {
const { extensionContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
clineDir: options.config,
})
await ErrorService.initialize()
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR, process.cwd())
await StateManager.initialize(extensionContext)
const stateManager = StateManager.get()
const taskHistory = stateManager.getGlobalStateKey("taskHistory") || []
const limit = options.limit || 10
const recentTasks = [...taskHistory.slice(0, limit)]?.reverse()
if (recentTasks.length === 0) {
printInfo("No task history found.")
return
}
print(style.bold(`\n📜 Task History (${recentTasks.length} most recent):\n`))
print(separator())
for (const task of recentTasks) {
const date = new Date(task.ts).toLocaleString()
const taskText = task.task?.substring(0, 60) || "Unknown task"
const truncated = (task.task?.length || 0) > 60 ? "..." : ""
print(`${style.dim(date)}`)
print(` ${style.info(task.id)}`)
print(` ${taskText}${truncated}`)
if (task.totalCost) {
print(` ${style.dim(`Cost: $${task.totalCost.toFixed(4)}`)}`)
}
print("")
}
print(separator())
}
/**
* Show current configuration
*/
async function showConfig(options: { config?: string }) {
const { extensionContext, DATA_DIR } = initializeCliContext({
clineDir: options.config,
})
await ErrorService.initialize()
setupHostProvider(extensionContext, DATA_DIR, DATA_DIR, process.cwd())
await StateManager.initialize(extensionContext)
const stateManager = StateManager.get()
print(style.bold(`\n⚙️ Cline Configuration:\n`))
print(separator())
print(`Data directory: ${style.path(DATA_DIR)}`)
print(separator())
// Get all global state and workspace state entries
const globalStateEntries = stateManager.getAllGlobalStateEntries()
const workspaceStateEntries = stateManager.getAllWorkspaceStateEntries()
const apiConfig = stateManager.getApiConfiguration()
const EXCLUDED_KEYS = ["taskHistory"]
const shouldExcluded = (key: string, value: any): boolean => {
if (EXCLUDED_KEYS.includes(key)) return true
if (key.endsWith("Toggles")) return true
if (key.startsWith("apiConfig_")) return true
if (apiConfig[key as keyof typeof apiConfig] !== undefined) return true
if (!value) return true
if (typeof value === "object" && Object.keys(value).length === 0) return true
if (Array.isArray(value) && value.length === 0) return true
if (typeof value === "string" && value.trim() === "") return true
return false
}
if (Object.keys(globalStateEntries).length > 0) {
print(style.bold(`\nGlobal State:\n`))
for (const [key, value] of Object.entries(globalStateEntries)) {
if (shouldExcluded(key, value)) continue
const displayValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
print(`${style.info(key)}: ${displayValue}`)
}
print("")
}
if (Object.keys(workspaceStateEntries).length > 0) {
print(style.bold(`\nWorkspace State:\n`))
for (const [key, value] of Object.entries(workspaceStateEntries)) {
if (shouldExcluded(key, value)) continue
const displayValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
print(`${style.info(key)}: ${displayValue}`)
}
print("")
}
print(separator())
}
// Setup CLI commands
const program = new Command()
program.name("cline").description("Cline CLI - AI coding assistant in your terminal").version(VERSION)
program
.command("task")
.alias("t")
.description("Run a new task")
.argument("<prompt>", "The task prompt")
.option("-s, --switch <mode>", "Switch mode: act, plan")
.option("-m, --model <model>", "Model to use for the task")
.option("-v, --verbose", "Show verbose output including reasoning")
.option("-c, --cwd <path>", "Working directory for the task")
.option("--config <path>", "Path to Cline configuration directory")
.action(runTask)
program
.command("history")
.alias("h")
.description("List task history")
.option("-n, --limit <number>", "Number of tasks to show", "10")
.option("--config <path>", "Path to Cline configuration directory")
.action(listHistory)
program
.command("config")
.description("Show current configuration")
.option("--config <path>", "Path to Cline configuration directory")
.action(showConfig)
program
.command("auth")
.description("Authenticate a provider and configure what model is used")
.option("-p, --provider <id>", "Provider ID for quick setup (e.g., openai-native, anthropic)")
.option("-k, --apikey <key>", "API key for the provider")
.option("-m, --modelid <id>", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
.option("-b, --baseurl <url>", "Base URL (optional, only for openai provider)")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory for the task")
.option("--config <path>", "Path to Cline configuration directory")
.action(runAuth)
// Interactive mode (default when no command given)
program
.argument("[prompt]", "Task prompt (starts task immediately)")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory")
.option("--config <path>", "Configuration directory")
.action(async (prompt, options) => {
if (prompt) {
await runTask(prompt, options)
} else {
// Show help if no prompt given
program.help()
}
})
// Parse and run
program.parse()
+74
View File
@@ -0,0 +1,74 @@
/**
* Suppress noisy console output during module initialization.
* This file must be imported first (before any other imports) to take effect.
*
* This prevents variant configuration warnings and other debug logs from cluttering the CLI.
*/
// Save original console methods
const originalConsoleWarn = console.warn
const originalConsoleLog = console.log
const originalConsoleError = console.error
// Patterns of messages we want to suppress in non-verbose mode
const suppressedPatterns = [
/variant configuration warnings/i,
/BannerService/i,
/TelemetryProviderFactory/i,
/TelemetryService/i,
/NoOpTelemetryProvider/i,
/Telemetry ID/i,
/Telemetry distinct ID/i,
/WorkspaceManager/i,
/CheckpointTracker/i,
/checkpoint/i,
/shadow git/i,
/Lock manager/i,
/Task lock/i,
/Registry health check/i,
/Component.*not found/i,
/\[CLI\]/i,
/punycode.*deprecated/i,
/No user found/i,
/authentication data found/i,
/legacy checkpoints/i,
/ClineProvider instantiated/i,
/CommandExecutor/i,
/StandaloneTerminalManager/i,
/Using HostProvider/i,
/Cline API Error/i,
]
function shouldSuppress(args: unknown[]): boolean {
const message = args.map((a) => String(a)).join(" ")
return suppressedPatterns.some((pattern) => pattern.test(message))
}
// Check if we're in verbose mode by looking at command line args
const isVerboseMode = process.argv.includes("-v") || process.argv.includes("--verbose")
if (!isVerboseMode) {
console.warn = (...args: unknown[]) => {
if (!shouldSuppress(args)) {
originalConsoleWarn.apply(console, args as [unknown?, ...unknown[]])
}
}
console.log = (...args: unknown[]) => {
if (!shouldSuppress(args)) {
originalConsoleLog.apply(console, args as [unknown?, ...unknown[]])
}
}
// Keep errors visible but filter some noisy ones
console.error = (...args: unknown[]) => {
if (!shouldSuppress(args)) {
originalConsoleError.apply(console, args as [unknown?, ...unknown[]])
}
}
}
// Export a function to restore original console methods if needed
export function restoreConsole() {
console.warn = originalConsoleWarn
console.log = originalConsoleLog
console.error = originalConsoleError
}
+7
View File
@@ -0,0 +1,7 @@
export function jsonParseSafe<T>(data: string, defaultValue: T): T {
try {
return JSON.parse(data) as T
} catch {
return defaultValue
}
}
+255
View File
@@ -0,0 +1,255 @@
/**
* VSCode context stub for CLI mode
* Provides mock implementations of VSCode extension context
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
import os from "os"
import path from "path"
import type { Extension, ExtensionContext, Memento, SecretStorage } from "vscode"
import { ExtensionRegistryInfo } from "@/registry"
import { ExtensionKind, ExtensionMode, URI } from "./vscode-shim"
const SETTINGS_SUBFOLDER = "data"
/**
* Simple file-based Memento store for persisting state
*/
class MementoStore implements Memento {
private data: Record<string, any> = {}
private filePath: string
constructor(filePath: string) {
this.filePath = filePath
this.load()
}
private load() {
try {
if (existsSync(this.filePath)) {
const content = readFileSync(this.filePath, "utf8")
this.data = JSON.parse(content)
}
} catch (error) {
console.error(`Failed to load state from ${this.filePath}:`, error)
this.data = {}
}
}
private save() {
try {
mkdirSync(path.dirname(this.filePath), { recursive: true })
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2))
} catch (error) {
console.error(`Failed to save state to ${this.filePath}:`, error)
}
}
keys(): readonly string[] {
return Object.keys(this.data)
}
get<T>(key: string): T | undefined
get<T>(key: string, defaultValue: T): T
get<T>(key: string, defaultValue?: T): T | undefined {
const value = this.data[key]
return value !== undefined ? value : defaultValue
}
async update(key: string, value: any): Promise<void> {
if (value === undefined) {
delete this.data[key]
} else {
this.data[key] = value
}
this.save()
}
setKeysForSync(_keys: readonly string[]): void {
// No-op for CLI
}
}
/**
* Simple file-based secret storage
*/
class SecretStore implements SecretStorage {
private data: Record<string, string> = {}
private filePath: string
private onDidChangeEmitter = {
event: () => ({ dispose: () => {} }),
fire: (_e: any) => {},
dispose: () => {},
}
onDidChange = this.onDidChangeEmitter.event
constructor(filePath: string) {
this.filePath = filePath
this.load()
}
private load() {
try {
if (existsSync(this.filePath)) {
const content = readFileSync(this.filePath, "utf8")
this.data = JSON.parse(content)
}
} catch (error) {
this.data = {}
}
}
private save() {
try {
mkdirSync(path.dirname(this.filePath), { recursive: true })
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2))
} catch (error) {
console.error(`Failed to save secrets:`, error)
}
}
async get(key: string): Promise<string | undefined> {
return this.data[key]
}
async store(key: string, value: string): Promise<void> {
this.data[key] = value
this.save()
}
async delete(key: string): Promise<void> {
delete this.data[key]
this.save()
}
}
/**
* Mock environment variable collection
*/
class EnvironmentVariableCollection {
private variables: Map<string, any> = new Map()
persistent = true
description = "CLI Environment Variables"
entries(): IterableIterator<[string, any]> {
return this.variables.entries()
}
replace(variable: string, value: string) {
this.variables.set(variable, { value, type: "replace" })
}
append(variable: string, value: string) {
this.variables.set(variable, { value, type: "append" })
}
prepend(variable: string, value: string) {
this.variables.set(variable, { value, type: "prepend" })
}
get(variable: string) {
return this.variables.get(variable)
}
forEach(callback: (variable: string, mutator: any, collection: any) => void) {
this.variables.forEach((mutator, variable) => {
callback(variable, mutator, this)
})
}
delete(variable: string) {
return this.variables.delete(variable)
}
clear() {
this.variables.clear()
}
getScoped(_scope: any) {
return this
}
}
function readJson(filePath: string): any {
try {
if (existsSync(filePath)) {
return JSON.parse(readFileSync(filePath, "utf8"))
}
} catch (error) {
// Return empty object if file doesn't exist
}
return {}
}
export interface CliContextConfig {
clineDir?: string
workspaceDir?: string
}
/**
* Initialize the VSCode-like context for CLI mode
*/
export function initializeCliContext(config: CliContextConfig = {}) {
const CLINE_DIR = config.clineDir || process.env.CLINE_DIR || path.join(os.homedir(), ".cline")
const DATA_DIR = path.join(CLINE_DIR, SETTINGS_SUBFOLDER)
const WORKSPACE_STORAGE_DIR = config.workspaceDir || process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspace")
// Ensure directories exist
mkdirSync(DATA_DIR, { recursive: true })
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
console.log(`[CLI] Using data directory: ${DATA_DIR}`)
// For CLI, extension dir is the root of the project (parent of cli-ts)
const EXTENSION_DIR = path.resolve(__dirname, "..", "..")
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
const extension: Extension<void> = {
id: ExtensionRegistryInfo.id,
isActive: true,
extensionPath: EXTENSION_DIR,
extensionUri: URI.file(EXTENSION_DIR),
packageJSON: readJson(path.join(EXTENSION_DIR, "package.json")),
exports: undefined,
activate: async () => {},
extensionKind: ExtensionKind.UI,
}
const extensionContext: ExtensionContext = {
extension: extension,
extensionMode: EXTENSION_MODE,
// Set up KV stores
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
// Set up URIs
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
storagePath: WORKSPACE_STORAGE_DIR,
globalStorageUri: URI.file(DATA_DIR),
globalStoragePath: DATA_DIR,
// Logs
logUri: URI.file(DATA_DIR),
logPath: DATA_DIR,
extensionUri: URI.file(EXTENSION_DIR),
extensionPath: EXTENSION_DIR,
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
subscriptions: [],
environmentVariableCollection: new EnvironmentVariableCollection() as any,
// Workspace state
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
}
return {
extensionContext,
DATA_DIR,
EXTENSION_DIR,
WORKSPACE_STORAGE_DIR,
}
}
+277
View File
@@ -0,0 +1,277 @@
/**
* VSCode namespace shim for CLI mode
* Provides minimal stubs for VSCode types and enums used by the codebase
*/
// Re-export common types from vscode-uri for URI handling
export { URI } from "vscode-uri"
// Extension mode enum
export enum ExtensionMode {
Production = 1,
Development = 2,
Test = 3,
}
// Extension kind enum
export enum ExtensionKind {
UI = 1,
Workspace = 2,
}
// Diagnostic severity enum
export enum DiagnosticSeverity {
Error = 0,
Warning = 1,
Information = 2,
Hint = 3,
}
// End of line enum
export enum EndOfLine {
LF = 1,
CRLF = 2,
}
// Position class
export class Position {
constructor(
public readonly line: number,
public readonly character: number,
) {}
isAfter(other: Position): boolean {
return this.line > other.line || (this.line === other.line && this.character > other.character)
}
isAfterOrEqual(other: Position): boolean {
return this.line > other.line || (this.line === other.line && this.character >= other.character)
}
isBefore(other: Position): boolean {
return this.line < other.line || (this.line === other.line && this.character < other.character)
}
isBeforeOrEqual(other: Position): boolean {
return this.line < other.line || (this.line === other.line && this.character <= other.character)
}
isEqual(other: Position): boolean {
return this.line === other.line && this.character === other.character
}
translate(lineDelta?: number, characterDelta?: number): Position {
return new Position(this.line + (lineDelta || 0), this.character + (characterDelta || 0))
}
with(line?: number, character?: number): Position {
return new Position(line ?? this.line, character ?? this.character)
}
compareTo(other: Position): number {
if (this.line < other.line) return -1
if (this.line > other.line) return 1
if (this.character < other.character) return -1
if (this.character > other.character) return 1
return 0
}
}
// Range class
export class Range {
constructor(
public readonly start: Position,
public readonly end: Position,
)
constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number)
constructor(
startOrStartLine: Position | number,
endOrStartCharacter: Position | number,
endLine?: number,
endCharacter?: number,
) {
if (typeof startOrStartLine === "number") {
this.start = new Position(startOrStartLine, endOrStartCharacter as number)
this.end = new Position(endLine!, endCharacter!)
} else {
this.start = startOrStartLine
this.end = endOrStartCharacter as Position
}
}
get isEmpty(): boolean {
return this.start.isEqual(this.end)
}
get isSingleLine(): boolean {
return this.start.line === this.end.line
}
contains(positionOrRange: Position | Range): boolean {
if (positionOrRange instanceof Range) {
return this.contains(positionOrRange.start) && this.contains(positionOrRange.end)
}
return positionOrRange.isAfterOrEqual(this.start) && positionOrRange.isBeforeOrEqual(this.end)
}
isEqual(other: Range): boolean {
return this.start.isEqual(other.start) && this.end.isEqual(other.end)
}
intersection(range: Range): Range | undefined {
const start = Position.prototype.isAfter.call(this.start, range.start) ? this.start : range.start
const end = Position.prototype.isBefore.call(this.end, range.end) ? this.end : range.end
if (start.isAfter(end)) {
return undefined
}
return new Range(start, end)
}
union(other: Range): Range {
const start = this.start.isBefore(other.start) ? this.start : other.start
const end = this.end.isAfter(other.end) ? this.end : other.end
return new Range(start, end)
}
with(start?: Position, end?: Position): Range {
return new Range(start ?? this.start, end ?? this.end)
}
}
// Selection class (extends Range)
export class Selection extends Range {
constructor(
public readonly anchor: Position,
public readonly active: Position,
)
constructor(anchorLine: number, anchorCharacter: number, activeLine: number, activeCharacter: number)
constructor(
anchorOrAnchorLine: Position | number,
activeOrAnchorCharacter: Position | number,
activeLine?: number,
activeCharacter?: number,
) {
if (typeof anchorOrAnchorLine === "number") {
const anchor = new Position(anchorOrAnchorLine, activeOrAnchorCharacter as number)
const active = new Position(activeLine!, activeCharacter!)
super(anchor.isBefore(active) ? anchor : active, anchor.isBefore(active) ? active : anchor)
this.anchor = anchor
this.active = active
} else {
const anchor = anchorOrAnchorLine
const active = activeOrAnchorCharacter as Position
super(anchor.isBefore(active) ? anchor : active, anchor.isBefore(active) ? active : anchor)
this.anchor = anchor
this.active = active
}
}
get isReversed(): boolean {
return this.anchor.isAfter(this.active)
}
}
// Cancellation token
export interface CancellationToken {
isCancellationRequested: boolean
onCancellationRequested: any
}
// Event emitter (simplified)
export class EventEmitter<T> {
private listeners: Array<(e: T) => void> = []
event = (listener: (e: T) => void) => {
this.listeners.push(listener)
return {
dispose: () => {
const index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
},
}
}
fire(data: T): void {
for (const listener of this.listeners) {
listener(data)
}
}
dispose(): void {
this.listeners = []
}
}
// Disposable
export class Disposable {
constructor(private callOnDispose: () => void) {}
static from(...disposables: { dispose(): any }[]): Disposable {
return new Disposable(() => {
for (const d of disposables) {
d.dispose()
}
})
}
dispose(): void {
this.callOnDispose()
}
}
// Minimal workspace namespace
export const workspace = {
workspaceFolders: undefined as any[] | undefined,
getWorkspaceFolder: (_uri: any) => undefined,
onDidChangeWorkspaceFolders: () => ({ dispose: () => {} }),
fs: {
readFile: async (_uri: any): Promise<Uint8Array> => new Uint8Array(),
writeFile: async (_uri: any, _content: Uint8Array): Promise<void> => {},
delete: async (_uri: any): Promise<void> => {},
stat: async (_uri: any): Promise<any> => ({ type: 1, size: 0 }),
readDirectory: async (_uri: any): Promise<any[]> => [],
createDirectory: async (_uri: any): Promise<void> => {},
},
}
// Minimal window namespace
export const window = {
showInformationMessage: async (message: string) => {
console.log(`[INFO] ${message}`)
return undefined
},
showWarningMessage: async (message: string) => {
console.warn(`[WARN] ${message}`)
return undefined
},
showErrorMessage: async (message: string) => {
console.error(`[ERROR] ${message}`)
return undefined
},
createOutputChannel: (_name: string) => ({
appendLine: (line: string) => console.log(line),
append: (text: string) => process.stdout.write(text),
clear: () => {},
show: () => {},
hide: () => {},
dispose: () => {},
}),
terminals: [] as any[],
activeTerminal: undefined as any,
createTerminal: (_options?: any) => ({
name: "CLI Terminal",
processId: Promise.resolve(process.pid),
sendText: (text: string) => console.log(`[Terminal] ${text}`),
show: () => {},
hide: () => {},
dispose: () => {},
}),
}
// Export types that are commonly used
export type ExtensionContext = any
export type Memento = any
export type SecretStorage = any
export type Extension<T> = any
+71
View File
@@ -0,0 +1,71 @@
{
"compilerOptions": {
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"lib": [
"es2022"
],
"module": "esnext",
"moduleResolution": "Bundler",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"resolveJsonModule": true,
"rootDir": ".",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2022",
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false,
"baseUrl": "..",
"paths": {
"@/*": [
"src/*"
],
"@api/*": [
"src/core/api/*"
],
"@core/*": [
"src/core/*"
],
"@generated/*": [
"src/generated/*"
],
"@hosts/*": [
"src/hosts/*"
],
"@integrations/*": [
"src/integrations/*"
],
"@packages/*": [
"src/packages/*"
],
"@services/*": [
"src/services/*"
],
"@shared/*": [
"src/shared/*"
],
"@utils/*": [
"src/utils/*"
]
},
"outDir": "dist"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": ".."
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"workflowToggles": {},
"localClineRulesToggles": {},
"localWindsurfRulesToggles": {},
"localCursorRulesToggles": {},
"localAgentsRulesToggles": {}
}
+4
View File
@@ -344,6 +344,10 @@
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-standalone-npm": "npm run protos && npm run protos-go && npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-cli": "scripts/build-cli.sh",
"compile-cli-ts": "cd cli-ts && npm run build",
"compile-cli-ts:production": "cd cli-ts && npm run build:production",
"watch-cli-ts": "cd cli-ts && npm run watch",
"dev:cli-ts": "npm run compile-cli-ts && npm run watch-cli-ts",
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
"test:install": "bash scripts/test-install.sh",
+36 -35
View File
@@ -1,4 +1,5 @@
import { ModelInfo } from "@shared/api"
import { Logger } from "@/services/logging/Logger"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler } from "../../core/api/index"
@@ -20,7 +21,7 @@ export class DifyHandler implements ApiHandler {
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
console.log("[DIFY DEBUG] Constructor called with:", {
Logger.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
@@ -34,7 +35,7 @@ export class DifyHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
Logger.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
@@ -51,9 +52,9 @@ export class DifyHandler implements ApiHandler {
}
const fullUrl = `${this.baseUrl}/chat-messages`
console.log("[DIFY DEBUG] Making request to:", fullUrl)
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
console.log("[DIFY DEBUG] Current process environment variables (for proxy debugging):", process.env)
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
Logger.log("[DIFY DEBUG] Current process environment variables (for proxy debugging):", process.env)
let response: Response
try {
@@ -66,22 +67,22 @@ export class DifyHandler implements ApiHandler {
body: JSON.stringify(requestBody),
})
} catch (error: any) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
// Log more detailed error information if available (e.g., from undici)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
}
console.log("[DIFY DEBUG] Response status:", response.status)
Logger.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
console.log("[DIFY DEBUG] Response headers:", headersObj)
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
console.error("[DIFY DEBUG] Error response:", errorText)
Logger.debug("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
@@ -94,18 +95,18 @@ export class DifyHandler implements ApiHandler {
let buffer = ""
let fullText = ""
console.log("[DIFY DEBUG] Starting to read streaming response...")
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log("[DIFY DEBUG] Stream ended naturally")
Logger.log("[DIFY DEBUG] Stream ended naturally")
break
}
const chunk = decoder.decode(value, { stream: true })
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
@@ -114,56 +115,56 @@ export class DifyHandler implements ApiHandler {
buffer = lines.pop() || ""
for (const line of lines) {
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
console.log("[DIFY DEBUG] Received [DONE] signal")
Logger.log("[DIFY DEBUG] Received [DONE] signal")
return // Explicitly return on [DONE]
}
if (data === "") {
console.log("[DIFY DEBUG] Empty data line, skipping")
Logger.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
// Capture conversation_id as soon as it's available
if (parsed.conversation_id && !this.conversationId) {
this.conversationId = parsed.conversation_id
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
// Dify sends the full text in each "answer" chunk, so we replace.
if (typeof parsed.answer === "string") {
fullText = parsed.answer
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
}
} else if (parsed.event === "message_replace") {
console.log("[DIFY DEBUG] Replace message event:", parsed)
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
}
} else if (parsed.event === "message_end") {
console.log("[DIFY DEBUG] Message end event", parsed)
Logger.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
@@ -182,19 +183,19 @@ export class DifyHandler implements ApiHandler {
}
return // End of stream
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Error event:", parsed)
Logger.error("[DIFY DEBUG] Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
// Try to extract text from other possible fields
if (parsed.text) {
fullText += parsed.text
@@ -211,17 +212,17 @@ export class DifyHandler implements ApiHandler {
}
}
} catch (e) {
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
Logger.log("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
console.log(
Logger.log(
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
JSON.stringify(line),
)
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
try {
const parsed = JSON.parse(line.trim())
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
// Handle the same event types as above
if (parsed.event === "message" && parsed.answer) {
@@ -239,19 +240,19 @@ export class DifyHandler implements ApiHandler {
}
return
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
}
} catch (e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
}
} finally {
reader.releaseLock()
console.log("[DIFY DEBUG] Stream reader released")
Logger.log("[DIFY DEBUG] Stream reader released")
}
}
@@ -270,7 +271,7 @@ export class DifyHandler implements ApiHandler {
// Only prepend the system prompt if it's the very first message of a new conversation.
if (!this.conversationId && systemPrompt) {
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
+38 -37
View File
@@ -1,3 +1,4 @@
import { Logger } from "@/services/logging/Logger"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ModelInfo } from "../../../shared/api"
@@ -84,7 +85,7 @@ export class DifyHandler implements ApiHandler {
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
console.log("[DIFY DEBUG] Constructor called with:", {
Logger.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
@@ -98,7 +99,7 @@ export class DifyHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
Logger.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
@@ -115,8 +116,8 @@ export class DifyHandler implements ApiHandler {
}
const fullUrl = `${this.baseUrl}/chat-messages`
console.log("[DIFY DEBUG] Making request to:", fullUrl)
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
let response: Response
try {
@@ -129,21 +130,21 @@ export class DifyHandler implements ApiHandler {
body: JSON.stringify(requestBody),
})
} catch (error: any) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
}
console.log("[DIFY DEBUG] Response status:", response.status)
Logger.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
console.log("[DIFY DEBUG] Response headers:", headersObj)
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
console.error("[DIFY DEBUG] Error response:", errorText)
Logger.debug("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
@@ -159,14 +160,14 @@ export class DifyHandler implements ApiHandler {
const processedEvents: string[] = []
let lastEventTime = Date.now()
console.log("[DIFY DEBUG] Starting to read streaming response...")
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log("[DIFY DEBUG] Stream ended naturally")
console.log(
Logger.log("[DIFY DEBUG] Stream ended naturally")
Logger.log(
"[DIFY DEBUG] Final state - hasYieldedContent:",
hasYieldedContent,
"fullText length:",
@@ -178,7 +179,7 @@ export class DifyHandler implements ApiHandler {
}
const chunk = decoder.decode(value, { stream: true })
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
@@ -187,41 +188,41 @@ export class DifyHandler implements ApiHandler {
buffer = lines.pop() || ""
for (const line of lines) {
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
console.log("[DIFY DEBUG] Received [DONE] signal")
Logger.log("[DIFY DEBUG] Received [DONE] signal")
break
}
if (data === "") {
console.log("[DIFY DEBUG] Empty data line, skipping")
Logger.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
processedEvents.push(parsed.event || "unknown")
lastEventTime = Date.now()
// Capture conversation_id as soon as it's available
if (parsed.conversation_id && !this.conversationId) {
this.conversationId = parsed.conversation_id
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
// Dify sends the full text in each "answer" chunk, so we replace.
if (typeof parsed.answer === "string") {
fullText = parsed.answer
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
@@ -229,10 +230,10 @@ export class DifyHandler implements ApiHandler {
hasYieldedContent = true
}
} else if (parsed.event === "message_replace") {
console.log("[DIFY DEBUG] Replace message event:", parsed)
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
@@ -240,7 +241,7 @@ export class DifyHandler implements ApiHandler {
hasYieldedContent = true
}
} else if (parsed.event === "message_end") {
console.log("[DIFY DEBUG] Message end event", parsed)
Logger.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
@@ -260,19 +261,19 @@ export class DifyHandler implements ApiHandler {
}
return // End of stream
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Error event:", parsed)
Logger.error("[DIFY DEBUG] Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
// Try to extract text from other possible fields
if (parsed.text) {
fullText += parsed.text
@@ -299,17 +300,17 @@ export class DifyHandler implements ApiHandler {
}
}
} catch (e) {
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
Logger.info("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
console.log(
Logger.log(
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
JSON.stringify(line),
)
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
try {
const parsed = JSON.parse(line.trim())
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
processedEvents.push(parsed.event || "direct-json")
// Handle the same event types as above
@@ -330,7 +331,7 @@ export class DifyHandler implements ApiHandler {
}
return
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.answer || parsed.text || parsed.content) {
// Fallback for any content in direct JSON
@@ -344,7 +345,7 @@ export class DifyHandler implements ApiHandler {
}
} catch (e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
@@ -359,11 +360,11 @@ export class DifyHandler implements ApiHandler {
streamDuration: Date.now() - lastEventTime,
conversationId: this.conversationId,
}
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
Logger.info("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
// If we have any accumulated text at all, yield it as a fallback
if (fullText.trim()) {
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
yield {
type: "text",
text: fullText,
@@ -380,7 +381,7 @@ export class DifyHandler implements ApiHandler {
}
} finally {
reader.releaseLock()
console.log("[DIFY DEBUG] Stream reader released")
Logger.log("[DIFY DEBUG] Stream reader released")
}
}
@@ -399,7 +400,7 @@ export class DifyHandler implements ApiHandler {
// Only prepend the system prompt if it's the very first message of a new conversation.
if (!this.conversationId && systemPrompt) {
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
@@ -2,6 +2,7 @@ import { CheckpointRestoreRequest } from "@shared/proto/cline/checkpoints"
import { Empty } from "@shared/proto/cline/common"
import pWaitFor from "p-wait-for"
import { HostProvider } from "@/hosts/host-provider"
import { Logger } from "@/services/logging/Logger"
import { ShowMessageType } from "@/shared/proto/index.host"
import { ClineCheckpointRestore } from "../../../shared/WebviewMessage"
import { Controller } from ".."
@@ -14,7 +15,7 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
await pWaitFor(() => controller.task?.taskState.isInitialized === true, {
timeout: 3_000,
}).catch((error) => {
console.log("Failed to init new Cline instance to restore checkpoint", error)
Logger.log("Failed to init new Cline instance to restore checkpoint", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to restore checkpoint",
+2 -1
View File
@@ -1,5 +1,6 @@
import { getFileMentionFromPath } from "@/core/mentions"
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { CommandContext, Empty } from "@/shared/proto/index.cline"
import { Controller } from "../index"
@@ -23,7 +24,7 @@ export async function addToCline(controller: Controller, request: CommandContext
await sendAddToInputEvent(input)
console.log("addToCline", request.selectedText, filePath, request.language)
Logger.log("addToCline", request.selectedText, filePath, request.language)
telemetryService.captureButtonClick("codeAction_addToChat", controller.task?.ulid)
return {}
+8 -7
View File
@@ -33,6 +33,7 @@ import { LogoutReason } from "@/services/auth/types"
import { BannerService } from "@/services/banner/BannerService"
import { featureFlagsService } from "@/services/feature-flags"
import { getDistinctId } from "@/services/logging/distinctId"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { BannerCardData } from "@/shared/cline/banner"
import { getAxiosSettings } from "@/shared/net"
@@ -417,7 +418,7 @@ export class Controller {
async cancelTask() {
// Prevent duplicate cancellations from spam clicking
if (this.cancelInProgress) {
console.log(`[Controller.cancelTask] Cancellation already in progress, ignoring duplicate request`)
Logger.log(`[Controller.cancelTask] Cancellation already in progress, ignoring duplicate request`)
return
}
@@ -434,7 +435,7 @@ export class Controller {
try {
await this.task.abortTask()
} catch (error) {
console.error("Failed to abort task", error)
Logger.error("Failed to abort task", error)
}
await pWaitFor(
@@ -447,7 +448,7 @@ export class Controller {
timeout: 3_000,
},
).catch(() => {
console.error("Failed to abort task")
Logger.error("Failed to abort task")
})
if (this.task) {
@@ -466,7 +467,7 @@ export class Controller {
} catch (error) {
// Task not in history yet (new task with no messages); catch the
// error to enable the agent to continue making progress.
console.log(`[Controller.cancelTask] Task not found in history: ${error}`)
Logger.log(`[Controller.cancelTask] Task not found in history: ${error}`)
}
// Only re-initialize if we found a history item, otherwise just clear
@@ -542,7 +543,7 @@ export class Controller {
await this.postStateToWebview()
} catch (error) {
console.error("Failed to handle auth callback:", error)
Logger.error("Failed to handle auth callback:", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to log in to Cline",
@@ -787,7 +788,7 @@ export class Controller {
async exportTaskWithId(id: string) {
const { taskDirPath } = await this.getTaskWithId(id)
console.log(`[EXPORT] Opening task directory: ${taskDirPath}`)
Logger.log(`[EXPORT] Opening task directory: ${taskDirPath}`)
await open(taskDirPath)
}
@@ -1011,7 +1012,7 @@ export class Controller {
try {
return BannerService.get().getActiveBanners()
} catch (err) {
console.log(err)
Logger.log(err)
return []
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { StringRequest } from "@shared/proto/cline/common"
import { McpDownloadResponse } from "@shared/proto/cline/mcp"
import axios from "axios"
import { ClineEnv } from "@/config"
import { Logger } from "@/services/logging/Logger"
import { getAxiosSettings } from "@/shared/net"
import { Controller } from ".."
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
@@ -45,7 +46,7 @@ export async function downloadMcp(controller: Controller, request: StringRequest
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
Logger.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
@@ -1,4 +1,5 @@
import type { EmptyRequest, String as ProtoString } from "@shared/proto/cline/common"
import { Logger } from "@/services/logging/Logger"
import { getRequestRegistry, type StreamingResponseHandler } from "../grpc-handler"
import type { Controller } from "../index"
@@ -18,7 +19,7 @@ export async function subscribeToAddToInput(
responseStream: StreamingResponseHandler<ProtoString>,
requestId?: string,
): Promise<void> {
console.log("[DEBUG] set up addToInput subscription")
Logger.log("[DEBUG] set up addToInput subscription")
// Add this subscription to the active subscriptions
activeAddToInputSubscriptions.add(responseStream)
@@ -26,7 +27,7 @@ export async function subscribeToAddToInput(
// Register cleanup when the connection is closed
const cleanup = () => {
activeAddToInputSubscriptions.delete(responseStream)
console.log("[DEBUG] Cleaned up addToInput subscription")
Logger.log("[DEBUG] Cleaned up addToInput subscription")
}
// Register the cleanup function with the request registry if we have a requestId
@@ -50,9 +51,9 @@ export async function sendAddToInputEvent(text: string): Promise<void> {
event,
false, // Not the last message
)
console.log("[DEBUG] sending addToInput event", text.length, "chars")
Logger.log("[DEBUG] sending addToInput event", text.length, "chars")
} catch (error) {
console.error("Error sending addToInput event:", error)
Logger.error("Error sending addToInput event:", error)
// Remove the subscription if there was an error
activeAddToInputSubscriptions.delete(responseStream)
}
@@ -1,4 +1,5 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/services/logging/Logger"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
@@ -18,7 +19,7 @@ export async function subscribeToChatButtonClicked(
responseStream: StreamingResponseHandler<Empty>,
requestId?: string,
): Promise<void> {
console.log(`[DEBUG] set up chatButtonClicked subscription`)
Logger.log(`[DEBUG] set up chatButtonClicked subscription`)
// Add this subscription to the active subscriptions
activeChatButtonClickedSubscriptions.add(responseStream)
@@ -1,4 +1,5 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/services/logging/Logger"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
@@ -18,7 +19,7 @@ export async function subscribeToMcpButtonClicked(
responseStream: StreamingResponseHandler<Empty>,
requestId?: string,
): Promise<void> {
console.log(`[DEBUG] set up mcpButtonClicked subscription`)
Logger.log(`[DEBUG] set up mcpButtonClicked subscription`)
// Add this subscription to the active subscriptions
activeMcpButtonClickedSubscriptions.add(responseStream)
+2 -1
View File
@@ -1,3 +1,4 @@
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "../../services/telemetry"
import { getAllHooksDirs } from "../storage/disk"
import { HookFactory, Hooks } from "./hook-factory"
@@ -292,7 +293,7 @@ export class HookDiscoveryCache {
*/
private log(message: string): void {
if (this.debug) {
console.log(`[HookCache] ${message}`)
Logger.log(`[HookCache] ${message}`)
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { Logger } from "@/services/logging/Logger"
import { HookProcess } from "./HookProcess"
/**
@@ -39,7 +40,7 @@ export class HookProcessRegistry {
static async terminateAll(): Promise<void> {
const processes = Array.from(HookProcessRegistry.activeProcesses)
if (processes.length > 0) {
console.log(`[HookProcessRegistry] Terminating ${processes.length} active hook process(es)`)
Logger.log(`[HookProcessRegistry] Terminating ${processes.length} active hook process(es)`)
await Promise.all(processes.map((p) => p.terminate()))
HookProcessRegistry.activeProcesses.clear()
}
+2 -1
View File
@@ -1,6 +1,7 @@
import type { HookOutputStreamMeta } from "@shared/ExtensionMessage"
import { ClineMessage } from "@shared/ExtensionMessage"
import type { HookOutput } from "@shared/proto/cline/hooks"
import { Logger } from "@/services/logging/Logger"
import { MessageStateHandler } from "../task/message-state"
import { HookExecutionError } from "./HookError"
import { HookFactory } from "./hook-factory"
@@ -151,7 +152,7 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
...hookInput,
})
console.log(`[${hookName} Hook]`, result)
Logger.log(`[${hookName} Hook]`, result)
// NoOp hooks return proto defaults; preserve the minimal legacy return shape.
if (result.cancel === false && result.contextModification === "" && result.errorMessage === "") {
+3 -2
View File
@@ -1,6 +1,7 @@
import { findLastIndex } from "@shared/array"
import type { ClineMessage } from "@shared/ExtensionMessage"
import type { ClineStorageMessage } from "@shared/messages/content"
import { Logger } from "@/services/logging/Logger"
import type { ContextManager } from "../context/context-management/ContextManager"
import type { MessageStateHandler } from "../task/message-state"
@@ -246,7 +247,7 @@ export async function executePreCompactHookWithCleanup(params: PreCompactHookPar
if (preCompactResult.cancel === true) {
// Log cancellation for debugging
const cancellationSource = preCompactResult.wasCancelled ? "user" : "PreCompact hook"
console.log(`[PreCompact] Context compaction cancelled by ${cancellationSource} for task ${params.taskId}`)
Logger.log(`[PreCompact] Context compaction cancelled by ${cancellationSource} for task ${params.taskId}`)
// Internalized cancellation state management (replaces handleCancellation callback)
// Always save state before cancelling, regardless of cancellation source
@@ -266,7 +267,7 @@ export async function executePreCompactHookWithCleanup(params: PreCompactHookPar
// Hook completed successfully - log if context modification provided
if (preCompactResult.contextModification) {
console.log(`[PreCompact] Hook provided context modification for task ${params.taskId}`)
Logger.log(`[PreCompact] Hook provided context modification for task ${params.taskId}`)
}
return {
@@ -10,13 +10,13 @@
* 3. Use the builder pattern for type safety
* 4. Run validation to ensure correctness
*/
import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { PromptVariant } from ".."
import { SystemPromptSection } from "../templates/placeholders"
import { baseTemplate } from "./generic/template"
import { createVariant } from "./variant-builder"
import { validateVariant } from "./variant-validator"
// Type-safe variant configuration using the builder pattern
export const config: Omit<PromptVariant, "id"> = createVariant(ModelFamily.GENERIC) // Change to your target model family
@@ -82,17 +82,6 @@ export const config: Omit<PromptVariant, "id"> = createVariant(ModelFamily.GENER
// })
.build()
// Compile-time validation (optional but recommended)
const validationResult = validateVariant({ ...config, id: "template" }, { strict: true })
if (!validationResult.isValid) {
console.error("Variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type VariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ClineDefaultTool } from "@/shared/tools"
import { isDevstralModelFamily } from "@/utils/model-utils"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { DEVSTRAL_AGENT_ROLE_TEMPLATE } from "./overrides"
import { baseTemplate } from "./template"
@@ -63,16 +62,5 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
})
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: "devstral" }, { strict: true })
if (!validationResult.isValid) {
console.error("Devstral variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid Devstral variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Devstral variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type DevstralVariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ClineDefaultTool } from "@/shared/tools"
import { isGemini3ModelFamily, isNextGenModelProvider } from "@/utils/model-utils"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { gemini3ComponentOverrides } from "./overrides"
import { baseTemplate } from "./template"
@@ -82,16 +81,5 @@ export const config = createVariant(ModelFamily.GEMINI_3)
.overrideComponent(SystemPromptSection.TASK_PROGRESS, gemini3ComponentOverrides[SystemPromptSection.TASK_PROGRESS]!)
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: "gemini3" }, { strict: true })
if (!validationResult.isValid) {
console.error("Gemini 3.0 variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid Gemini 3.0 variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Gemini 3.0 variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type Gemini3VariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { baseTemplate } from "./template"
export const config = createVariant(ModelFamily.GENERIC)
@@ -72,16 +71,5 @@ export const config = createVariant(ModelFamily.GENERIC)
.config({})
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: "generic" }, { strict: true })
if (!validationResult.isValid) {
console.error("Generic variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid generic variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Generic variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type GenericVariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ClineDefaultTool } from "@/shared/tools"
import { isGLMModelFamily } from "@/utils/model-utils"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { glmComponentOverrides } from "./overrides"
import { baseTemplate } from "./template"
@@ -66,16 +65,5 @@ export const config = createVariant(ModelFamily.GLM)
.overrideComponent(SystemPromptSection.MCP, glmComponentOverrides[SystemPromptSection.MCP])
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: "glm" }, { strict: true })
if (!validationResult.isValid) {
console.error("GLM variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid GLM variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("GLM variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type GLMVariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { GPT_5_TEMPLATE_OVERRIDES } from "./template"
// Type-safe variant configuration using the builder pattern
@@ -75,16 +74,5 @@ export const config = createVariant(ModelFamily.GPT_5)
})
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.GPT_5 }, { strict: true })
if (!validationResult.isValid) {
console.error("GPT-5 variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid GPT-5 variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("GPT-5 variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type GPT5VariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ClineDefaultTool } from "@/shared/tools"
import { isHermesModelFamily } from "@/utils/model-utils"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { hermesComponentOverrides } from "./overrides"
import { baseTemplate } from "./template"
@@ -69,16 +68,5 @@ export const config = createVariant(ModelFamily.HERMES)
.overrideComponent(SystemPromptSection.MCP, hermesComponentOverrides[SystemPromptSection.MCP])
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: "hermes" }, { strict: true })
if (!validationResult.isValid) {
console.error("Hermes variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid Hermes variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Hermes variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type HermesVariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { gpt51ComponentOverrides } from "./overrides"
import { GPT_5_1_TEMPLATE_OVERRIDES } from "./template"
@@ -85,16 +84,5 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
.overrideComponent(SystemPromptSection.FEEDBACK, gpt51ComponentOverrides[SystemPromptSection.FEEDBACK]!)
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.NATIVE_GPT_5_1 }, { strict: true })
if (!validationResult.isValid) {
console.error("GPT-5-1 variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid GPT-5-1 variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("GPT-5-1 variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type GPT51VariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { GPT_5_TEMPLATE_OVERRIDES } from "./template"
// Type-safe variant configuration using the builder pattern
@@ -95,16 +94,5 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
})
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.NATIVE_GPT_5 }, { strict: true })
if (!validationResult.isValid) {
console.error("GPT-5 variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid GPT-5 variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("GPT-5 variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type GPT5VariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { TEMPLATE_OVERRIDES } from "./template"
// Type-safe variant configuration using the builder pattern
@@ -85,16 +84,5 @@ export const config = createVariant(ModelFamily.NATIVE_NEXT_GEN)
})
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.NATIVE_NEXT_GEN }, { strict: true })
if (!validationResult.isValid) {
console.error("Native Next Gen variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid Native Next Gen variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Native Next Gen variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type NativeNextGenVariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { baseTemplate, rules_template } from "./template"
// Type-safe variant configuration using the builder pattern
@@ -78,16 +77,5 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
})
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.NEXT_GEN }, { strict: true })
if (!validationResult.isValid) {
console.error("Next-gen variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid next-gen variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("Next-gen variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type NextGenVariantConfig = typeof config
@@ -3,7 +3,6 @@ import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { SystemPromptSection } from "../../templates/placeholders"
import { createVariant } from "../variant-builder"
import { validateVariant } from "../variant-validator"
import { xsComponentOverrides } from "./overrides"
import { baseTemplate } from "./template"
@@ -59,16 +58,5 @@ export const config = createVariant(ModelFamily.XS)
// This is necessary because the builder pattern doesn't support bulk overrides
Object.assign(config.componentOverrides, xsComponentOverrides)
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.XS }, { strict: true })
if (!validationResult.isValid) {
console.error("XS variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid XS variant configuration: ${validationResult.errors.join(", ")}`)
}
if (validationResult.warnings.length > 0) {
console.warn("XS variant configuration warnings:", validationResult.warnings)
}
// Export type information for better IDE support
export type XsVariantConfig = typeof config
+20
View File
@@ -827,4 +827,24 @@ export class StateManager {
return { ...secrets, ...settings } satisfies ApiConfiguration
}
/**
* Get all global state entries (for debugging/inspection)
*/
getAllGlobalStateEntries(): Record<string, unknown> {
if (!this.isInitialized) {
throw new Error(STATE_MANAGER_NOT_INITIALIZED)
}
return { ...this.globalStateCache }
}
/**
* Get all workspace state entries (for debugging/inspection)
*/
getAllWorkspaceStateEntries(): Record<string, unknown> {
if (!this.isInitialized) {
throw new Error(STATE_MANAGER_NOT_INITIALIZED)
}
return { ...this.workspaceStateCache }
}
}
-2
View File
@@ -2447,8 +2447,6 @@ export class Task {
if (lastMessage && lastMessage.partial) {
// lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
console.log("updating partial message", lastMessage)
// await this.saveClineMessagesAndUpdateHistory()
}
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
+2 -6
View File
@@ -426,12 +426,8 @@ export class BannerService {
* @param forceRefresh If true, bypasses cache and fetches fresh data
* @returns Array of non-dismissed banners converted to BannerCardData format
*/
public async getActiveBanners(forceRefresh = false): Promise<BannerCardData[]> {
const allBanners = await this.fetchActiveBanners(forceRefresh)
const nonDismissedBanners = allBanners.filter((banner) => !this.isBannerDismissed(banner.id))
return nonDismissedBanners
.map((banner) => this.convertToBannerCardData(banner))
.filter((banner): banner is BannerCardData => banner !== null)
public async getActiveBanners(_forceRefresh = false): Promise<BannerCardData[]> {
return []
}
/**
@@ -50,6 +50,12 @@ export class ErrorProviderFactory {
* @returns Default configuration using PostHog
*/
public static getDefaultConfig(): ErrorProviderConfig {
if (!posthogConfig.errorTrackingApiKey) {
return {
type: "no-op",
config: posthogConfig,
}
}
return {
type: "posthog",
config: posthogConfig,
-1
View File
@@ -41,7 +41,6 @@ export class ErrorService {
public logException(error: Error | ClineError, properties?: Record<string, unknown>): void {
this.provider.logException(error, properties)
console.error("[ErrorService] Logging exception", JSON.stringify(error))
}
public logMessage(
@@ -89,8 +89,6 @@ export class PostHogErrorProvider implements IErrorProvider {
timestamp: new Date().toISOString(),
},
})
console.error("[PostHogErrorProvider] Logging exception", error)
}
public logMessage(
+14 -17
View File
@@ -1,5 +1,4 @@
import { HostProvider } from "@/hosts/host-provider"
import { ErrorService } from "../error"
/**
* Simple logging utility for the extension's backend code.
@@ -8,33 +7,31 @@ export class Logger {
public readonly channelName = "Cline Dev Logger"
static error(message: string, error?: Error) {
Logger.#output("ERROR", message, error)
ErrorService.get().logMessage(message, "error")
error && ErrorService.get().logException(error)
}
static warn(message: string) {
Logger.#output("WARN", message)
ErrorService.get().logMessage(message, "warning")
}
static log(message: string) {
Logger.#output("LOG", message)
static log(message: string, ...optionalParams: any[]) {
Logger.#output("LOG", message, ...optionalParams)
}
static debug(message: string) {
Logger.#output("DEBUG", message)
static debug(message: string, ...optionalParams: any[]) {
Logger.#output("DEBUG", message, ...optionalParams)
}
static info(message: string) {
Logger.#output("INFO", message)
static info(message: string, ...optionalParams: any[]) {
Logger.#output("INFO", message, ...optionalParams)
}
static trace(message: string) {
Logger.#output("TRACE", message)
}
static #output(level: string, message: string, error?: Error) {
let fullMessage = message
if (error?.message) {
fullMessage += ` ${error.message}`
}
HostProvider.get().logToChannel(`${level} ${fullMessage}`)
if (error?.stack) {
console.log(`Stack trace:\n${error.stack}`)
try {
let fullMessage = message
if (error?.message) {
fullMessage += ` ${error.message}`
}
HostProvider.get().logToChannel(`${level} ${fullMessage}`)
} catch {
// Don't crash if logging fails
}
}
}
@@ -338,7 +338,6 @@ export class TelemetryService {
private telemetryMetadata: TelemetryMetadata,
) {
this.capture({ event: TelemetryService.EVENTS.USER.TELEMETRY_ENABLED })
console.info(`[TelemetryService] Initialized with ${providers.length} telemetry provider(s)`)
}
public addProvider(provider: ITelemetryProvider) {
+46 -41
View File
@@ -1,47 +1,52 @@
import { ApiFormat } from "./proto/cline/models"
import { ApiHandlerSettings } from "./storage/state-keys"
export type ApiProvider =
| "anthropic"
| "claude-code"
| "openrouter"
| "bedrock"
| "vertex"
| "openai"
| "ollama"
| "lmstudio"
| "gemini"
| "openai-native"
| "requesty"
| "together"
| "deepseek"
| "qwen"
| "qwen-code"
| "doubao"
| "mistral"
| "vscode-lm"
| "cline"
| "litellm"
| "moonshot"
| "nebius"
| "fireworks"
| "asksage"
| "xai"
| "sambanova"
| "cerebras"
| "sapaicore"
| "groq"
| "huggingface"
| "huawei-cloud-maas"
| "dify"
| "baseten"
| "vercel-ai-gateway"
| "zai"
| "oca"
| "aihubmix"
| "minimax"
| "hicap"
| "nousResearch"
const API_PROVIDERS_LIST_BASE = [
"anthropic",
"claude-code",
"openrouter",
"bedrock",
"vertex",
"openai",
"ollama",
"lmstudio",
"gemini",
"openai-native",
"requesty",
"together",
"deepseek",
"qwen",
"qwen-code",
"doubao",
"mistral",
"vscode-lm",
"cline",
"litellm",
"moonshot",
"nebius",
"fireworks",
"asksage",
"xai",
"sambanova",
"cerebras",
"sapaicore",
"groq",
"huggingface",
"huawei-cloud-maas",
"dify",
"baseten",
"vercel-ai-gateway",
"zai",
"oca",
"aihubmix",
"minimax",
"hicap",
"nousResearch",
]
export type ApiProvider = (typeof API_PROVIDERS_LIST_BASE)[number]
export const API_PROVIDERS_LIST = API_PROVIDERS_LIST_BASE as ApiProvider[]
export const DEFAULT_API_PROVIDER = "openrouter" as ApiProvider
+6 -2
View File
@@ -54,7 +54,10 @@
"@utils/*": [
"src/utils/*"
]
}
},
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": [
"src/**/*"
@@ -63,6 +66,7 @@
"node_modules",
".vscode-test",
"webview-ui",
"src/test/e2e/**/*"
"src/test/e2e/**/*",
"cli-ts"
]
}
File diff suppressed because one or more lines are too long