mirror of
https://github.com/cline/cline.git
synced 2026-09-18 09:24:17 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91127ffdb9 | ||
|
|
11f76f0218 | ||
|
|
74f1b9c75d | ||
|
|
942c78119e | ||
|
|
3130c3c96e | ||
|
|
f0225bc20d | ||
|
|
1bf158245c |
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
})
|
||||
Generated
+1449
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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(" ")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function jsonParseSafe<T>(data: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(data) as T
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"workflowToggles": {},
|
||||
"localClineRulesToggles": {},
|
||||
"localWindsurfRulesToggles": {},
|
||||
"localCursorRulesToggles": {},
|
||||
"localAgentsRulesToggles": {}
|
||||
}
|
||||
@@ -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
@@ -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}`
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 === "") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user