mirror of
https://github.com/cline/cline.git
synced 2026-10-13 09:09:43 +08:00
remove unused code/files
fix broken webview build remove unused code/files fix broken webview build
This commit is contained in:
committed by
Dominic Cooney
parent
1f31738b32
commit
78b1f30133
+32
-21
@@ -1,23 +1,34 @@
|
||||
{
|
||||
"entry": [
|
||||
"src/extension.ts",
|
||||
"src/standalone/cline-core.ts",
|
||||
"src/generated/hosts/standalone/protobus-server-setup.ts",
|
||||
"src/generated/hosts/standalone/host-bridge-clients.ts",
|
||||
"src/generated/hosts/vscode/protobus-services.ts",
|
||||
"src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"ignore": [
|
||||
"out/**",
|
||||
"node_modules/**",
|
||||
"*.d.ts",
|
||||
"**/*.test.ts",
|
||||
"**/__tests__",
|
||||
"src/test/**",
|
||||
"src/shared/**"
|
||||
],
|
||||
"vite": true
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"workspaces": {
|
||||
".": {
|
||||
"entry": [
|
||||
"src/extension.ts",
|
||||
"src/standalone/cline-core.ts",
|
||||
"src/generated/hosts/standalone/protobus-server-setup.ts",
|
||||
"src/generated/hosts/standalone/host-bridge-clients.ts",
|
||||
"src/generated/hosts/vscode/protobus-services.ts",
|
||||
"src/generated/hosts/vscode/hostbridge-grpc-service-config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/__tests__/**/*.ts",
|
||||
"src/test/**/*.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
},
|
||||
"webview-ui": {
|
||||
"entry": [
|
||||
"src/services/grpc-client.ts",
|
||||
"src/**/*.test.{ts,tsx}",
|
||||
"src/**/*.spec.{ts,tsx}",
|
||||
"src/**/__tests__/**/*.{ts,tsx}"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"*.ts"
|
||||
],
|
||||
"vite": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,10 @@
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
|
||||
"lint": "biome lint --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"analyze:unused": "npx --yes knip --config knip.json --include files,exports,types,enumMembers,duplicates",
|
||||
"analyze:unused:prod": "npx --yes knip --config knip.json --production --include files,exports,types,enumMembers,dependencies",
|
||||
"analyze:unused:fix-exports": "node scripts/remove-unused-exports.mjs --apply",
|
||||
"analyze:unused:fix-exports:dry": "node scripts/remove-unused-exports.mjs",
|
||||
"format": "biome format --config-path ./biome.jsonc --changed --since main --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error --semicolons=as-needed",
|
||||
"format:fix": "biome check --config-path ./biome.jsonc --changed --since main --no-errors-on-unmatched --files-ignore-unknown=true --write --semicolons=as-needed",
|
||||
"fix:all": "biome check --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe --semicolons=as-needed",
|
||||
@@ -401,7 +405,7 @@
|
||||
"lint-staged": {
|
||||
"src/shared/storage/state-keys.ts": [
|
||||
"node scripts/generate-state-proto.mjs",
|
||||
"git add proto/cline/state.proto"
|
||||
"git add apps/vscode/proto/cline/state.proto"
|
||||
],
|
||||
"*": [
|
||||
"biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true --semicolons=as-needed"
|
||||
|
||||
@@ -4,12 +4,11 @@ import * as path from "path"
|
||||
import { Environment, type EnvironmentConfig } from "./shared/config-types"
|
||||
import { Logger } from "./shared/services/Logger"
|
||||
|
||||
export { Environment, type EnvironmentConfig }
|
||||
|
||||
/**
|
||||
export { Environment } /**
|
||||
* Schema for the endpoints.json configuration file used in on-premise deployments.
|
||||
* All fields are required and must be valid URLs.
|
||||
*/
|
||||
|
||||
interface EndpointsFileSchema {
|
||||
appBaseUrl: string
|
||||
apiBaseUrl: string
|
||||
@@ -36,7 +35,7 @@ class ClineEndpoint {
|
||||
private onPremiseConfig: EndpointsFileSchema | null = null
|
||||
private environment: Environment = Environment.production
|
||||
// Track if config came from bundled file (enterprise distribution)
|
||||
private isBundled: boolean = false
|
||||
private isBundled = false
|
||||
|
||||
private constructor() {
|
||||
// Set environment at module load. Use override if provided.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { type ApiHandler as SdkApiHandler, type ApiStreamChunk as SdkApiStreamChunk } from "@cline/llms"
|
||||
import { ApiConfiguration, ModelInfo } from "@shared/api"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
|
||||
|
||||
// buildApiHandler now routes inference through the Cline SDK. It lives in
|
||||
// apps/vscode/src/sdk/sdk-api-handler.ts and callers import it directly from
|
||||
@@ -13,22 +9,6 @@ import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
|
||||
// at module-eval time (which can break extension activation). Keep this file
|
||||
// types-only.
|
||||
|
||||
// Re-export the SDK inference contracts so callers can depend on the SDK types
|
||||
// through the existing @core/api entry point. These are the canonical handler
|
||||
// and stream types going forward; the local interfaces below remain for the
|
||||
// classic provider classes until they are removed.
|
||||
export type { SdkApiHandler, SdkApiStreamChunk }
|
||||
|
||||
export type CommonApiHandlerOptions = {
|
||||
onRetryAttempt?: ApiConfiguration["onRetryAttempt"]
|
||||
}
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[], useResponseApi?: boolean): ApiStream
|
||||
getModel(): ApiHandlerModel
|
||||
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
|
||||
abort?(): void
|
||||
}
|
||||
|
||||
export interface ApiHandlerModel {
|
||||
id: string
|
||||
info: ModelInfo
|
||||
@@ -41,7 +21,3 @@ export interface ApiProviderInfo {
|
||||
mode: Mode
|
||||
customPrompt?: string // "compact"
|
||||
}
|
||||
|
||||
export interface SingleCompletionHandler {
|
||||
completePrompt(prompt: string): Promise<string>
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
export type ApiStream = AsyncGenerator<ApiStreamChunk> & { id?: string }
|
||||
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamThinkingChunk | ApiStreamUsageChunk | ApiStreamToolCallsChunk
|
||||
|
||||
export interface ApiStreamTextChunk {
|
||||
type: "text"
|
||||
/**
|
||||
* Text content generated by the model
|
||||
*/
|
||||
text: string
|
||||
/**
|
||||
* The response ID associated with this chunk
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* The thought signature associated with this chunk used by Gemini
|
||||
*/
|
||||
signature?: string
|
||||
}
|
||||
|
||||
export interface ApiStreamUsageChunk {
|
||||
type: "usage"
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
thoughtsTokenCount?: number // openrouter
|
||||
totalCost?: number // openrouter
|
||||
/**
|
||||
* The response ID associated with this response
|
||||
*/
|
||||
id?: string
|
||||
}
|
||||
|
||||
export interface ApiStreamToolCallsChunk {
|
||||
type: "tool_calls"
|
||||
/**
|
||||
* The tool call information
|
||||
*/
|
||||
tool_call: ApiStreamToolCall
|
||||
/**
|
||||
* The response ID associated with this chunk
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* The thought signature associated with this chunk used by Gemini
|
||||
*/
|
||||
signature?: string
|
||||
}
|
||||
|
||||
export interface ApiStreamToolCall {
|
||||
/**
|
||||
* The call ID associated with this tool call
|
||||
*/
|
||||
call_id?: string
|
||||
// Information about the tool being called
|
||||
function: {
|
||||
/**
|
||||
* The tool call ID
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* Name of the tool
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* The arguments passed to the tool execution
|
||||
*/
|
||||
arguments?: any
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiStreamThinkingChunk {
|
||||
type: "reasoning"
|
||||
/**
|
||||
* The reasoning text generated by the model.
|
||||
* Redacted reasoning block will have this field set to "[REDACTED]" or an empty string.
|
||||
*/
|
||||
reasoning: string
|
||||
/**
|
||||
* openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
|
||||
* This is also where we store the summary details for OpenAI.
|
||||
*/
|
||||
details?: unknown
|
||||
/**
|
||||
* It's used when sending the thinking block back to the API.
|
||||
* API expects this in completed form, not as array of deltas.
|
||||
* Also used by Gemini for thought signature associated with this chunk
|
||||
*/
|
||||
signature?: string
|
||||
/**
|
||||
* redacted data
|
||||
*/
|
||||
redacted_data?: string
|
||||
/**
|
||||
* The response ID associated with this chunk
|
||||
*/
|
||||
id?: string
|
||||
}
|
||||
@@ -8,14 +8,14 @@ export interface FileMetadataEntry {
|
||||
user_edit_date?: number | null
|
||||
}
|
||||
|
||||
export interface ModelMetadataEntry {
|
||||
interface ModelMetadataEntry {
|
||||
ts: number
|
||||
model_id: string
|
||||
model_provider_id: string
|
||||
mode: string
|
||||
}
|
||||
|
||||
export interface EnvironmentMetadataEntry {
|
||||
interface EnvironmentMetadataEntry {
|
||||
ts: number
|
||||
os_name: string
|
||||
os_version: string
|
||||
|
||||
@@ -1,150 +1,8 @@
|
||||
import {
|
||||
ActivatedConditionalRule,
|
||||
getRemoteRulesTotalContentWithMetadata,
|
||||
getRuleFilesTotalContentWithMetadata,
|
||||
RULE_SOURCE_PREFIX,
|
||||
RuleLoadResultWithInstructions,
|
||||
synchronizeRuleToggles,
|
||||
} from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { StateManager } from "@core/storage/StateManager"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
import { evaluateRuleConditionals, type RuleEvaluationContext } from "./rule-conditionals"
|
||||
|
||||
export const getGlobalClineRules = async (
|
||||
globalClineRulesFilePath: string,
|
||||
toggles: ClineRulesToggles,
|
||||
opts?: { evaluationContext?: RuleEvaluationContext },
|
||||
): Promise<RuleLoadResultWithInstructions> => {
|
||||
let combinedContent = ""
|
||||
const activatedConditionalRules: ActivatedConditionalRule[] = []
|
||||
|
||||
// 1. Get file-based rules
|
||||
if (await fileExistsAtPath(globalClineRulesFilePath)) {
|
||||
if (await isDirectory(globalClineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
|
||||
// Note: ruleNamePrefix explicitly set to "global" for clarity (matches the default)
|
||||
const rulesFilesTotal = await getRuleFilesTotalContentWithMetadata(
|
||||
rulesFilePaths,
|
||||
globalClineRulesFilePath,
|
||||
toggles,
|
||||
{
|
||||
evaluationContext: opts?.evaluationContext,
|
||||
ruleNamePrefix: "global",
|
||||
},
|
||||
)
|
||||
if (rulesFilesTotal.content) {
|
||||
combinedContent = rulesFilesTotal.content
|
||||
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
|
||||
}
|
||||
} catch {
|
||||
Logger.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
|
||||
}
|
||||
} else {
|
||||
Logger.error(`${globalClineRulesFilePath} is not a directory`)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Append remote config rules
|
||||
const stateManager = StateManager.get()
|
||||
const remoteConfigSettings = stateManager.getRemoteConfigSettings()
|
||||
const remoteRules = remoteConfigSettings.remoteGlobalRules || []
|
||||
const remoteToggles = stateManager.getGlobalStateKey("remoteRulesToggles") || {}
|
||||
const remoteResult = getRemoteRulesTotalContentWithMetadata(remoteRules, remoteToggles, {
|
||||
evaluationContext: opts?.evaluationContext,
|
||||
})
|
||||
if (remoteResult.content) {
|
||||
if (combinedContent) combinedContent += "\n\n"
|
||||
combinedContent += remoteResult.content
|
||||
activatedConditionalRules.push(...remoteResult.activatedConditionalRules)
|
||||
}
|
||||
|
||||
// 3. Return formatted instructions
|
||||
if (!combinedContent) {
|
||||
return { instructions: undefined, activatedConditionalRules: [] }
|
||||
}
|
||||
|
||||
return {
|
||||
instructions: formatResponse.clineRulesGlobalDirectoryInstructions(globalClineRulesFilePath, combinedContent),
|
||||
activatedConditionalRules,
|
||||
}
|
||||
}
|
||||
|
||||
export const getLocalClineRules = async (
|
||||
cwd: string,
|
||||
toggles: ClineRulesToggles,
|
||||
opts?: { evaluationContext?: RuleEvaluationContext },
|
||||
): Promise<RuleLoadResultWithInstructions> => {
|
||||
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
let instructions: string | undefined
|
||||
const activatedConditionalRules: ActivatedConditionalRule[] = []
|
||||
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
[".clinerules", "skills"],
|
||||
])
|
||||
|
||||
const rulesFilesTotal = await getRuleFilesTotalContentWithMetadata(rulesFilePaths, cwd, toggles, {
|
||||
evaluationContext: opts?.evaluationContext,
|
||||
ruleNamePrefix: "workspace",
|
||||
})
|
||||
if (rulesFilesTotal.content) {
|
||||
instructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotal.content)
|
||||
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
|
||||
}
|
||||
} catch {
|
||||
Logger.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (clineRulesFilePath in toggles && toggles[clineRulesFilePath] !== false) {
|
||||
const raw = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
|
||||
if (raw) {
|
||||
// Keep single-file .clinerules behavior consistent with directory/remote rules:
|
||||
// - Parse YAML frontmatter (fail-open on parse errors)
|
||||
// - Evaluate conditionals against the request's evaluation context
|
||||
const parsed = parseYamlFrontmatter(raw)
|
||||
if (parsed.hadFrontmatter && parsed.parseError) {
|
||||
// Fail-open: preserve the raw contents so the LLM can still see the author's intent.
|
||||
instructions = formatResponse.clineRulesLocalFileInstructions(cwd, raw)
|
||||
} else {
|
||||
const { passed, matchedConditions } = evaluateRuleConditionals(
|
||||
parsed.data,
|
||||
opts?.evaluationContext ?? {},
|
||||
)
|
||||
if (passed) {
|
||||
instructions = formatResponse.clineRulesLocalFileInstructions(cwd, parsed.body.trim())
|
||||
if (parsed.hadFrontmatter && Object.keys(matchedConditions).length > 0) {
|
||||
activatedConditionalRules.push({
|
||||
name: `${RULE_SOURCE_PREFIX.workspace}:${GlobalFileNames.clineRules}`,
|
||||
matchedConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { instructions, activatedConditionalRules }
|
||||
}
|
||||
|
||||
export async function refreshClineRulesToggles(
|
||||
controller: Controller,
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
combineRuleToggles,
|
||||
getRuleFilesTotalContent,
|
||||
readDirectoryRecursive,
|
||||
synchronizeRuleToggles,
|
||||
} from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { combineRuleToggles, synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { listFiles } from "@services/glob/list-files"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Refreshes the toggles for windsurf, cursor, and agents rules
|
||||
@@ -57,146 +47,3 @@ export async function refreshExternalRulesToggles(
|
||||
agentsLocalToggles: updatedLocalAgentsToggles,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather formatted windsurf rules
|
||||
*/
|
||||
export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesToggles) => {
|
||||
const windsurfRulesFilePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
|
||||
|
||||
let windsurfRulesFileInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(windsurfRulesFilePath)) {
|
||||
if (!(await isDirectory(windsurfRulesFilePath))) {
|
||||
try {
|
||||
if (windsurfRulesFilePath in toggles && toggles[windsurfRulesFilePath] !== false) {
|
||||
const ruleFileContent = (await fs.readFile(windsurfRulesFilePath, "utf8")).trim()
|
||||
if (ruleFileContent) {
|
||||
windsurfRulesFileInstructions = formatResponse.windsurfRulesLocalFileInstructions(cwd, ruleFileContent)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return windsurfRulesFileInstructions
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather formatted cursor rules, which can come from two sources
|
||||
*/
|
||||
export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggles) => {
|
||||
// we first check for the .cursorrules file
|
||||
const cursorRulesFilePath = path.resolve(cwd, GlobalFileNames.cursorRulesFile)
|
||||
let cursorRulesFileInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(cursorRulesFilePath)) {
|
||||
if (!(await isDirectory(cursorRulesFilePath))) {
|
||||
try {
|
||||
if (cursorRulesFilePath in toggles && toggles[cursorRulesFilePath] !== false) {
|
||||
const ruleFileContent = (await fs.readFile(cursorRulesFilePath, "utf8")).trim()
|
||||
if (ruleFileContent) {
|
||||
cursorRulesFileInstructions = formatResponse.cursorRulesLocalFileInstructions(cwd, ruleFileContent)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we then check for the .cursor/rules dir
|
||||
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
|
||||
let cursorRulesDirInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(cursorRulesDirPath)) {
|
||||
if (await isDirectory(cursorRulesDirPath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
}
|
||||
} catch {
|
||||
Logger.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [cursorRulesFileInstructions, cursorRulesDirInstructions]
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to find all agents.md files recursively (case-insensitive)
|
||||
* Only searches if a top-level agents.md file exists
|
||||
*/
|
||||
async function findAgentsMdFiles(cwd: string): Promise<string[]> {
|
||||
try {
|
||||
// First check if top-level agents.md exists
|
||||
const topLevelAgentsPath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
|
||||
const topLevelExists = await fileExistsAtPath(topLevelAgentsPath)
|
||||
|
||||
// Only search recursively if top-level agents.md exists
|
||||
if (!topLevelExists) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Search recursively for all agents.md files
|
||||
const [allFiles] = await listFiles(cwd, true, 500)
|
||||
return allFiles.filter((filePath) => {
|
||||
const basename = path.basename(filePath).toLowerCase()
|
||||
return basename === GlobalFileNames.agentsRulesFile.toLowerCase()
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to find agents.md files in ${cwd}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather formatted agents rules - searches recursively and combines all agents.md files
|
||||
*/
|
||||
export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggles) => {
|
||||
const agentsRulesFilePath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
|
||||
|
||||
// Check if the top-level agents.md file is enabled
|
||||
if (agentsRulesFilePath in toggles && toggles[agentsRulesFilePath] === false) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const agentsMdFiles = await findAgentsMdFiles(cwd)
|
||||
|
||||
if (agentsMdFiles.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Read and combine all agents.md files
|
||||
const combinedContent = await Promise.all(
|
||||
agentsMdFiles.map(async (filePath) => {
|
||||
try {
|
||||
const fullPath = path.resolve(cwd, filePath)
|
||||
const content = (await fs.readFile(fullPath, "utf8")).trim()
|
||||
if (content) {
|
||||
const relativePath = path.relative(cwd, fullPath)
|
||||
return `## ${relativePath}\n\n${content}`
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to read agents.md file at ${filePath}:`, error)
|
||||
return null
|
||||
}
|
||||
}),
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
|
||||
if (combinedContent) {
|
||||
return formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Failed to read agents.md files:", error)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* - The `paths` conditional matches if any candidate path matches any glob pattern.
|
||||
* - Candidate paths are expected to be workspace-root-relative POSIX paths.
|
||||
*/
|
||||
import * as path from "path"
|
||||
import picomatch from "picomatch"
|
||||
|
||||
export type RuleEvaluationContext = {
|
||||
@@ -20,8 +19,6 @@ export type RuleEvaluationContext = {
|
||||
paths?: string[]
|
||||
}
|
||||
|
||||
export type ConditionalEvaluator = (frontmatterValue: unknown, context: RuleEvaluationContext) => boolean
|
||||
|
||||
type MatchedConditions = Record<string, string[]>
|
||||
|
||||
type ConditionalEvaluatorResult = {
|
||||
@@ -154,14 +151,3 @@ export function extractPathLikeStrings(text: string): string[] {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an absolute filesystem path to a workspace-root-relative POSIX path.
|
||||
* Returns undefined if the absolute path is not within the given root.
|
||||
*/
|
||||
export function toWorkspaceRelativePosixPath(absPath: string, workspaceRoot: string): string | undefined {
|
||||
const rel = path.relative(workspaceRoot, absPath)
|
||||
// Outside the root
|
||||
if (rel.startsWith("..") || path.isAbsolute(rel)) return undefined
|
||||
return toPosix(rel)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { evaluateRuleConditionals, RuleEvaluationContext } from "./rule-conditio
|
||||
/**
|
||||
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
|
||||
*/
|
||||
export async function readDirectoryRecursive(
|
||||
async function readDirectoryRecursive(
|
||||
directoryPath: string,
|
||||
allowedFileExtension: string,
|
||||
excludedPaths: string[][] = [],
|
||||
@@ -145,11 +145,11 @@ export function combineRuleToggles(toggles1: ClineRulesToggles, toggles2: ClineR
|
||||
/**
|
||||
* Read the content of rules files
|
||||
*/
|
||||
export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
|
||||
const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
|
||||
return (await getRuleFilesTotalContentWithMetadata(rulesFilePaths, basePath, toggles)).content
|
||||
}
|
||||
|
||||
export type ActivatedConditionalRule = {
|
||||
type ActivatedConditionalRule = {
|
||||
name: string
|
||||
matchedConditions: Record<string, string[]>
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export type RuleLoadResult = {
|
||||
* Result type for rule loading functions that return formatted instructions.
|
||||
* Used by getGlobalClineRules and getLocalClineRules.
|
||||
*/
|
||||
export type RuleLoadResultWithInstructions = {
|
||||
type RuleLoadResultWithInstructions = {
|
||||
instructions?: string
|
||||
activatedConditionalRules: ActivatedConditionalRule[]
|
||||
}
|
||||
@@ -231,7 +231,7 @@ export const getRuleFilesTotalContentWithMetadata = async (
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteRulesTotalContentWithMetadata(
|
||||
function getRemoteRulesTotalContentWithMetadata(
|
||||
remoteRules: GlobalInstructionsFile[],
|
||||
remoteToggles: ClineRulesToggles,
|
||||
opts?: { evaluationContext?: RuleEvaluationContext },
|
||||
@@ -275,7 +275,7 @@ export function getRemoteRulesTotalContentWithMetadata(
|
||||
* Doesn't do anything if the dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
|
||||
async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
|
||||
try {
|
||||
const exists = await fileExistsAtPath(clinerulePath)
|
||||
|
||||
|
||||
@@ -18,13 +18,6 @@ export interface ValidatedRemoteSkill {
|
||||
contents: string
|
||||
}
|
||||
|
||||
export interface SkillToggleState {
|
||||
globalSkillsToggles?: Record<string, boolean>
|
||||
localSkillsToggles?: Record<string, boolean>
|
||||
remoteSkillsToggles?: Record<string, boolean>
|
||||
remoteSkillEntries?: GlobalInstructionsFile[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate remote skill entries from GlobalInstructionsFile[].
|
||||
*
|
||||
@@ -202,34 +195,6 @@ export function getAvailableSkills(skills: SkillMetadata[]): SkillMetadata[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function filterEnabledSkills(skills: SkillMetadata[], toggleState: SkillToggleState = {}): SkillMetadata[] {
|
||||
const globalSkillsToggles = toggleState.globalSkillsToggles ?? {}
|
||||
const localSkillsToggles = toggleState.localSkillsToggles ?? {}
|
||||
const remoteSkillsToggles = toggleState.remoteSkillsToggles ?? {}
|
||||
const remoteSkillMap = new Map(
|
||||
parseRemoteSkillEntries(toggleState.remoteSkillEntries || []).map((entry) => [entry.name, entry]),
|
||||
)
|
||||
|
||||
return skills.filter((skill) => {
|
||||
if (skill.path.startsWith("remote:")) {
|
||||
const name = skill.path.replace("remote:", "")
|
||||
const entry = remoteSkillMap.get(name)
|
||||
if (entry?.alwaysEnabled) {
|
||||
return true
|
||||
}
|
||||
return remoteSkillsToggles[name] !== false
|
||||
}
|
||||
|
||||
const toggles = skill.source === "global" ? globalSkillsToggles : localSkillsToggles
|
||||
return toggles[skill.path] !== false
|
||||
})
|
||||
}
|
||||
|
||||
export async function discoverAvailableSkills(cwd: string, toggleState: SkillToggleState = {}): Promise<SkillMetadata[]> {
|
||||
const allSkills = await discoverSkills(cwd, toggleState.remoteSkillEntries)
|
||||
return filterEnabledSkills(getAvailableSkills(allSkills), toggleState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full skill content including instructions.
|
||||
* For remote skills, pass remoteSkillEntries so content can be loaded without disk I/O.
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import { CheckpointEvent, CheckpointEvent_OperationType, CheckpointSubscriptionRequest } from "@shared/proto/cline/checkpoints"
|
||||
import { Timestamp } from "@shared/proto/google/protobuf/timestamp"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { CheckpointEvent, CheckpointSubscriptionRequest } from "@shared/proto/cline/checkpoints"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Parameters for creating a checkpoint event
|
||||
*/
|
||||
export interface CheckpointEventData {
|
||||
operation: keyof typeof CheckpointEvent_OperationType
|
||||
cwdHash: string
|
||||
isActive: boolean
|
||||
taskId?: string
|
||||
commitHash?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Track active checkpoint subscriptions per workspace.
|
||||
* Map structure: cwdHash -> Set of response streams
|
||||
@@ -72,47 +59,3 @@ export async function subscribeToCheckpoints(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a checkpoint event to all subscribers of the specified workspace.
|
||||
*
|
||||
* @param eventData The checkpoint event to send
|
||||
*/
|
||||
export async function sendCheckpointEvent(eventData: CheckpointEventData): Promise<void> {
|
||||
const { cwdHash } = eventData
|
||||
|
||||
const subscriptions = activeCheckpointSubscriptions.get(cwdHash)
|
||||
if (!subscriptions || subscriptions.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const timestamp: Timestamp = {
|
||||
seconds: Math.trunc(now.getTime() / 1_000),
|
||||
nanos: (now.getTime() % 1_000) * 1_000_000,
|
||||
}
|
||||
|
||||
const event: CheckpointEvent = {
|
||||
operation: CheckpointEvent_OperationType[eventData.operation],
|
||||
cwdHash: eventData.cwdHash,
|
||||
isActive: eventData.isActive,
|
||||
timestamp,
|
||||
taskId: eventData.taskId,
|
||||
commitHash: eventData.commitHash,
|
||||
}
|
||||
|
||||
// Send the event to all active subscribers for this workspace
|
||||
const promises = Array.from(subscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
await responseStream(event, false) // Not the last message
|
||||
} catch (error) {
|
||||
Logger.error("Error sending checkpoint event:", error)
|
||||
subscriptions.delete(responseStream)
|
||||
if (subscriptions.size === 0) {
|
||||
activeCheckpointSubscriptions.delete(cwdHash)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FALLBACK_CLINE_RECOMMENDED_MODELS, fetchClineRecommendedModels } from "
|
||||
import { ClineEnv } from "@/config"
|
||||
import { fetch } from "@/shared/net"
|
||||
|
||||
export interface ClineRecommendedModelData {
|
||||
interface ClineRecommendedModelData {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
|
||||
@@ -325,7 +325,7 @@ const CLINE_STEALTH_MODELS: Record<string, ModelInfo> = {
|
||||
},
|
||||
}
|
||||
|
||||
export function appendClineStealthModels(currentModels: Record<string, ModelInfo>): Record<string, ModelInfo> {
|
||||
function appendClineStealthModels(currentModels: Record<string, ModelInfo>): Record<string, ModelInfo> {
|
||||
// Create a shallow clone of the current models to avoid mutating the original object
|
||||
const cloned = { ...currentModels }
|
||||
for (const [modelId, modelInfo] of Object.entries(CLINE_STEALTH_MODELS)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Controller } from "../index"
|
||||
const activePartialMessageSubscriptions = new Set<StreamingResponseHandler<ClineMessage>>()
|
||||
|
||||
// Keep track of callback-based subscriptions (for CLI and other non-gRPC consumers)
|
||||
export type PartialMessageCallback = (message: ClineMessage) => void
|
||||
type PartialMessageCallback = (message: ClineMessage) => void
|
||||
const callbackSubscriptions = new Set<PartialMessageCallback>()
|
||||
|
||||
/**
|
||||
@@ -38,18 +38,6 @@ export async function subscribeToPartialMessage(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to receive partial message events (for CLI and non-gRPC consumers)
|
||||
* @param callback The callback function to receive messages
|
||||
* @returns A function to unsubscribe
|
||||
*/
|
||||
export function registerPartialMessageCallback(callback: PartialMessageCallback): () => void {
|
||||
callbackSubscriptions.add(callback)
|
||||
return () => {
|
||||
callbackSubscriptions.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a partial message event to all active subscribers
|
||||
* @param partialMessage The ClineMessage to send
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
@@ -32,25 +31,3 @@ export async function subscribeToRelinquishControl(
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "relinquish_control_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a relinquish control event to all active subscribers
|
||||
*/
|
||||
export async function sendRelinquishControlEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeRelinquishControlSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
Logger.error("Error sending relinquish control event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeRelinquishControlSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Types of errors that can occur during hook execution
|
||||
*/
|
||||
export enum HookErrorType {
|
||||
enum HookErrorType {
|
||||
/** Hook execution exceeded the timeout limit */
|
||||
TIMEOUT = "timeout",
|
||||
/** Hook output failed JSON validation */
|
||||
|
||||
@@ -129,7 +129,7 @@ export interface Hooks {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HookModelInputContext {
|
||||
interface HookModelInputContext {
|
||||
provider?: string
|
||||
slug?: string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type LockType = "file" | "instance" | "folder"
|
||||
type LockType = "file" | "instance" | "folder"
|
||||
|
||||
export interface LockRow {
|
||||
id: number
|
||||
@@ -12,19 +12,3 @@ export interface SqliteLockManagerOptions {
|
||||
dbPath: string
|
||||
instanceAddress: string // cline core address
|
||||
}
|
||||
|
||||
export interface FolderLockOptions {
|
||||
lockTarget: string // The cwdHash of the folder to lock
|
||||
heldBy: string // taskId of the locking task
|
||||
}
|
||||
|
||||
export interface FolderLockResult {
|
||||
acquired: boolean // success or failure
|
||||
conflictingLock?: LockRow // conflicting lock if available
|
||||
}
|
||||
|
||||
export interface FolderLockWithRetryResult {
|
||||
acquired: boolean // success or failure
|
||||
skipped?: boolean // lock attempt was skipped (VS Code expected behavior)
|
||||
conflictingLock?: LockRow // conflicting lock if available
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { EnvironmentMetadataEntry, TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
|
||||
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
|
||||
import { execa } from "@packages/execa"
|
||||
import { RemoteConfig } from "@shared/remote-config/schema"
|
||||
import { GlobalState, Settings } from "@shared/storage/state-keys"
|
||||
@@ -8,35 +8,10 @@ import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { McpMarketplaceCatalog } from "@/shared/mcp"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { syncWorker } from "@/shared/services/worker/sync"
|
||||
import { StateManager } from "./StateManager"
|
||||
|
||||
/**
|
||||
* Atomically write data to a file using temp file + rename pattern.
|
||||
* This prevents readers from seeing partial/incomplete data by writing to a temporary
|
||||
* file first, then renaming it to the target location. The rename operation is atomic
|
||||
* in most cases on modern systems, though behavior may vary across platforms and filesystems.
|
||||
*
|
||||
* @param filePath - The target file path
|
||||
* @param data - The data to write
|
||||
*/
|
||||
async function atomicWriteFile(filePath: string, data: string): Promise<void> {
|
||||
const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(7)}.json`
|
||||
try {
|
||||
// Write to temporary file first
|
||||
await fs.writeFile(tmpPath, data, "utf8")
|
||||
// Rename temp file to target (atomic in most cases)
|
||||
await fs.rename(tmpPath, filePath)
|
||||
} catch (error) {
|
||||
// Clean up temp file if it exists
|
||||
fs.unlink(tmpPath).catch(() => {})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
contextHistory: "context_history.json",
|
||||
@@ -109,7 +84,7 @@ export async function getDocumentsPath(): Promise<string> {
|
||||
*
|
||||
* This is intended to eventually replace ~/Documents/Cline as the global config location.
|
||||
*/
|
||||
export function getClineHomePath(): string {
|
||||
function getClineHomePath(): string {
|
||||
return path.join(os.homedir(), ".cline")
|
||||
}
|
||||
|
||||
@@ -237,54 +212,6 @@ export async function getSavedApiConversationHistory(taskId: string): Promise<An
|
||||
return []
|
||||
}
|
||||
|
||||
export async function saveApiConversationHistory(taskId: string, apiConversationHistory: Anthropic.MessageParam[]) {
|
||||
try {
|
||||
if (apiConversationHistory.length > 0) {
|
||||
const fileName = GlobalFileNames.apiConversationHistory
|
||||
const data = JSON.stringify(apiConversationHistory)
|
||||
// Queue for remote sync without blocking
|
||||
syncWorker().enqueue(taskId, fileName, data)
|
||||
// Store locally
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(taskId), fileName)
|
||||
await atomicWriteFile(filePath, data)
|
||||
}
|
||||
} catch (error) {
|
||||
// in the off chance this fails, we don't want to stop the task
|
||||
Logger.error("Failed to save API conversation history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects environment metadata for the current system and host.
|
||||
* This information is used for debugging and task portability.
|
||||
* Returns metadata without timestamp - timestamp is added by EnvironmentContextTracker.
|
||||
*/
|
||||
export async function collectEnvironmentMetadata(): Promise<Omit<EnvironmentMetadataEntry, "ts">> {
|
||||
try {
|
||||
const hostVersion = await HostProvider.env.getHostVersion({})
|
||||
|
||||
return {
|
||||
os_name: os.platform(),
|
||||
os_version: os.release(),
|
||||
os_arch: os.arch(),
|
||||
host_name: hostVersion.platform || "Unknown",
|
||||
host_version: hostVersion.version || "Unknown",
|
||||
cline_version: ExtensionRegistryInfo.version,
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Failed to collect environment metadata:", error)
|
||||
// Return fallback values if collection fails
|
||||
return {
|
||||
os_name: os.platform(),
|
||||
os_version: os.release(),
|
||||
os_arch: os.arch(),
|
||||
host_name: "Unknown",
|
||||
host_version: "Unknown",
|
||||
cline_version: "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskMetadata(taskId: string): Promise<TaskMetadata> {
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.taskMetadata)
|
||||
try {
|
||||
@@ -307,10 +234,6 @@ export async function saveTaskMetadata(taskId: string, metadata: TaskMetadata) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureStateDirectoryExists(): Promise<string> {
|
||||
return getGlobalStorageDir("state")
|
||||
}
|
||||
|
||||
export async function ensureCacheDirectoryExists(): Promise<string> {
|
||||
return getGlobalStorageDir("cache")
|
||||
}
|
||||
@@ -422,7 +345,7 @@ export async function deleteRemoteConfigFromCache(organizationId: string): Promi
|
||||
* Gets the path to the global hooks directory if it exists.
|
||||
* Returns undefined if the directory doesn't exist.
|
||||
*/
|
||||
export async function getGlobalHooksDir(): Promise<string | undefined> {
|
||||
async function getGlobalHooksDir(): Promise<string | undefined> {
|
||||
const globalHooksDir = await ensureHooksDirectoryExists()
|
||||
return (await isDirectory(globalHooksDir)) ? globalHooksDir : undefined
|
||||
}
|
||||
@@ -491,121 +414,3 @@ export async function getWorkspaceHooksDirs(): Promise<string[]> {
|
||||
)
|
||||
).filter((path): path is string => Boolean(path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the conversation history to a temporary JSON file for PreCompact hook consumption.
|
||||
* The file is created in the task's directory with a unique timestamp-based name.
|
||||
* Returns the absolute path to the created file.
|
||||
*
|
||||
* @param taskId The task ID
|
||||
* @param apiConversationHistory The conversation history to write
|
||||
* @param timestamp Optional timestamp to use for the filename (defaults to Date.now())
|
||||
* @returns The absolute path to the temporary file
|
||||
*/
|
||||
export async function writeConversationHistoryJson(
|
||||
taskId: string,
|
||||
apiConversationHistory: Anthropic.MessageParam[],
|
||||
timestamp?: number,
|
||||
): Promise<string> {
|
||||
const taskDir = await ensureTaskDirectoryExists(taskId)
|
||||
const fileTimestamp = timestamp ?? Date.now()
|
||||
const tempFileName = `conversation_history_${fileTimestamp}.json`
|
||||
const tempFilePath = path.join(taskDir, tempFileName)
|
||||
|
||||
try {
|
||||
await atomicWriteFile(tempFilePath, JSON.stringify(apiConversationHistory, null, 2))
|
||||
return tempFilePath
|
||||
} catch (error) {
|
||||
Logger.error("Failed to write conversation history JSON for hook:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up a temporary conversation history file created for hook execution.
|
||||
* Silently handles errors (file already deleted, permissions, etc.)
|
||||
*
|
||||
* @param filePath The path to the temporary file to delete
|
||||
*/
|
||||
export async function cleanupConversationHistoryFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
await fs.unlink(filePath)
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle errors - this is cleanup, not critical
|
||||
Logger.debug("Failed to cleanup conversation history file:", filePath, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the conversation history in human-readable text format to a temporary file for PreCompact hook consumption.
|
||||
* This formats the conversation history (user and assistant messages) in a readable text format,
|
||||
* making it easy to analyze the conversation flow without parsing JSON.
|
||||
*
|
||||
* @param taskId The task ID
|
||||
* @param conversationHistory The conversation history messages
|
||||
* @param timestamp Optional timestamp to use for the filename (defaults to Date.now())
|
||||
* @returns The absolute path to the temporary file
|
||||
*/
|
||||
export async function writeConversationHistoryText(
|
||||
taskId: string,
|
||||
conversationHistory: Anthropic.MessageParam[],
|
||||
timestamp?: number,
|
||||
): Promise<string> {
|
||||
const taskDir = await ensureTaskDirectoryExists(taskId)
|
||||
const fileTimestamp = timestamp ?? Date.now()
|
||||
const tempFileName = `conversation_history_${fileTimestamp}.txt`
|
||||
const tempFilePath = path.join(taskDir, tempFileName)
|
||||
|
||||
try {
|
||||
// Build the formatted conversation history (excluding system prompt)
|
||||
let fullContext = "=== CONVERSATION HISTORY ===\n\n"
|
||||
|
||||
// Format each message in the conversation
|
||||
for (let i = 0; i < conversationHistory.length; i++) {
|
||||
const message = conversationHistory[i]
|
||||
fullContext += `--- Message ${i + 1} (${message.role.toUpperCase()}) ---\n`
|
||||
|
||||
// Handle content which can be a string or array
|
||||
if (typeof message.content === "string") {
|
||||
fullContext += message.content
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type === "text") {
|
||||
fullContext += block.text
|
||||
} else if (block.type === "image") {
|
||||
fullContext += `[IMAGE: ${block.source?.type || "unknown"}]`
|
||||
} else if (block.type === "tool_use") {
|
||||
fullContext += `[TOOL USE: ${block.name}]\n`
|
||||
fullContext += `Input: ${JSON.stringify(block.input, null, 2)}`
|
||||
} else if (block.type === "tool_result") {
|
||||
fullContext += `[TOOL RESULT: ${block.tool_use_id}]\n`
|
||||
if (typeof block.content === "string") {
|
||||
fullContext += block.content
|
||||
} else if (Array.isArray(block.content)) {
|
||||
for (const resultBlock of block.content) {
|
||||
if (resultBlock.type === "text") {
|
||||
fullContext += resultBlock.text
|
||||
} else if (resultBlock.type === "image") {
|
||||
fullContext += `[IMAGE]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fullContext += "\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
fullContext += "\n"
|
||||
}
|
||||
|
||||
fullContext += "=== END OF CONTEXT ===\n"
|
||||
|
||||
await atomicWriteFile(tempFilePath, fullContext)
|
||||
return tempFilePath
|
||||
} catch (error) {
|
||||
Logger.error("Failed to write conversation history text for hook:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ async function ensureGlobalRemoteConfigWorkspacePath(): Promise<string> {
|
||||
return workspacePath
|
||||
}
|
||||
|
||||
export async function getRemoteConfigWorkspacePath(workspacePath?: string): Promise<string> {
|
||||
async function getRemoteConfigWorkspacePath(workspacePath?: string): Promise<string> {
|
||||
const trimmed = workspacePath?.trim()
|
||||
if (trimmed) {
|
||||
return trimmed
|
||||
|
||||
@@ -70,29 +70,6 @@ export async function migrateTaskHistoryToFile(_context: vscode.ExtensionContext
|
||||
// TODO migrate to sdk location
|
||||
}
|
||||
|
||||
export async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
|
||||
if (mcpMarketplaceEnabled !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("mcpMarketplace.enabled", undefined, true)
|
||||
|
||||
return !mcpMarketplaceEnabled
|
||||
}
|
||||
return mcpMarketplaceEnabledRaw ?? true
|
||||
}
|
||||
|
||||
export async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
|
||||
if (enableCheckpoints !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("enableCheckpoints", undefined, true)
|
||||
return enableCheckpoints
|
||||
}
|
||||
return enableCheckpointsSettingRaw ?? true
|
||||
}
|
||||
|
||||
export async function migrateCustomInstructionsToGlobalRules(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const customInstructions = (await context.globalState.get("customInstructions")) as string | undefined
|
||||
@@ -138,375 +115,6 @@ export async function migrateCustomInstructionsToGlobalRules(context: vscode.Ext
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check if migration is needed - if planModeApiProvider already exists, skip migration
|
||||
const planModeApiProvider = await context.globalState.get("planModeApiProvider")
|
||||
if (planModeApiProvider !== undefined) {
|
||||
Logger.log("Legacy API configuration migration already completed, skipping...")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.log("Starting legacy API configuration migration to mode-specific keys...")
|
||||
|
||||
// Get the planActSeparateModelsSetting to determine migration strategy
|
||||
const planActSeparateModelsSetting = (await context.globalState.get("planActSeparateModelsSetting")) as
|
||||
| boolean
|
||||
| undefined
|
||||
|
||||
// Read legacy values directly
|
||||
const apiProvider = await context.globalState.get("apiProvider")
|
||||
const apiModelId = await context.globalState.get("apiModelId")
|
||||
const thinkingBudgetTokens = await context.globalState.get("thinkingBudgetTokens")
|
||||
const reasoningEffort = await context.globalState.get("reasoningEffort")
|
||||
const vsCodeLmModelSelector = await context.globalState.get("vsCodeLmModelSelector")
|
||||
const awsBedrockCustomSelected = await context.globalState.get("awsBedrockCustomSelected")
|
||||
const awsBedrockCustomModelBaseId = await context.globalState.get("awsBedrockCustomModelBaseId")
|
||||
const openRouterModelId = await context.globalState.get("openRouterModelId")
|
||||
const openRouterModelInfo = await context.globalState.get("openRouterModelInfo")
|
||||
const openAiModelId = await context.globalState.get("openAiModelId")
|
||||
const openAiModelInfo = await context.globalState.get("openAiModelInfo")
|
||||
const ollamaModelId = await context.globalState.get("ollamaModelId")
|
||||
const lmStudioModelId = await context.globalState.get("lmStudioModelId")
|
||||
const liteLlmModelId = await context.globalState.get("liteLlmModelId")
|
||||
const liteLlmModelInfo = await context.globalState.get("liteLlmModelInfo")
|
||||
const requestyModelId = await context.globalState.get("requestyModelId")
|
||||
const requestyModelInfo = await context.globalState.get("requestyModelInfo")
|
||||
const togetherModelId = await context.globalState.get("togetherModelId")
|
||||
const fireworksModelId = await context.globalState.get("fireworksModelId")
|
||||
const sapAiCoreModelId = await context.globalState.get("sapAiCoreModelId")
|
||||
const groqModelId = await context.globalState.get("groqModelId")
|
||||
const groqModelInfo = await context.globalState.get("groqModelInfo")
|
||||
const huggingFaceModelId = await context.globalState.get("huggingFaceModelId")
|
||||
const huggingFaceModelInfo = await context.globalState.get("huggingFaceModelInfo")
|
||||
|
||||
// Read previous mode values
|
||||
const previousModeApiProvider = await context.globalState.get("previousModeApiProvider")
|
||||
const previousModeModelId = await context.globalState.get("previousModeModelId")
|
||||
const previousModeModelInfo = await context.globalState.get("previousModeModelInfo")
|
||||
const previousModeVsCodeLmModelSelector = await context.globalState.get("previousModeVsCodeLmModelSelector")
|
||||
const previousModeThinkingBudgetTokens = await context.globalState.get("previousModeThinkingBudgetTokens")
|
||||
const previousModeReasoningEffort = await context.globalState.get("previousModeReasoningEffort")
|
||||
const previousModeAwsBedrockCustomSelected = await context.globalState.get("previousModeAwsBedrockCustomSelected")
|
||||
const previousModeAwsBedrockCustomModelBaseId = await context.globalState.get("previousModeAwsBedrockCustomModelBaseId")
|
||||
const previousModeSapAiCoreModelId = await context.globalState.get("previousModeSapAiCoreModelId")
|
||||
|
||||
// Migrate based on planActSeparateModelsSetting
|
||||
if (planActSeparateModelsSetting === false) {
|
||||
Logger.log("Migrating with separate models DISABLED - using current values for both modes")
|
||||
|
||||
// Use current values for both plan and act modes
|
||||
if (apiProvider !== undefined) {
|
||||
await context.globalState.update("planModeApiProvider", apiProvider)
|
||||
await context.globalState.update("actModeApiProvider", apiProvider)
|
||||
}
|
||||
if (apiModelId !== undefined) {
|
||||
await context.globalState.update("planModeApiModelId", apiModelId)
|
||||
await context.globalState.update("actModeApiModelId", apiModelId)
|
||||
}
|
||||
if (thinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
}
|
||||
if (reasoningEffort !== undefined) {
|
||||
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
|
||||
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
|
||||
}
|
||||
if (vsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
}
|
||||
if (awsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
}
|
||||
if (awsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
}
|
||||
if (openRouterModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
|
||||
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
|
||||
}
|
||||
if (openRouterModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
}
|
||||
if (openAiModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
|
||||
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
|
||||
}
|
||||
if (openAiModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
|
||||
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
|
||||
}
|
||||
if (ollamaModelId !== undefined) {
|
||||
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
|
||||
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
|
||||
}
|
||||
if (lmStudioModelId !== undefined) {
|
||||
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
|
||||
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
|
||||
}
|
||||
if (liteLlmModelId !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
|
||||
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
|
||||
}
|
||||
if (liteLlmModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
}
|
||||
if (requestyModelId !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelId", requestyModelId)
|
||||
await context.globalState.update("actModeRequestyModelId", requestyModelId)
|
||||
}
|
||||
if (requestyModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
|
||||
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
|
||||
}
|
||||
if (togetherModelId !== undefined) {
|
||||
await context.globalState.update("planModeTogetherModelId", togetherModelId)
|
||||
await context.globalState.update("actModeTogetherModelId", togetherModelId)
|
||||
}
|
||||
if (fireworksModelId !== undefined) {
|
||||
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
|
||||
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
|
||||
}
|
||||
if (sapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
}
|
||||
if (groqModelId !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelId", groqModelId)
|
||||
await context.globalState.update("actModeGroqModelId", groqModelId)
|
||||
}
|
||||
if (groqModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
|
||||
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
|
||||
}
|
||||
if (huggingFaceModelId !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
|
||||
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
|
||||
}
|
||||
if (huggingFaceModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
}
|
||||
} else {
|
||||
Logger.log("Migrating with separate models ENABLED - using current->plan, previous->act")
|
||||
|
||||
// Use current values for plan mode
|
||||
if (apiProvider !== undefined) {
|
||||
await context.globalState.update("planModeApiProvider", apiProvider)
|
||||
}
|
||||
if (apiModelId !== undefined) {
|
||||
await context.globalState.update("planModeApiModelId", apiModelId)
|
||||
}
|
||||
if (thinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
}
|
||||
if (reasoningEffort !== undefined) {
|
||||
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
|
||||
}
|
||||
if (vsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
}
|
||||
if (awsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
}
|
||||
if (awsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
}
|
||||
if (openRouterModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
|
||||
}
|
||||
if (openRouterModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
}
|
||||
if (openAiModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
|
||||
}
|
||||
if (openAiModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
|
||||
}
|
||||
if (ollamaModelId !== undefined) {
|
||||
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
|
||||
}
|
||||
if (lmStudioModelId !== undefined) {
|
||||
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
|
||||
}
|
||||
if (liteLlmModelId !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
|
||||
}
|
||||
if (liteLlmModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
}
|
||||
if (requestyModelId !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelId", requestyModelId)
|
||||
}
|
||||
if (requestyModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
|
||||
}
|
||||
if (togetherModelId !== undefined) {
|
||||
await context.globalState.update("planModeTogetherModelId", togetherModelId)
|
||||
}
|
||||
if (fireworksModelId !== undefined) {
|
||||
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
|
||||
}
|
||||
if (sapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
}
|
||||
if (groqModelId !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelId", groqModelId)
|
||||
}
|
||||
if (groqModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
|
||||
}
|
||||
if (huggingFaceModelId !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
|
||||
}
|
||||
if (huggingFaceModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
}
|
||||
|
||||
// Use previous values for act mode (with fallback to current values)
|
||||
if (previousModeApiProvider !== undefined) {
|
||||
await context.globalState.update("actModeApiProvider", previousModeApiProvider)
|
||||
} else if (apiProvider !== undefined) {
|
||||
await context.globalState.update("actModeApiProvider", apiProvider)
|
||||
}
|
||||
if (previousModeModelId !== undefined) {
|
||||
await context.globalState.update("actModeApiModelId", previousModeModelId)
|
||||
} else if (apiModelId !== undefined) {
|
||||
await context.globalState.update("actModeApiModelId", apiModelId)
|
||||
}
|
||||
if (previousModeThinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("actModeThinkingBudgetTokens", previousModeThinkingBudgetTokens)
|
||||
} else if (thinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
}
|
||||
if (previousModeReasoningEffort !== undefined) {
|
||||
await context.globalState.update("actModeReasoningEffort", previousModeReasoningEffort)
|
||||
} else if (reasoningEffort !== undefined) {
|
||||
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
|
||||
}
|
||||
if (previousModeVsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("actModeVsCodeLmModelSelector", previousModeVsCodeLmModelSelector)
|
||||
} else if (vsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
}
|
||||
if (previousModeAwsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomSelected", previousModeAwsBedrockCustomSelected)
|
||||
} else if (awsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
}
|
||||
if (previousModeAwsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", previousModeAwsBedrockCustomModelBaseId)
|
||||
} else if (awsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
}
|
||||
if (previousModeSapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("actModeSapAiCoreModelId", previousModeSapAiCoreModelId)
|
||||
} else if (sapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
}
|
||||
|
||||
// For fields without previous variants, use current values for act mode
|
||||
if (previousModeModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeOpenRouterModelInfo", previousModeModelInfo)
|
||||
} else if (openRouterModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
}
|
||||
if (openRouterModelId !== undefined) {
|
||||
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
|
||||
}
|
||||
if (openAiModelId !== undefined) {
|
||||
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
|
||||
}
|
||||
if (openAiModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
|
||||
}
|
||||
if (ollamaModelId !== undefined) {
|
||||
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
|
||||
}
|
||||
if (lmStudioModelId !== undefined) {
|
||||
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
|
||||
}
|
||||
if (liteLlmModelId !== undefined) {
|
||||
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
|
||||
}
|
||||
if (liteLlmModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
}
|
||||
if (requestyModelId !== undefined) {
|
||||
await context.globalState.update("actModeRequestyModelId", requestyModelId)
|
||||
}
|
||||
if (requestyModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
|
||||
}
|
||||
if (togetherModelId !== undefined) {
|
||||
await context.globalState.update("actModeTogetherModelId", togetherModelId)
|
||||
}
|
||||
if (fireworksModelId !== undefined) {
|
||||
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
|
||||
}
|
||||
if (groqModelId !== undefined) {
|
||||
await context.globalState.update("actModeGroqModelId", groqModelId)
|
||||
}
|
||||
if (groqModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
|
||||
}
|
||||
if (huggingFaceModelId !== undefined) {
|
||||
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
|
||||
}
|
||||
if (huggingFaceModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up legacy keys after successful migration
|
||||
Logger.log("Cleaning up legacy keys...")
|
||||
await context.globalState.update("apiProvider", undefined)
|
||||
await context.globalState.update("apiModelId", undefined)
|
||||
await context.globalState.update("thinkingBudgetTokens", undefined)
|
||||
await context.globalState.update("reasoningEffort", undefined)
|
||||
await context.globalState.update("vsCodeLmModelSelector", undefined)
|
||||
await context.globalState.update("awsBedrockCustomSelected", undefined)
|
||||
await context.globalState.update("awsBedrockCustomModelBaseId", undefined)
|
||||
await context.globalState.update("openRouterModelId", undefined)
|
||||
await context.globalState.update("openRouterModelInfo", undefined)
|
||||
await context.globalState.update("openAiModelId", undefined)
|
||||
await context.globalState.update("openAiModelInfo", undefined)
|
||||
await context.globalState.update("ollamaModelId", undefined)
|
||||
await context.globalState.update("lmStudioModelId", undefined)
|
||||
await context.globalState.update("liteLlmModelId", undefined)
|
||||
await context.globalState.update("liteLlmModelInfo", undefined)
|
||||
await context.globalState.update("requestyModelId", undefined)
|
||||
await context.globalState.update("requestyModelInfo", undefined)
|
||||
await context.globalState.update("togetherModelId", undefined)
|
||||
await context.globalState.update("fireworksModelId", undefined)
|
||||
await context.globalState.update("sapAiCoreModelId", undefined)
|
||||
await context.globalState.update("groqModelId", undefined)
|
||||
await context.globalState.update("groqModelInfo", undefined)
|
||||
await context.globalState.update("huggingFaceModelId", undefined)
|
||||
await context.globalState.update("huggingFaceModelInfo", undefined)
|
||||
await context.globalState.update("previousModeApiProvider", undefined)
|
||||
await context.globalState.update("previousModeModelId", undefined)
|
||||
await context.globalState.update("previousModeModelInfo", undefined)
|
||||
await context.globalState.update("previousModeVsCodeLmModelSelector", undefined)
|
||||
await context.globalState.update("previousModeThinkingBudgetTokens", undefined)
|
||||
await context.globalState.update("previousModeReasoningEffort", undefined)
|
||||
await context.globalState.update("previousModeAwsBedrockCustomSelected", undefined)
|
||||
await context.globalState.update("previousModeAwsBedrockCustomModelBaseId", undefined)
|
||||
await context.globalState.update("previousModeSapAiCoreModelId", undefined)
|
||||
|
||||
Logger.log("Successfully migrated legacy API configuration to mode-specific keys")
|
||||
} catch (error) {
|
||||
Logger.error("Failed to migrate legacy API configuration to mode-specific keys:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check if welcomeViewCompleted is already set
|
||||
|
||||
@@ -6,14 +6,14 @@ import { ensureTaskDirectoryExists } from "../../storage/disk"
|
||||
/**
|
||||
* Generate the standard file path for a task's focusChain markdown file
|
||||
*/
|
||||
export function getFocusChainFilePath(taskDir: string, taskId: string): string {
|
||||
function getFocusChainFilePath(taskDir: string, taskId: string): string {
|
||||
return path.join(taskDir, `focus_chain_taskid_${taskId}.md`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the standard markdown content structure for a focusChain file
|
||||
*/
|
||||
export function createFocusChainMarkdownContent(taskId: string, focusChainList: string): string {
|
||||
function createFocusChainMarkdownContent(taskId: string, focusChainList: string): string {
|
||||
return `# Focus Chain List for Task ${taskId}
|
||||
|
||||
<!-- Edit this markdown file to update your focus chain list -->
|
||||
@@ -28,7 +28,7 @@ ${focusChainList}
|
||||
* Extract focusChain items from text content (markdown or message text)
|
||||
* Returns array of lines that match focusChain item format
|
||||
*/
|
||||
export function extractFocusChainItemsFromText(text: string): string[] {
|
||||
function extractFocusChainItemsFromText(text: string): string[] {
|
||||
const lines = text.split("\n")
|
||||
return lines.filter((line) => {
|
||||
const trimmed = line.trim()
|
||||
|
||||
@@ -9,7 +9,7 @@ import { z } from "zod"
|
||||
import { buildSubagentToolName } from "./SubagentToolName"
|
||||
|
||||
/** Default Directory for agent configurations: ~/Documents/Cline/Agents */
|
||||
export const AGENTS_CONFIG_DIRECTORY_NAME = "Agents"
|
||||
const AGENTS_CONFIG_DIRECTORY_NAME = "Agents"
|
||||
const SUBAGENT_DYNAMIC_TOOL_NAMESPACE = "subagent"
|
||||
|
||||
const AgentBaseConfigSchema = z.object({
|
||||
@@ -77,7 +77,7 @@ function parseSkills(skills: string | string[] | undefined): string[] | undefine
|
||||
return Array.from(new Set(rawSkills.map(normalizeSkillName)))
|
||||
}
|
||||
|
||||
export function parseAgentConfigFromYaml(content: string): AgentBaseConfig {
|
||||
function parseAgentConfigFromYaml(content: string): AgentBaseConfig {
|
||||
const { data, body, hadFrontmatter, parseError } = parseYamlFrontmatter(content)
|
||||
if (parseError) {
|
||||
throw new Error(`Failed to parse YAML frontmatter: ${parseError}`)
|
||||
@@ -102,7 +102,7 @@ export function parseAgentConfigFromYaml(content: string): AgentBaseConfig {
|
||||
}) as AgentBaseConfig
|
||||
}
|
||||
|
||||
export function getAgentsConfigPath(homeDir = os.homedir()): string {
|
||||
function getAgentsConfigPath(homeDir = os.homedir()): string {
|
||||
return path.join(homeDir, "Documents", "Cline", AGENTS_CONFIG_DIRECTORY_NAME)
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ function isYamlFile(filePath: string): boolean {
|
||||
return /\.(yaml|yml)$/i.test(filePath)
|
||||
}
|
||||
|
||||
export async function readAgentConfigsFromDisk(homeDir = os.homedir()): Promise<Map<string, AgentBaseConfig>> {
|
||||
async function readAgentConfigsFromDisk(homeDir = os.homedir()): Promise<Map<string, AgentBaseConfig>> {
|
||||
const agentsDirectoryPath = getAgentsConfigPath(homeDir)
|
||||
const configs = new Map<string, AgentBaseConfig>()
|
||||
|
||||
|
||||
@@ -39,7 +39,3 @@ export function buildSubagentToolName(agentName: string): string {
|
||||
const body = sanitized.slice(0, Math.max(1, maxBodyLength))
|
||||
return trimToolNameToMax(`${SUBAGENT_TOOL_NAME_PREFIX}${body}_${hashSuffix}`)
|
||||
}
|
||||
|
||||
export function isSubagentToolName(toolName: string): boolean {
|
||||
return toolName.startsWith(SUBAGENT_TOOL_NAME_PREFIX)
|
||||
}
|
||||
|
||||
@@ -327,13 +327,6 @@ export function resolveWorkspacePath(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if we're in trace mode
|
||||
*/
|
||||
export function isWorkspaceTraceEnabled(): boolean {
|
||||
return process.env.MULTI_ROOT_TRACE === "true" || process.env.NODE_ENV === "development"
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 0: Convenience function for path.basename with tracking
|
||||
* This is what we'll use to replace existing path.basename() calls
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface WorkspaceContext {
|
||||
|
||||
export class WorkspaceRootManager {
|
||||
private roots: WorkspaceRoot[] = []
|
||||
private primaryIndex: number = 0
|
||||
private primaryIndex = 0
|
||||
|
||||
constructor(roots: WorkspaceRoot[] = [], primaryIndex: number = 0) {
|
||||
constructor(roots: WorkspaceRoot[] = [], primaryIndex = 0) {
|
||||
this.roots = roots
|
||||
this.primaryIndex = Math.min(primaryIndex, Math.max(0, roots.length - 1))
|
||||
}
|
||||
@@ -247,12 +247,3 @@ export class WorkspaceRootManager {
|
||||
return JSON.stringify({ workspaces }, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in Task and Controller
|
||||
export function createLegacyWorkspaceRoot(cwd: string): WorkspaceRoot {
|
||||
return {
|
||||
path: cwd,
|
||||
name: path.basename(cwd),
|
||||
vcs: VcsType.None, // Will be detected properly during initialization
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,7 @@
|
||||
*/
|
||||
|
||||
// Export workspace path parsing utilities
|
||||
export type { ParsedWorkspacePath } from "./utils/parseWorkspaceInlinePath"
|
||||
export {
|
||||
addWorkspaceHint,
|
||||
hasWorkspaceHint,
|
||||
parseMultipleWorkspacePaths,
|
||||
parseWorkspaceInlinePath,
|
||||
removeWorkspaceHint,
|
||||
} from "./utils/parseWorkspaceInlinePath"
|
||||
export type { WorkspaceAdapterConfig } from "./WorkspacePathAdapter"
|
||||
export { createWorkspacePathAdapter, WorkspacePathAdapter } from "./WorkspacePathAdapter"
|
||||
export {
|
||||
getWorkspaceBasename,
|
||||
isWorkspaceTraceEnabled,
|
||||
resolveWorkspacePath,
|
||||
WorkspaceResolver,
|
||||
workspaceResolver,
|
||||
} from "./WorkspaceResolver"
|
||||
export type { WorkspaceContext } from "./WorkspaceRootManager"
|
||||
export { createLegacyWorkspaceRoot, WorkspaceRootManager } from "./WorkspaceRootManager"
|
||||
|
||||
export { getWorkspaceBasename, workspaceResolver } from "./WorkspaceResolver"
|
||||
export { WorkspaceRootManager } from "./WorkspaceRootManager"
|
||||
// Re-export convenience function at module level for easier imports
|
||||
// Usage: import { resolveWorkspacePath } from "@core/workspace"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { EmptyRequest } from "@/shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Checks if the current workspace has multiple root folders open.
|
||||
* This is a lightweight check that only counts workspace folders,
|
||||
* independent of feature flags or internal multi-root implementation status.
|
||||
*
|
||||
* Use this when you need to know the actual workspace state (e.g., for telemetry,
|
||||
* headers, or UI display), not whether multi-root features are enabled.
|
||||
*
|
||||
* @returns true if 2 or more workspace folders are open, false otherwise
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isMultiRoot = await isMultiRootWorkspace()
|
||||
* Logger.log(`User has ${isMultiRoot ? 'multiple' : 'single'} workspace folders open`)
|
||||
* ```
|
||||
*/
|
||||
export async function isMultiRootWorkspace(): Promise<boolean> {
|
||||
try {
|
||||
const workspacePaths = await HostProvider.workspace.getWorkspacePaths(EmptyRequest.create({}))
|
||||
return workspacePaths.paths.length > 1
|
||||
} catch (error) {
|
||||
Logger.error("Failed to detect multi-root workspace", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ export type GrpcStreamingResponseHandlerWrapper = <TRequest, TResponse>(
|
||||
controller: Controller,
|
||||
) => grpc.handleServerStreamingCall<TRequest, TResponse>
|
||||
|
||||
export type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
|
||||
type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
|
||||
|
||||
/**
|
||||
* Abstract base class for type-safe gRPC client implementations.
|
||||
|
||||
@@ -173,11 +173,3 @@ export interface HostServiceHandlerConfig {
|
||||
requestId?: string,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request registry instance
|
||||
* This allows other parts of the code to access the registry
|
||||
*/
|
||||
export function getRequestRegistry(): GrpcRequestRegistry {
|
||||
return requestRegistry
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface MethodMetadata {
|
||||
/**
|
||||
* Generic service registry for gRPC services
|
||||
*/
|
||||
export class ServiceRegistry {
|
||||
class ServiceRegistry {
|
||||
private serviceName: string
|
||||
private methodRegistry: Record<string, ServiceMethodHandler> = {}
|
||||
private streamingMethodRegistry: Record<string, StreamingMethodHandler> = {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function ansiRegex({ onlyFirst = false } = {}) {
|
||||
function ansiRegex({ onlyFirst = false } = {}) {
|
||||
// Valid string terminator sequences are BEL, ESC\, and 0x9c
|
||||
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"
|
||||
const pattern = [
|
||||
|
||||
@@ -1,99 +1,3 @@
|
||||
import { access, constants, mkdir } from "fs/promises"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
|
||||
/**
|
||||
* Gets the path to the shadow Git repository in globalStorage.
|
||||
*
|
||||
* Checkpoints path structure:
|
||||
* globalStorage/
|
||||
* checkpoints/
|
||||
* {cwdHash}/
|
||||
* .git/
|
||||
*
|
||||
* @param cwdHash - Hash of the working directory path
|
||||
* @returns Promise<string> The absolute path to the shadow git directory
|
||||
* @throws Error if global storage path is invalid
|
||||
*/
|
||||
export async function getShadowGitPath(cwdHash: string): Promise<string> {
|
||||
const checkpointsDir = path.join(HostProvider.get().globalStorageFsPath, "checkpoints", cwdHash)
|
||||
await mkdir(checkpointsDir, { recursive: true })
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
return gitPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a workspace path is safe for checkpoints.
|
||||
* Checks that checkpoints are not being used in protected directories
|
||||
* like home, Desktop, Documents, or Downloads. Also confirms that the workspace
|
||||
* is accessible and that we will not encounter breaking permissions issues when
|
||||
* creating checkpoints.
|
||||
*
|
||||
* Protected directories:
|
||||
* - User's home directory
|
||||
* - Desktop
|
||||
* - Documents
|
||||
* - Downloads
|
||||
*
|
||||
* @param workspacePath - The absolute path to the workspace directory to validate
|
||||
* @returns Promise<void> Resolves if the path is valid
|
||||
* @throws Error if the path is in a protected directory or if no read access
|
||||
*/
|
||||
export async function validateWorkspacePath(workspacePath: string): Promise<void> {
|
||||
// Check if directory exists and we have read permissions
|
||||
try {
|
||||
await access(workspacePath, constants.R_OK)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Cannot access workspace directory. Please ensure VS Code has permission to access your workspace. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const homedir = os.homedir()
|
||||
const desktopPath = getDesktopDir()
|
||||
const documentsPath = path.join(homedir, "Documents")
|
||||
const downloadsPath = path.join(homedir, "Downloads")
|
||||
|
||||
switch (workspacePath) {
|
||||
case homedir:
|
||||
throw new Error("Cannot use checkpoints in home directory")
|
||||
case desktopPath:
|
||||
throw new Error("Cannot use checkpoints in Desktop directory")
|
||||
case documentsPath:
|
||||
throw new Error("Cannot use checkpoints in Documents directory")
|
||||
case downloadsPath:
|
||||
throw new Error("Cannot use checkpoints in Downloads directory")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current working directory from the VS Code workspace.
|
||||
* Validates that checkpoints are not being used in protected directories
|
||||
* like home, Desktop, Documents, or Downloads. Checks to confirm that the workspace
|
||||
* is accessible and that we will not encounter breaking permissions issues when
|
||||
* creating checkpoints.
|
||||
*
|
||||
* Protected directories:
|
||||
* - User's home directory
|
||||
* - Desktop
|
||||
* - Documents
|
||||
* - Downloads
|
||||
*
|
||||
* @returns Promise<string> The absolute path to the current working directory
|
||||
* @throws Error if no workspace is detected, if in a protected directory, or if no read access
|
||||
*/
|
||||
export async function getWorkingDirectory(): Promise<string> {
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
throw new Error("No workspace detected. Please open Cline in a workspace to use checkpoints.")
|
||||
}
|
||||
|
||||
await validateWorkspacePath(cwd)
|
||||
return cwd
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the current working directory to a 13-character numeric hash.
|
||||
* @param workingDir - The absolute path to the working directory
|
||||
|
||||
@@ -15,17 +15,17 @@ export async function detectEncoding(fileBuffer: Buffer, fileExtension?: string)
|
||||
const detected = chardet.detect(fileBuffer)
|
||||
if (typeof detected === "string") {
|
||||
return detected
|
||||
} else if (detected && (detected as any).encoding) {
|
||||
return (detected as any).encoding
|
||||
} else {
|
||||
if (fileExtension) {
|
||||
const isBinary = await isBinaryFile(fileBuffer).catch(() => false)
|
||||
if (isBinary) {
|
||||
throw new Error(`Cannot read text for file type: ${fileExtension}`)
|
||||
}
|
||||
}
|
||||
return "utf8"
|
||||
}
|
||||
if (detected && (detected as any).encoding) {
|
||||
return (detected as any).encoding
|
||||
}
|
||||
if (fileExtension) {
|
||||
const isBinary = await isBinaryFile(fileBuffer).catch(() => false)
|
||||
if (isBinary) {
|
||||
throw new Error(`Cannot read text for file type: ${fileExtension}`)
|
||||
}
|
||||
}
|
||||
return "utf8"
|
||||
}
|
||||
|
||||
export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
@@ -42,7 +42,7 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
* Expects the fs.access call to have already been performed prior to calling.
|
||||
* Content is automatically truncated if it exceeds 400KB to prevent context overflow.
|
||||
*/
|
||||
export async function callTextExtractionFunctions(filePath: string): Promise<string> {
|
||||
async function callTextExtractionFunctions(filePath: string): Promise<string> {
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
|
||||
let content: string
|
||||
@@ -131,9 +131,8 @@ function formatCellValue(cell: ExcelJS.Cell): string {
|
||||
if (typeof value === "object" && "formula" in value) {
|
||||
if ("result" in value && value.result !== undefined && value.result !== null) {
|
||||
return value.result.toString()
|
||||
} else {
|
||||
return `[Formula: ${value.formula}]`
|
||||
}
|
||||
return `[Formula: ${value.formula}]`
|
||||
}
|
||||
|
||||
return value.toString()
|
||||
|
||||
@@ -15,7 +15,7 @@ const IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/svg+xml
|
||||
* @param cell A notebook cell object
|
||||
* @returns The cell with sanitized outputs
|
||||
*/
|
||||
export function sanitizeCellOutputs(cell: Record<string, unknown>): Record<string, unknown> {
|
||||
function sanitizeCellOutputs(cell: Record<string, unknown>): Record<string, unknown> {
|
||||
if (cell.cell_type !== "code" || !cell.outputs || !Array.isArray(cell.outputs)) {
|
||||
return cell
|
||||
}
|
||||
|
||||
@@ -62,28 +62,27 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
const mimeType = getMimeType(filePath)
|
||||
|
||||
return { type: "image", data: `data:${mimeType};base64,${base64}` }
|
||||
} else {
|
||||
// for standard models we will check the size of the file to ensure its not too large
|
||||
try {
|
||||
const stats = await fs.stat(filePath)
|
||||
if (stats.size > 20 * 1000 * 1024) {
|
||||
Logger.warn(`File too large, skipping: ${filePath}`)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`Error checking file size for ${filePath}:`, error)
|
||||
}
|
||||
// for standard models we will check the size of the file to ensure its not too large
|
||||
try {
|
||||
const stats = await fs.stat(filePath)
|
||||
if (stats.size > 20 * 1000 * 1024) {
|
||||
Logger.warn(`File too large, skipping: ${filePath}`)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
|
||||
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
return { type: "file", data: filePath }
|
||||
} catch (error) {
|
||||
Logger.error(`Error checking file size for ${filePath}:`, error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
return { type: "file", data: filePath }
|
||||
})
|
||||
|
||||
const dataUrlsWithNulls = await Promise.all(processFilesPromises)
|
||||
@@ -103,7 +102,7 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
return { images, files }
|
||||
}
|
||||
|
||||
export function getMimeType(filePath: string): string {
|
||||
function getMimeType(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
switch (ext) {
|
||||
case ".png":
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Logger } from "@/shared/services/Logger"
|
||||
* - Fixed callback port: 1455
|
||||
* - Codex-specific params: codex_cli_simplified_flow=true, originator=cline
|
||||
*/
|
||||
export const OPENAI_CODEX_OAUTH_CONFIG = {
|
||||
const OPENAI_CODEX_OAUTH_CONFIG = {
|
||||
authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
|
||||
tokenEndpoint: "https://auth.openai.com/oauth/token",
|
||||
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
||||
@@ -25,9 +25,6 @@ export const OPENAI_CODEX_OAUTH_CONFIG = {
|
||||
callbackPort: 1455,
|
||||
} as const
|
||||
|
||||
// Token storage key - must match the key in SECRETS_KEYS (state-keys.ts)
|
||||
const OPENAI_CODEX_CREDENTIALS_KEY = "openai-codex-oauth-credentials"
|
||||
|
||||
type ProviderSettingsManager = {
|
||||
getProviderSettings(providerId: string):
|
||||
| {
|
||||
@@ -78,7 +75,7 @@ const openAiCodexCredentialsSchema = z.object({
|
||||
accountId: z.string().optional(),
|
||||
})
|
||||
|
||||
export type OpenAiCodexCredentials = z.infer<typeof openAiCodexCredentialsSchema>
|
||||
type OpenAiCodexCredentials = z.infer<typeof openAiCodexCredentialsSchema>
|
||||
|
||||
function providerSettingsToCredentials(
|
||||
settings: NonNullable<ReturnType<ProviderSettingsManager["getProviderSettings"]>>,
|
||||
@@ -287,7 +284,7 @@ function parseOAuthErrorDetails(errorText: string): { errorCode?: string; errorM
|
||||
* Generates a cryptographically random PKCE code verifier
|
||||
* Must be 43-128 characters long using unreserved characters
|
||||
*/
|
||||
export function generateCodeVerifier(): string {
|
||||
function generateCodeVerifier(): string {
|
||||
const buffer = crypto.randomBytes(32)
|
||||
return buffer.toString("base64url")
|
||||
}
|
||||
@@ -295,7 +292,7 @@ export function generateCodeVerifier(): string {
|
||||
/**
|
||||
* Generates the PKCE code challenge from the verifier using S256 method
|
||||
*/
|
||||
export function generateCodeChallenge(verifier: string): string {
|
||||
function generateCodeChallenge(verifier: string): string {
|
||||
const hash = crypto.createHash("sha256").update(verifier).digest()
|
||||
return hash.toString("base64url")
|
||||
}
|
||||
@@ -303,7 +300,7 @@ export function generateCodeChallenge(verifier: string): string {
|
||||
/**
|
||||
* Generates a random state parameter for CSRF protection
|
||||
*/
|
||||
export function generateState(): string {
|
||||
function generateState(): string {
|
||||
return crypto.randomBytes(16).toString("hex")
|
||||
}
|
||||
|
||||
@@ -311,7 +308,7 @@ export function generateState(): string {
|
||||
* Builds the authorization URL for OpenAI Codex OAuth flow
|
||||
* Includes Codex-specific parameters per the implementation guide
|
||||
*/
|
||||
export function buildAuthorizationUrl(codeChallenge: string, state: string): string {
|
||||
function buildAuthorizationUrl(codeChallenge: string, state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
|
||||
redirect_uri: OPENAI_CODEX_OAUTH_CONFIG.redirectUri,
|
||||
@@ -333,7 +330,7 @@ export function buildAuthorizationUrl(codeChallenge: string, state: string): str
|
||||
* Important: Uses application/x-www-form-urlencoded (not JSON)
|
||||
* Important: state must NOT be included in token exchange body
|
||||
*/
|
||||
export async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<OpenAiCodexCredentials> {
|
||||
async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<OpenAiCodexCredentials> {
|
||||
// Per the implementation guide: use application/x-www-form-urlencoded
|
||||
// and do NOT include state in the body (OpenAI returns error if included)
|
||||
const body = new URLSearchParams({
|
||||
@@ -388,7 +385,7 @@ export async function exchangeCodeForTokens(code: string, codeVerifier: string):
|
||||
* Refreshes the access token using the refresh token
|
||||
* Uses application/x-www-form-urlencoded (not JSON)
|
||||
*/
|
||||
export async function refreshAccessToken(credentials: OpenAiCodexCredentials): Promise<OpenAiCodexCredentials> {
|
||||
async function refreshAccessToken(credentials: OpenAiCodexCredentials): Promise<OpenAiCodexCredentials> {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
|
||||
@@ -441,7 +438,7 @@ export async function refreshAccessToken(credentials: OpenAiCodexCredentials): P
|
||||
* Checks if the credentials are expired (with 5 minute buffer)
|
||||
* Per the implementation guide: expires is in milliseconds since epoch
|
||||
*/
|
||||
export function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
|
||||
function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
|
||||
const bufferMs = 5 * 60 * 1000 // 5 minutes buffer
|
||||
return Date.now() >= credentials.expires - bufferMs
|
||||
}
|
||||
@@ -449,7 +446,7 @@ export function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
|
||||
/**
|
||||
* OpenAiCodexOAuthManager - Handles OAuth flow and token management
|
||||
*/
|
||||
export class OpenAiCodexOAuthManager {
|
||||
class OpenAiCodexOAuthManager {
|
||||
private credentials: OpenAiCodexCredentials | null = null
|
||||
private refreshPromise: Promise<OpenAiCodexCredentials> | null = null
|
||||
private pendingAuth: {
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* CommandExecutor - Unified command execution for all terminal modes.
|
||||
*
|
||||
* This class handles command execution for both VSCode terminal mode and
|
||||
* standalone/CLI mode. It uses the shared CommandOrchestrator for the
|
||||
* common orchestration logic (buffering, user interaction, result formatting).
|
||||
*
|
||||
* The differentiation between modes happens at the TerminalManager level:
|
||||
* - VscodeTerminalManager → VscodeTerminalProcess (shell integration)
|
||||
* - StandaloneTerminalManager → StandaloneTerminalProcess (child_process)
|
||||
*
|
||||
* IMPORTANT: Background execution mode uses StandaloneTerminalManager to run
|
||||
* commands in hidden terminals without cluttering the visible terminal.
|
||||
*/
|
||||
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import { ClineToolResponseContent } from "@shared/messages"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
|
||||
import type {
|
||||
CommandExecutionOptions,
|
||||
CommandExecutorCallbacks,
|
||||
CommandExecutorConfig,
|
||||
ITerminalManager,
|
||||
ShellIntegrationWarningTracker,
|
||||
TerminalProcessResultPromise,
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* CommandExecutor - Unified command executor for all terminal modes.
|
||||
*
|
||||
* Uses the shared CommandOrchestrator for common logic and delegates
|
||||
* process management to the appropriate TerminalManager.
|
||||
*/
|
||||
export class CommandExecutor {
|
||||
private cwd: string
|
||||
private taskId: string
|
||||
private ulid: string
|
||||
private terminalExecutionMode: "vscodeTerminal" | "backgroundExec"
|
||||
private terminalManager: ITerminalManager
|
||||
private standaloneManager: StandaloneTerminalManager
|
||||
private callbacks: CommandExecutorCallbacks
|
||||
|
||||
// Track the currently executing foreground process for cancellation
|
||||
private currentProcess: TerminalProcessResultPromise | null = null
|
||||
|
||||
// Flag to track if the current command was cancelled externally
|
||||
private wasCancelledExternally = false
|
||||
|
||||
// Track shell integration warnings to determine when to show background terminal suggestion
|
||||
private shellIntegrationWarningTracker: ShellIntegrationWarningTracker = {
|
||||
timestamps: [],
|
||||
lastSuggestionShown: undefined,
|
||||
}
|
||||
|
||||
constructor(config: CommandExecutorConfig, callbacks: CommandExecutorCallbacks) {
|
||||
this.cwd = config.cwd
|
||||
this.taskId = config.taskId
|
||||
this.ulid = config.ulid
|
||||
this.terminalExecutionMode = config.terminalExecutionMode
|
||||
this.terminalManager = config.terminalManager
|
||||
this.callbacks = callbacks
|
||||
|
||||
// When in backgroundExec mode, the terminalManager is already a StandaloneTerminalManager
|
||||
// created by Task. We should reuse it so that Task.getEnvironmentDetails() can see
|
||||
// the terminals and processes we create (for isHot logic, busy terminals, etc.)
|
||||
if (config.terminalExecutionMode === "backgroundExec" && config.terminalManager instanceof StandaloneTerminalManager) {
|
||||
// Reuse the same instance that Task is using
|
||||
this.standaloneManager = config.terminalManager
|
||||
Logger.info(`[CommandExecutor] Reusing Task's StandaloneTerminalManager for backgroundExec mode`)
|
||||
} else {
|
||||
// Create a standalone manager for background execution support.
|
||||
this.standaloneManager = new StandaloneTerminalManager()
|
||||
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager`)
|
||||
|
||||
// Copy settings from the provided terminalManager to ensure consistency
|
||||
if ("shellIntegrationTimeout" in config.terminalManager) {
|
||||
const tm = config.terminalManager as any
|
||||
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
|
||||
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
|
||||
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command in the terminal.
|
||||
*
|
||||
* Routing logic:
|
||||
* 1. Background mode commands use StandaloneTerminalManager
|
||||
* 2. Regular commands use the configured terminal manager
|
||||
*
|
||||
* @param command The command to execute
|
||||
* @param timeoutSeconds Optional timeout in seconds
|
||||
* @returns [userRejected, result] tuple
|
||||
*/
|
||||
async execute(
|
||||
command: string,
|
||||
timeoutSeconds: number | undefined,
|
||||
options?: CommandExecutionOptions,
|
||||
): Promise<[boolean, ClineToolResponseContent]> {
|
||||
// Strip leading `cd` to workspace from command
|
||||
const workspaceCdPrefix = `cd ${this.cwd} && `
|
||||
if (command.startsWith(workspaceCdPrefix)) {
|
||||
command = command.substring(workspaceCdPrefix.length)
|
||||
}
|
||||
|
||||
// Select the appropriate terminal manager
|
||||
const useStandalone = options?.useBackgroundExecution || this.terminalExecutionMode === "backgroundExec"
|
||||
const manager = useStandalone ? this.standaloneManager : this.terminalManager
|
||||
Logger.info(
|
||||
`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal (mode=${this.terminalExecutionMode}, managerCtor=${manager?.constructor?.name}): ${command}`,
|
||||
)
|
||||
|
||||
// Get terminal and run command
|
||||
const terminalInfo = await manager.getOrCreateTerminal(this.cwd)
|
||||
terminalInfo.terminal.show()
|
||||
const process = manager.runCommand(terminalInfo, command)
|
||||
|
||||
// Reset cancellation flag and track the current process
|
||||
this.wasCancelledExternally = false
|
||||
this.currentProcess = process
|
||||
const clearCurrentProcess = () => {
|
||||
this.currentProcess = null
|
||||
}
|
||||
process.once("completed", clearCurrentProcess)
|
||||
process.once("error", clearCurrentProcess)
|
||||
|
||||
// Use shared orchestration logic
|
||||
// The StandaloneTerminalManager handles background command tracking internally
|
||||
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
|
||||
command,
|
||||
timeoutSeconds,
|
||||
suppressUserInteraction: options?.suppressUserInteraction,
|
||||
// When "Proceed While Running" is triggered, track the command in the manager
|
||||
// Returns the log file path so the orchestrator can send it to the UI
|
||||
// existingOutput contains all output lines captured so far
|
||||
onProceedWhileRunning: useStandalone
|
||||
? (existingOutput: string[]) => {
|
||||
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
|
||||
return { logFilePath: backgroundCmd.logFilePath }
|
||||
}
|
||||
: undefined,
|
||||
showShellIntegrationSuggestion: this.shouldShowBackgroundTerminalSuggestion(),
|
||||
terminalType: useStandalone ? "standalone" : "vscode",
|
||||
})
|
||||
|
||||
// If the command was cancelled externally (via cancel button), return a clear cancellation message
|
||||
// This ensures the AI agent knows the command was cancelled by the user
|
||||
if (this.wasCancelledExternally) {
|
||||
const outputSoFar =
|
||||
result.outputLines.length > 0
|
||||
? `\nOutput captured before cancellation:\n${manager.processOutput(result.outputLines)}`
|
||||
: ""
|
||||
return [true, `Command was cancelled by the user.${outputSoFar}`]
|
||||
}
|
||||
|
||||
return [result.userRejected, result.result]
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all running commands (both foreground and background).
|
||||
*
|
||||
* This method cancels:
|
||||
* 1. All detached background commands (those that were "proceeded while running")
|
||||
* 2. The current foreground process (if one is actively running)
|
||||
*
|
||||
* @returns true if any commands were cancelled, false otherwise
|
||||
*/
|
||||
async cancelBackgroundCommand(): Promise<boolean> {
|
||||
let cancelled = false
|
||||
|
||||
// 1. Cancel all detached background commands
|
||||
const runningCommands = this.standaloneManager.getRunningBackgroundCommands()
|
||||
for (const cmd of runningCommands) {
|
||||
if (this.standaloneManager.cancelBackgroundCommand(cmd.id)) {
|
||||
cancelled = true
|
||||
Logger.info(`Cancelled background command: ${cmd.command}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Cancel the current foreground process (if any)
|
||||
if (this.currentProcess && typeof (this.currentProcess as any).terminate === "function") {
|
||||
// Set flag so execute() knows the command was cancelled externally
|
||||
this.wasCancelledExternally = true
|
||||
;(this.currentProcess as any).terminate()
|
||||
this.currentProcess = null
|
||||
cancelled = true
|
||||
Logger.info("Cancelled foreground command")
|
||||
}
|
||||
|
||||
// 3. Update UI state and notify user by modifying existing message
|
||||
// We modify the previous command_output message instead of sending a new say()
|
||||
// to avoid interfering with any pending ask() dialogs (which would cause
|
||||
// "Current ask promise was ignored" errors)
|
||||
if (cancelled) {
|
||||
this.callbacks.updateBackgroundCommandState(false)
|
||||
|
||||
// Wait for terminal buffers to flush before updating the message
|
||||
// This prevents the cancellation notice from appearing in the middle of output
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// Find the last command_output message and update it
|
||||
const messages = this.callbacks.getClineMessages()
|
||||
const lastCommandOutputIndex = findLastIndex(messages, (m) => m.ask === "command_output")
|
||||
if (lastCommandOutputIndex !== -1) {
|
||||
const existingText = messages[lastCommandOutputIndex].text || ""
|
||||
const cancellationNotice = "\n\nCommand(s) cancelled by user."
|
||||
await this.callbacks.updateClineMessage(lastCommandOutputIndex, {
|
||||
text: existingText + cancellationNotice,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return cancelled
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any active background commands.
|
||||
* Delegates to StandaloneTerminalManager.
|
||||
*/
|
||||
hasActiveBackgroundCommand(): boolean {
|
||||
return this.standaloneManager.hasActiveBackgroundCommands()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of background commands for environment details.
|
||||
* Delegates to StandaloneTerminalManager which tracks multiple commands.
|
||||
*/
|
||||
getBackgroundCommandSummary(): string | undefined {
|
||||
const summary = this.standaloneManager.getBackgroundCommandsSummary()
|
||||
return summary || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to show the background terminal suggestion.
|
||||
* Shows suggestion if there have been 3+ shell integration warnings in the last hour,
|
||||
* and we haven't shown the suggestion in the last hour.
|
||||
*
|
||||
* @returns true if the suggestion should be shown, false otherwise
|
||||
*/
|
||||
private shouldShowBackgroundTerminalSuggestion(): boolean {
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000
|
||||
|
||||
// Clean old timestamps (older than 1 hour)
|
||||
this.shellIntegrationWarningTracker.timestamps = this.shellIntegrationWarningTracker.timestamps.filter(
|
||||
(ts) => ts > oneHourAgo,
|
||||
)
|
||||
|
||||
// Add current warning
|
||||
this.shellIntegrationWarningTracker.timestamps.push(Date.now())
|
||||
|
||||
// Check if we've shown suggestion recently (within last hour)
|
||||
if (
|
||||
this.shellIntegrationWarningTracker.lastSuggestionShown &&
|
||||
Date.now() - this.shellIntegrationWarningTracker.lastSuggestionShown < 60 * 60 * 1000
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Show suggestion if 3+ warnings in last hour
|
||||
if (this.shellIntegrationWarningTracker.timestamps.length >= 3) {
|
||||
this.shellIntegrationWarningTracker.lastSuggestionShown = Date.now()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -675,7 +675,7 @@ export async function orchestrateCommandExecution(
|
||||
/**
|
||||
* Helper to find last index matching a predicate
|
||||
*/
|
||||
export function findLastIndex<T>(array: T[], predicate: (item: T) => boolean): number {
|
||||
function findLastIndex<T>(array: T[], predicate: (item: T) => boolean): number {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
if (predicate(array[i])) {
|
||||
return i
|
||||
|
||||
@@ -82,10 +82,10 @@ export const BACKGROUND_COMMAND_TIMEOUT_MS = 10 * 60 * 1000
|
||||
// Used to detect if a command is compiling/building
|
||||
|
||||
/** Markers that indicate compilation is starting */
|
||||
export const COMPILING_MARKERS = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
|
||||
const COMPILING_MARKERS = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
|
||||
|
||||
/** Markers that indicate compilation is done (nullify extended timeout) */
|
||||
export const COMPILING_NULLIFIERS = [
|
||||
const COMPILING_NULLIFIERS = [
|
||||
"compiled",
|
||||
"success",
|
||||
"finish",
|
||||
|
||||
@@ -20,36 +20,8 @@
|
||||
*/
|
||||
|
||||
// Export unified command executor
|
||||
export { CommandExecutor } from "./CommandExecutor"
|
||||
|
||||
// Export command orchestrator (shared logic)
|
||||
export { findLastIndex, orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
|
||||
// Export standalone terminal implementations
|
||||
export { StandaloneTerminal } from "./standalone/StandaloneTerminal"
|
||||
export { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
|
||||
export { StandaloneTerminalProcess } from "./standalone/StandaloneTerminalProcess"
|
||||
export { StandaloneTerminalRegistry } from "./standalone/StandaloneTerminalRegistry"
|
||||
|
||||
// Export all types from types.ts
|
||||
export type {
|
||||
// Command Executor types
|
||||
ActiveBackgroundCommand,
|
||||
AskResponse,
|
||||
CommandExecutionOptions,
|
||||
CommandExecutorCallbacks,
|
||||
CommandExecutorConfig,
|
||||
FullCommandExecutorConfig,
|
||||
// Terminal types
|
||||
ITerminal,
|
||||
ITerminalManager,
|
||||
ITerminalProcess,
|
||||
ITerminalProcessResult,
|
||||
// Command Orchestrator types
|
||||
OrchestrationOptions,
|
||||
OrchestrationResult,
|
||||
StandaloneTerminalOptions,
|
||||
TerminalInfo,
|
||||
TerminalProcessEvents,
|
||||
TerminalProcessResultPromise,
|
||||
} from "./types"
|
||||
export type { ITerminalManager } from "./types"
|
||||
|
||||
@@ -131,13 +131,6 @@ export interface ITerminal {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal process result interface.
|
||||
* @deprecated Use ITerminalProcess instead.
|
||||
* This is kept for backwards compatibility.
|
||||
*/
|
||||
export type ITerminalProcessResult = ITerminalProcess
|
||||
|
||||
/**
|
||||
* Promise-like interface for terminal process results.
|
||||
* Combines Promise<void> with ITerminalProcess for flexible usage.
|
||||
@@ -281,34 +274,10 @@ export interface BackgroundCommand {
|
||||
// Command Executor Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Tracker for shell integration warnings to determine when to show background terminal suggestion.
|
||||
* Used internally by CommandExecutor to track warning frequency.
|
||||
*/
|
||||
export interface ShellIntegrationWarningTracker {
|
||||
/** Timestamps of recent shell integration warnings */
|
||||
timestamps: number[]
|
||||
/** Timestamp when the suggestion was last shown */
|
||||
lastSuggestionShown?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an active background command that can be cancelled
|
||||
* @deprecated Use BackgroundCommand instead
|
||||
*/
|
||||
export interface ActiveBackgroundCommand {
|
||||
process: {
|
||||
terminate?: () => void
|
||||
continue?: () => void
|
||||
}
|
||||
command: string
|
||||
outputLines: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from an ask() call
|
||||
*/
|
||||
export interface AskResponse {
|
||||
interface AskResponse {
|
||||
response: string // "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
text?: string
|
||||
images?: string[]
|
||||
@@ -343,41 +312,6 @@ export interface CommandExecutorCallbacks {
|
||||
addToUserMessageContent: (content: { type: string; text: string }) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional per-command execution behavior overrides.
|
||||
*/
|
||||
export interface CommandExecutionOptions {
|
||||
/**
|
||||
* Force command execution in standalone/background terminal mode for this command.
|
||||
* This is useful for subagent runs and headless-style execution flows.
|
||||
*/
|
||||
useBackgroundExecution?: boolean
|
||||
/**
|
||||
* Suppress command interaction/output UI messages (ask/say) for this command execution.
|
||||
* Command output is still captured and returned as the tool result.
|
||||
*/
|
||||
suppressUserInteraction?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for CommandExecutor
|
||||
*/
|
||||
export interface CommandExecutorConfig {
|
||||
/** Working directory for command execution */
|
||||
cwd: string
|
||||
/** Task ID for tracking */
|
||||
taskId: string
|
||||
/** Unique task identifier */
|
||||
ulid: string
|
||||
/** Terminal execution mode */
|
||||
terminalExecutionMode: "vscodeTerminal" | "backgroundExec"
|
||||
/** The primary terminal manager (VSCode or Standalone) */
|
||||
terminalManager: ITerminalManager
|
||||
}
|
||||
|
||||
/** Alias for backwards compatibility */
|
||||
export type FullCommandExecutorConfig = CommandExecutorConfig
|
||||
|
||||
// =============================================================================
|
||||
// Command Orchestrator Types
|
||||
// =============================================================================
|
||||
|
||||
@@ -15,7 +15,6 @@ export { Controller } from "./SdkController"
|
||||
export * from "./sdk-followup-coordinator"
|
||||
export * from "./sdk-interaction-coordinator"
|
||||
export * from "./sdk-mcp-coordinator"
|
||||
export type { SessionEventListener } from "./sdk-message-coordinator"
|
||||
export * from "./sdk-message-coordinator"
|
||||
export * from "./sdk-mode-coordinator"
|
||||
export * from "./sdk-session-config-builder"
|
||||
|
||||
@@ -31,47 +31,47 @@ export function resolveDataDir(override?: string): string {
|
||||
}
|
||||
|
||||
/** Path to globalState.json */
|
||||
export function globalStatePath(dataDir?: string): string {
|
||||
function globalStatePath(dataDir?: string): string {
|
||||
return path.join(resolveDataDir(dataDir), "globalState.json")
|
||||
}
|
||||
|
||||
/** Path to secrets.json */
|
||||
export function secretsPath(dataDir?: string): string {
|
||||
function secretsPath(dataDir?: string): string {
|
||||
return path.join(resolveDataDir(dataDir), "secrets.json")
|
||||
}
|
||||
|
||||
/** Path to taskHistory.json (stored in state/ subdirectory) */
|
||||
export function taskHistoryPath(dataDir?: string): string {
|
||||
function taskHistoryPath(dataDir?: string): string {
|
||||
return path.join(resolveDataDir(dataDir), "state", "taskHistory.json")
|
||||
}
|
||||
|
||||
/** Path to MCP settings file */
|
||||
export function mcpSettingsPath(dataDir?: string): string {
|
||||
function mcpSettingsPath(dataDir?: string): string {
|
||||
return path.join(resolveDataDir(dataDir), "settings", "cline_mcp_settings.json")
|
||||
}
|
||||
|
||||
/** Path to a task directory */
|
||||
export function taskDirPath(taskId: string, dataDir?: string): string {
|
||||
function taskDirPath(taskId: string, dataDir?: string): string {
|
||||
return path.join(resolveDataDir(dataDir), "tasks", taskId)
|
||||
}
|
||||
|
||||
/** Path to api_conversation_history.json for a task */
|
||||
export function apiConversationHistoryPath(taskId: string, dataDir?: string): string {
|
||||
function apiConversationHistoryPath(taskId: string, dataDir?: string): string {
|
||||
return path.join(taskDirPath(taskId, dataDir), "api_conversation_history.json")
|
||||
}
|
||||
|
||||
/** Path to ui_messages.json for a task */
|
||||
export function uiMessagesPath(taskId: string, dataDir?: string): string {
|
||||
function uiMessagesPath(taskId: string, dataDir?: string): string {
|
||||
return path.join(taskDirPath(taskId, dataDir), "ui_messages.json")
|
||||
}
|
||||
|
||||
/** Path to context_history.json for a task */
|
||||
export function contextHistoryPath(taskId: string, dataDir?: string): string {
|
||||
function contextHistoryPath(taskId: string, dataDir?: string): string {
|
||||
return path.join(taskDirPath(taskId, dataDir), "context_history.json")
|
||||
}
|
||||
|
||||
/** Path to task_metadata.json for a task */
|
||||
export function taskMetadataPath(taskId: string, dataDir?: string): string {
|
||||
function taskMetadataPath(taskId: string, dataDir?: string): string {
|
||||
return path.join(taskDirPath(taskId, dataDir), "task_metadata.json")
|
||||
}
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ export class MessageTranslatorState {
|
||||
* ask_question/ask_followup_question → (not a visual tool — handled by askQuestion executor in SdkController)
|
||||
* MCP tools (serverName__toolName) → (handled before reaching sdkToolToClineSayTool — emitted as say="use_mcp_server")
|
||||
*/
|
||||
export function sdkToolToClineSayTool(toolName: string, input?: unknown): ClineSayTool {
|
||||
function sdkToolToClineSayTool(toolName: string, input?: unknown): ClineSayTool {
|
||||
// Parse input if it's a string (some SDK tools pass stringified JSON)
|
||||
const parsedInput = parseToolInput(input)
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface EffectiveProviderConfig {
|
||||
* Empty patches are allowed and are no-ops. A field present with value
|
||||
* `null` means "clear this field"; an absent field means "leave unchanged."
|
||||
*/
|
||||
export interface ProviderReasoningPatch {
|
||||
interface ProviderReasoningPatch {
|
||||
readonly enabled?: boolean
|
||||
readonly effort?: string // "none" | "low" | "medium" | "high" | "xhigh"
|
||||
readonly budgetTokens?: number
|
||||
@@ -230,7 +230,7 @@ export interface ProviderListing {
|
||||
* Source of a model-list result. Observability counterpart to the cache
|
||||
* record. Logged but not used for control flow.
|
||||
*/
|
||||
export type CatalogSource = "sdk-dynamic" | "sdk-bundled" | "extension-dynamic" | "legacy-static" | "host-adapter"
|
||||
type CatalogSource = "sdk-dynamic" | "sdk-bundled" | "extension-dynamic" | "legacy-static" | "host-adapter"
|
||||
|
||||
/**
|
||||
* Structured error from `resolveModels`. Carries enough information to
|
||||
|
||||
@@ -40,11 +40,7 @@ export interface BuildApiHandlerOptions {
|
||||
* BOTH. We therefore send at most one — preferring an explicit thinking budget
|
||||
* over effort — or none when reasoning is disabled.
|
||||
*/
|
||||
export function buildSdkProviderConfig(
|
||||
configuration: ApiConfiguration,
|
||||
mode: Mode,
|
||||
options?: BuildApiHandlerOptions,
|
||||
): ProviderConfig {
|
||||
function buildSdkProviderConfig(configuration: ApiConfiguration, mode: Mode, options?: BuildApiHandlerOptions): ProviderConfig {
|
||||
const providerId = (mode === "plan" ? configuration.planModeApiProvider : configuration.actModeApiProvider) ?? "cline"
|
||||
|
||||
const apiKey = resolveApiKey(providerId, configuration)
|
||||
|
||||
@@ -8,8 +8,8 @@ import { buildToolPolicies } from "./sdk-tool-policies"
|
||||
import type { SdkSessionHost } from "./session-host"
|
||||
import { VscodeSessionHost } from "./vscode-session-host"
|
||||
|
||||
export type RequestToolApprovalHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["requestToolApproval"]>
|
||||
export type AskQuestionHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["askQuestion"]>
|
||||
type RequestToolApprovalHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["requestToolApproval"]>
|
||||
type AskQuestionHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["askQuestion"]>
|
||||
|
||||
export interface SdkSessionLifecycleOptions {
|
||||
mcpHub: McpHub
|
||||
|
||||
@@ -14,16 +14,6 @@ import { sdkMessagesToClineMessages } from "./message-translator"
|
||||
import type { SdkSessionLifecycle } from "./sdk-session-lifecycle"
|
||||
import type { VscodeSessionHost } from "./vscode-session-host"
|
||||
|
||||
export interface TaskWithId {
|
||||
historyItem: HistoryItem
|
||||
taskDirPath: string
|
||||
apiConversationHistoryFilePath: string
|
||||
uiMessagesFilePath: string
|
||||
contextHistoryFilePath: string
|
||||
taskMetadataFilePath: string
|
||||
apiConversationHistory: unknown[]
|
||||
}
|
||||
|
||||
export interface TaskUsage {
|
||||
tokensIn: number
|
||||
tokensOut: number
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface MessageStateHandlerEvents {
|
||||
* Change event for message updates.
|
||||
* Mirrors ClineMessageChange from src/core/task/message-state.ts.
|
||||
*/
|
||||
export interface ClineMessageChange {
|
||||
interface ClineMessageChange {
|
||||
type: "add" | "update" | "set" | "delete"
|
||||
/** The full array after the change */
|
||||
messages: ClineMessage[]
|
||||
@@ -117,7 +117,7 @@ export class MessageStateHandler extends EventEmitter<MessageStateHandlerEvents>
|
||||
* The `api` property is settable — updateSettings() replaces it when the
|
||||
* user switches models/providers.
|
||||
*/
|
||||
export interface TaskProxyApi {
|
||||
interface TaskProxyApi {
|
||||
getModel: () => { id: string }
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export interface TaskProxyApi {
|
||||
* The updateSettings handler calls setDefaultTerminalProfile() which
|
||||
* returns { closedCount, busyTerminals }. This stub safely no-ops.
|
||||
*/
|
||||
export interface TaskProxyTerminalManager {
|
||||
interface TaskProxyTerminalManager {
|
||||
setDefaultTerminalProfile: (profileId: string) => { closedCount: number; busyTerminals: never[] }
|
||||
setShellIntegrationTimeout: (timeout: number) => void
|
||||
setTerminalReuseEnabled: (enabled: boolean) => void
|
||||
@@ -137,7 +137,7 @@ export interface TaskProxyTerminalManager {
|
||||
* Task state tracking — mirrors the subset of classic TaskState
|
||||
* that handlers reference.
|
||||
*/
|
||||
export interface TaskProxyState {
|
||||
interface TaskProxyState {
|
||||
askResponse?: ClineAskResponse
|
||||
autoRetryAttempts?: number
|
||||
/** Checkpoint manager initialized flag (stub — checkpoints removed) */
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { isMultiRootWorkspace } from "@/core/workspace/utils/workspace-detection"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { EmptyRequest } from "@/shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// Canonical header names for extra client/host context
|
||||
export const ClineHeaders = {
|
||||
const ClineHeaders = {
|
||||
PLATFORM: "X-PLATFORM",
|
||||
PLATFORM_VERSION: "X-PLATFORM-VERSION",
|
||||
CLIENT_VERSION: "X-CLIENT-VERSION",
|
||||
@@ -13,7 +12,6 @@ export const ClineHeaders = {
|
||||
CORE_VERSION: "X-CORE-VERSION",
|
||||
IS_MULTIROOT: "X-IS-MULTIROOT",
|
||||
} as const
|
||||
export type ClineHeaderName = (typeof ClineHeaders)[keyof typeof ClineHeaders]
|
||||
|
||||
export function buildExternalBasicHeaders(): Record<string, string> {
|
||||
return {
|
||||
@@ -40,17 +38,3 @@ export async function buildBasicClineHeaders(): Promise<Record<string, string>>
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function buildClineExtraHeaders(): Promise<Record<string, string>> {
|
||||
const headers = await buildBasicClineHeaders()
|
||||
|
||||
try {
|
||||
const isMultiRoot = await isMultiRootWorkspace()
|
||||
headers[ClineHeaders.IS_MULTIROOT] = isMultiRoot ? "true" : "false"
|
||||
} catch (error) {
|
||||
Logger.log("Failed to detect multi-root workspace", error)
|
||||
headers[ClineHeaders.IS_MULTIROOT] = "false"
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
// All modules that import AuthService from this path continue to work
|
||||
// because the SDK AuthService exposes the same interface.
|
||||
|
||||
export { AuthService, type ClineAccountOrganization, type ClineAccountUserInfo, type ClineAuthInfo } from "@/sdk/auth-service"
|
||||
export { AuthService, type ClineAccountUserInfo } from "@/sdk/auth-service" // ServiceConfig was defined in the classic AuthService but never imported
|
||||
|
||||
// ServiceConfig was defined in the classic AuthService but never imported
|
||||
// by any other module. Re-export a compatible type for safety.
|
||||
export type ServiceConfig = {
|
||||
type ServiceConfig = {
|
||||
URI?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type PkceState = {
|
||||
redirect_uri: string
|
||||
}
|
||||
|
||||
export class OcaRefreshError extends Error {
|
||||
class OcaRefreshError extends Error {
|
||||
status?: number
|
||||
code?: string
|
||||
invalidGrant?: boolean
|
||||
|
||||
@@ -14,8 +14,6 @@ export const DEFAULT_EXTERNAL_IDCS_URL = "https://login-ext.identity.oraclecloud
|
||||
export const DEFAULT_EXTERNAL_IDSC_SCOPES = "openid offline_access"
|
||||
export const DEFAULT_EXTERNAL_OCA_BASE_URL = "https://code.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm"
|
||||
|
||||
export const OCI_HEADER_OPC_REQUEST_ID = "opc-request-id"
|
||||
|
||||
export const CHAT_COMPLETIONS_API = "CHAT_COMPLETIONS"
|
||||
export const RESPONSES_API = "RESPONSES"
|
||||
export const MESSAGES_API = "MESSAGES"
|
||||
|
||||
@@ -93,7 +93,7 @@ export function pkceChallengeFromVerifier(verifier: string): string {
|
||||
*
|
||||
* Use: Send this single value as the opc-request-id header.
|
||||
*/
|
||||
export async function generateOpcRequestId(taskId: string, token: string): Promise<string> {
|
||||
async function generateOpcRequestId(taskId: string, token: string): Promise<string> {
|
||||
async function hash8(str: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(str)
|
||||
const hash = await crypto.subtle.digest("SHA-256", data)
|
||||
@@ -142,7 +142,7 @@ export async function createOcaHeaders(accessToken: string, taskId: string): Pro
|
||||
* Use only for non-security, informational, or display purposes.
|
||||
* @param token JWT string
|
||||
*/
|
||||
export function parseJwtPayload<T extends JwtPayload>(token: string): T | null {
|
||||
function parseJwtPayload<T extends JwtPayload>(token: string): T | null {
|
||||
try {
|
||||
const payload = jwtDecode<T>(token)
|
||||
return payload
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function isPortOpen(host: string, port: number, timeout = 1000): Pr
|
||||
/**
|
||||
* Try to connect to Chrome at a specific IP address
|
||||
*/
|
||||
export async function tryConnect(ipAddress: string): Promise<{ endpoint: string; ip: string } | null> {
|
||||
async function tryConnect(ipAddress: string): Promise<{ endpoint: string; ip: string } | null> {
|
||||
try {
|
||||
const response = await axios.get(`http://${ipAddress}:9222/json/version`, { timeout: 1000 })
|
||||
const data = response.data
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { serializeError } from "serialize-error";
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "../../shared/ClineAccount";
|
||||
import { serializeError } from "serialize-error"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "../../shared/ClineAccount"
|
||||
|
||||
export enum ClineErrorType {
|
||||
Auth = "auth",
|
||||
Network = "network",
|
||||
RateLimit = "rateLimit",
|
||||
Balance = "balance",
|
||||
SpendLimit = "spendLimit",
|
||||
@@ -16,50 +15,42 @@ interface ErrorDetails {
|
||||
/**
|
||||
* The HTTP status code of the error, if applicable.
|
||||
*/
|
||||
status?: number;
|
||||
status?: number
|
||||
/**
|
||||
* The request ID associated with the error, if available.
|
||||
* This can be useful for debugging and support.
|
||||
*/
|
||||
request_id?: string;
|
||||
request_id?: string
|
||||
/**
|
||||
* Specific error code provided by the API or service.
|
||||
*/
|
||||
code?: string;
|
||||
code?: string
|
||||
/**
|
||||
* The model ID associated with the error, if applicable.
|
||||
* This is useful for identifying which model the error relates to.
|
||||
*/
|
||||
modelId?: string;
|
||||
modelId?: string
|
||||
/**
|
||||
* The provider ID associated with the error, if applicable.
|
||||
* This is useful for identifying which provider the error relates to.
|
||||
*/
|
||||
providerId?: string;
|
||||
providerId?: string
|
||||
/**
|
||||
* The error message associated with the error, if applicable.
|
||||
*/
|
||||
message?: string;
|
||||
message?: string
|
||||
// Additional details that might be present in the error
|
||||
// This can include things like current balance, error messages, etc.
|
||||
details?: any;
|
||||
details?: any
|
||||
}
|
||||
|
||||
const RATE_LIMIT_PATTERNS = [
|
||||
/status code 429/i,
|
||||
/rate limit/i,
|
||||
/too many requests/i,
|
||||
/quota exceeded/i,
|
||||
/resource exhausted/i,
|
||||
];
|
||||
const ORG_CLINE_PASS_RESTRICTION_MESSAGE =
|
||||
"organization accounts cannot use individual model inference subscriptions";
|
||||
const ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE =
|
||||
"organization accounts cannot use clinepass subscriptions";
|
||||
const RATE_LIMIT_PATTERNS = [/status code 429/i, /rate limit/i, /too many requests/i, /quota exceeded/i, /resource exhausted/i]
|
||||
const ORG_CLINE_PASS_RESTRICTION_MESSAGE = "organization accounts cannot use individual model inference subscriptions"
|
||||
const ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE = "organization accounts cannot use clinepass subscriptions"
|
||||
|
||||
export class ClineError extends Error {
|
||||
readonly title = "ClineError";
|
||||
readonly _error: ErrorDetails;
|
||||
readonly title = "ClineError"
|
||||
readonly _error: ErrorDetails
|
||||
|
||||
// Error details per providers:
|
||||
// Cline: error?.error
|
||||
@@ -70,19 +61,15 @@ export class ClineError extends Error {
|
||||
public readonly modelId?: string,
|
||||
public readonly providerId?: string,
|
||||
) {
|
||||
const error = serializeError(raw);
|
||||
const error = serializeError(raw)
|
||||
|
||||
const message =
|
||||
error.message ||
|
||||
error?.response?.message ||
|
||||
String(error) ||
|
||||
error?.cause?.means;
|
||||
super(message);
|
||||
const message = error.message || error?.response?.message || String(error) || error?.cause?.means
|
||||
super(message)
|
||||
|
||||
// Extract status from multiple possible locations
|
||||
const status = error.status || error.statusCode || error.response?.status;
|
||||
this.modelId = modelId || error.modelId;
|
||||
this.providerId = providerId || error.providerId;
|
||||
const status = error.status || error.statusCode || error.response?.status
|
||||
this.modelId = modelId || error.modelId
|
||||
this.providerId = providerId || error.providerId
|
||||
|
||||
// Construct the error details object to includes relevant information
|
||||
// And ensure it has a consistent structure
|
||||
@@ -100,7 +87,7 @@ export class ClineError extends Error {
|
||||
providerId: this.providerId,
|
||||
details: error.details || error.error, // Additional details provided by the server
|
||||
stack: undefined, // Avoid serializing stack trace to keep the error object clean
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +103,7 @@ export class ClineError extends Error {
|
||||
modelId: this.modelId,
|
||||
providerId: this.providerId,
|
||||
details: this._error.details,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,33 +111,29 @@ export class ClineError extends Error {
|
||||
*/
|
||||
static parse(errorStr?: string, modelId?: string): ClineError | undefined {
|
||||
if (!errorStr || typeof errorStr !== "string") {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return ClineError.transform(errorStr, modelId);
|
||||
return ClineError.transform(errorStr, modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms any object into a ClineError instance.
|
||||
* Always returns a ClineError, even if the input is not a valid error object.
|
||||
*/
|
||||
static transform(
|
||||
error: any,
|
||||
modelId?: string,
|
||||
providerId?: string,
|
||||
): ClineError {
|
||||
static transform(error: any, modelId?: string, providerId?: string): ClineError {
|
||||
try {
|
||||
// If already a ClineError, return it directly to prevent infinite recursion
|
||||
if (error instanceof ClineError) {
|
||||
return error;
|
||||
return error
|
||||
}
|
||||
return new ClineError(JSON.parse(error), modelId, providerId);
|
||||
return new ClineError(JSON.parse(error), modelId, providerId)
|
||||
} catch {
|
||||
return new ClineError(error, modelId, providerId);
|
||||
return new ClineError(error, modelId, providerId)
|
||||
}
|
||||
}
|
||||
|
||||
public isErrorType(type: ClineErrorType): boolean {
|
||||
return ClineError.getErrorType(this) === type;
|
||||
return ClineError.getErrorType(this) === type
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,102 +141,66 @@ export class ClineError extends Error {
|
||||
* This is useful for determining how to handle the error in the UI or logic.
|
||||
*/
|
||||
static getErrorType(err: ClineError): ClineErrorType | undefined {
|
||||
const { code, status, details } = err._error;
|
||||
const message = (
|
||||
err._error?.message ||
|
||||
err.message ||
|
||||
JSON.stringify(err._error)
|
||||
)?.toLowerCase();
|
||||
const { code, status, details } = err._error
|
||||
const message = (err._error?.message || err.message || JSON.stringify(err._error))?.toLowerCase()
|
||||
|
||||
// Check balance error first (most specific)
|
||||
if (
|
||||
code === "insufficient_credits" &&
|
||||
typeof details?.current_balance === "number"
|
||||
) {
|
||||
return ClineErrorType.Balance;
|
||||
if (code === "insufficient_credits" && typeof details?.current_balance === "number") {
|
||||
return ClineErrorType.Balance
|
||||
}
|
||||
|
||||
// Check spend limit exceeded (org-enforced budget cap, 429 SPEND_LIMIT_EXCEEDED)
|
||||
// Must be checked before the generic rate-limit check since both use 429
|
||||
if (
|
||||
code === "SPEND_LIMIT_EXCEEDED" ||
|
||||
details?.code === "SPEND_LIMIT_EXCEEDED"
|
||||
) {
|
||||
return ClineErrorType.SpendLimit;
|
||||
if (code === "SPEND_LIMIT_EXCEEDED" || details?.code === "SPEND_LIMIT_EXCEEDED") {
|
||||
return ClineErrorType.SpendLimit
|
||||
}
|
||||
|
||||
// ClinePass entitlement errors are user-actionable and should not fall through to generic 403 auth.
|
||||
// The organization-account variant gets separate copy because subscribing is not the right action.
|
||||
const isEntitlementCode =
|
||||
code === "ENTITLEMENT_ERROR" || details?.code === "ENTITLEMENT_ERROR";
|
||||
const entitlementText =
|
||||
`${message ?? ""} ${details?.message ?? ""}`.toLowerCase();
|
||||
const isEntitlementCode = code === "ENTITLEMENT_ERROR" || details?.code === "ENTITLEMENT_ERROR"
|
||||
const entitlementText = `${message ?? ""} ${details?.message ?? ""}`.toLowerCase()
|
||||
if (
|
||||
isEntitlementCode &&
|
||||
(entitlementText.includes(ORG_CLINE_PASS_RESTRICTION_MESSAGE) ||
|
||||
entitlementText.includes(ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE))
|
||||
) {
|
||||
return ClineErrorType.OrgClinePassRestriction;
|
||||
return ClineErrorType.OrgClinePassRestriction
|
||||
}
|
||||
if (
|
||||
isEntitlementCode &&
|
||||
entitlementText.includes("not subscribed to required model plan")
|
||||
) {
|
||||
return ClineErrorType.Entitlement;
|
||||
if (isEntitlementCode && entitlementText.includes("not subscribed to required model plan")) {
|
||||
return ClineErrorType.Entitlement
|
||||
}
|
||||
|
||||
// Check auth errors
|
||||
const isAuthStatus = status !== undefined && status > 400 && status < 429;
|
||||
if (
|
||||
code === "ERR_BAD_REQUEST" ||
|
||||
err instanceof AuthInvalidTokenError ||
|
||||
isAuthStatus
|
||||
) {
|
||||
return ClineErrorType.Auth;
|
||||
const isAuthStatus = status !== undefined && status > 400 && status < 429
|
||||
if (code === "ERR_BAD_REQUEST" || err instanceof AuthInvalidTokenError || isAuthStatus) {
|
||||
return ClineErrorType.Auth
|
||||
}
|
||||
|
||||
if (code === "INFERENCE_CAP_ERROR") {
|
||||
return ClineErrorType.QuotaExceeded;
|
||||
return ClineErrorType.QuotaExceeded
|
||||
}
|
||||
|
||||
if (message) {
|
||||
// Check for specific error codes/messages if applicable
|
||||
const authErrorRegex = [
|
||||
/(?:in)?valid[-_ ]?(?:api )?(?:token|key)/i,
|
||||
/authentication[-_ ]?failed/i,
|
||||
/unauthorized/i,
|
||||
];
|
||||
if (
|
||||
message?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) ||
|
||||
authErrorRegex.some((regex) => regex.test(message))
|
||||
) {
|
||||
return ClineErrorType.Auth;
|
||||
const authErrorRegex = [/(?:in)?valid[-_ ]?(?:api )?(?:token|key)/i, /authentication[-_ ]?failed/i, /unauthorized/i]
|
||||
if (message?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) || authErrorRegex.some((regex) => regex.test(message))) {
|
||||
return ClineErrorType.Auth
|
||||
}
|
||||
|
||||
// Check rate limit patterns
|
||||
const lowerMessage = message.toLowerCase();
|
||||
const lowerMessage = message.toLowerCase()
|
||||
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(lowerMessage))) {
|
||||
return ClineErrorType.RateLimit;
|
||||
return ClineErrorType.RateLimit
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthNetworkError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
override readonly cause?: Error,
|
||||
) {
|
||||
super(message);
|
||||
this.name = ClineErrorType.Network;
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthInvalidTokenError extends Error {
|
||||
class AuthInvalidTokenError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = ClineErrorType.Auth;
|
||||
super(message)
|
||||
this.name = ClineErrorType.Auth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PostHogErrorProvider } from "./providers/PostHogErrorProvider"
|
||||
/**
|
||||
* Supported error provider types
|
||||
*/
|
||||
export type ErrorProviderType = "posthog" | "no-op"
|
||||
type ErrorProviderType = "posthog" | "no-op"
|
||||
|
||||
/**
|
||||
* Configuration for error providers
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { ErrorSettings } from "./providers/IErrorProvider"
|
||||
|
||||
export { ClineError, ClineErrorType } from "./ClineError"
|
||||
export { type ErrorProviderConfig, ErrorProviderFactory, type ErrorProviderType } from "./ErrorProviderFactory"
|
||||
export { ErrorService } from "./ErrorService"
|
||||
export type { ErrorSettings, IErrorProvider } from "./providers/IErrorProvider"
|
||||
export { PostHogErrorProvider } from "./providers/PostHogErrorProvider"
|
||||
|
||||
export type { ErrorSettings } from "./providers/IErrorProvider"
|
||||
export function getErrorLevelFromString(level: string | undefined): ErrorSettings["level"] {
|
||||
switch (level) {
|
||||
case "disabled":
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PostHogFeatureFlagsProvider } from "./providers/PostHogFeatureFlagsProv
|
||||
/**
|
||||
* Supported feature flags provider types
|
||||
*/
|
||||
export type FeatureFlagsProviderType = "posthog" | "no-op"
|
||||
type FeatureFlagsProviderType = "posthog" | "no-op"
|
||||
|
||||
/**
|
||||
* Configuration for feature flags providers
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
export {
|
||||
type FeatureFlagsProviderConfig,
|
||||
FeatureFlagsProviderFactory,
|
||||
type FeatureFlagsProviderType,
|
||||
} from "./FeatureFlagsProviderFactory"
|
||||
export { FeatureFlagsService } from "./FeatureFlagsService"
|
||||
export type { FeatureFlagsSettings, IFeatureFlagsProvider } from "./providers/IFeatureFlagsProvider"
|
||||
export { PostHogFeatureFlagsProvider } from "./providers/PostHogFeatureFlagsProvider"
|
||||
|
||||
import { FeatureFlagsProviderFactory } from "./FeatureFlagsProviderFactory"
|
||||
import { FeatureFlagsService } from "./FeatureFlagsService"
|
||||
@@ -25,13 +18,6 @@ export function getFeatureFlagsService(): FeatureFlagsService {
|
||||
return _featureFlagsServiceInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the feature flags service instance (useful for testing)
|
||||
*/
|
||||
export function resetFeatureFlagsService(): void {
|
||||
_featureFlagsServiceInstance = null
|
||||
}
|
||||
|
||||
export const featureFlagsService = new Proxy({} as FeatureFlagsService, {
|
||||
get(_target, prop, _receiver) {
|
||||
const service = getFeatureFlagsService()
|
||||
|
||||
@@ -20,7 +20,7 @@ import { shouldStartNewOAuthFlow } from "./mcpOAuthFlow"
|
||||
* "Authenticate"; 10 minutes leaves room to create an account, while still
|
||||
* guaranteeing a stale flow eventually expires so the system makes progress.
|
||||
*/
|
||||
export const MCP_OAUTH_STATE_EXPIRY_MS = 10 * 60 * 1000 // 10 minutes
|
||||
const MCP_OAUTH_STATE_EXPIRY_MS = 10 * 60 * 1000 // 10 minutes
|
||||
|
||||
/**
|
||||
* Structure for all OAuth data stored in the single mcpOAuthSecrets JSON
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DEFAULT_MCP_TIMEOUT_SECONDS, MIN_MCP_TIMEOUT_SECONDS } from "@shared/mc
|
||||
import { z } from "zod"
|
||||
import { TYPE_ERROR_MESSAGE } from "./constants"
|
||||
|
||||
export const AutoApproveSchema = z.array(z.string()).default([])
|
||||
const AutoApproveSchema = z.array(z.string()).default([])
|
||||
|
||||
export const BaseConfigSchema = z.object({
|
||||
autoApprove: AutoApproveSchema.optional(),
|
||||
|
||||
@@ -16,6 +16,4 @@ export type McpConnection = {
|
||||
authProvider?: OAuthClientProvider
|
||||
}
|
||||
|
||||
export type McpTransportType = "stdio" | "sse" | "http"
|
||||
|
||||
export type McpServerConfig = z.infer<typeof ServerConfigSchema>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getBinaryLocation } from "@/utils/fs"
|
||||
export type FileSearchSource = "host_index" | "ripgrep"
|
||||
|
||||
// Wrapper function for childProcess.spawn
|
||||
export type SpawnFunction = typeof childProcess.spawn
|
||||
type SpawnFunction = typeof childProcess.spawn
|
||||
export const getSpawnFunction = (): SpawnFunction => childProcess.spawn
|
||||
|
||||
/** Thrown when ripgrep fails to spawn or exits non-zero. */
|
||||
|
||||
@@ -12,11 +12,6 @@ import { OpenTelemetryTelemetryProvider } from "./providers/opentelemetry/OpenTe
|
||||
import { PostHogClientProvider } from "./providers/posthog/PostHogClientProvider"
|
||||
import { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
|
||||
|
||||
/**
|
||||
* Supported telemetry provider types
|
||||
*/
|
||||
export type TelemetryProviderType = "posthog" | "no-op" | "opentelemetry"
|
||||
|
||||
/**
|
||||
* Configuration for telemetry providers
|
||||
*/
|
||||
|
||||
@@ -46,7 +46,6 @@ export type TerminalOutputMethod = VscodeOutputMethod | StandaloneOutputMethod
|
||||
export enum TerminalOutputFailureReason {
|
||||
TIMEOUT = "timeout",
|
||||
NO_SHELL_INTEGRATION = "no_shell_integration",
|
||||
CLIPBOARD_FAILED = "clipboard_failed",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +53,6 @@ export enum TerminalOutputFailureReason {
|
||||
*/
|
||||
export enum TerminalUserInterventionAction {
|
||||
PROCESS_WHILE_RUNNING = "process_while_running",
|
||||
MANUAL_PASTE = "manual_paste",
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
|
||||
@@ -64,7 +62,6 @@ export enum TerminalUserInterventionAction {
|
||||
export enum TerminalHangStage {
|
||||
WAITING_FOR_COMPLETION = "waiting_for_completion",
|
||||
BUFFER_STUCK = "buffer_stuck",
|
||||
STREAM_TIMEOUT = "stream_timeout",
|
||||
}
|
||||
|
||||
export type TelemetryMetadata = {
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
export type {
|
||||
ITelemetryProvider,
|
||||
TelemetrySettings,
|
||||
} from "./providers/ITelemetryProvider"
|
||||
export { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
|
||||
export {
|
||||
type TelemetryProviderConfig,
|
||||
TelemetryProviderFactory,
|
||||
type TelemetryProviderType,
|
||||
} from "./TelemetryProviderFactory"
|
||||
// Export terminal type definitions for type-safe telemetry
|
||||
export type {
|
||||
StandaloneOutputMethod,
|
||||
TerminalOutputMethod,
|
||||
TerminalType,
|
||||
VscodeOutputMethod,
|
||||
} from "./TelemetryService"
|
||||
// Export the enums and types for terminal telemetry
|
||||
export {
|
||||
TerminalHangStage,
|
||||
TerminalOutputFailureReason,
|
||||
TerminalUserInterventionAction,
|
||||
} from "./TelemetryService"
|
||||
export { TerminalHangStage, TerminalOutputFailureReason, TerminalUserInterventionAction } from "./TelemetryService"
|
||||
|
||||
// Create a singleton instance for easy access throughout the application
|
||||
import { TelemetryService } from "./TelemetryService"
|
||||
@@ -33,7 +13,7 @@ let _initializationPromise: Promise<TelemetryService> | null = null
|
||||
* @param distinctId Optional distinct ID for the telemetry provider
|
||||
* @returns TelemetryService instance
|
||||
*/
|
||||
export async function getTelemetryService(): Promise<TelemetryService> {
|
||||
async function getTelemetryService(): Promise<TelemetryService> {
|
||||
if (_telemetryServiceInstance) {
|
||||
return _telemetryServiceInstance
|
||||
}
|
||||
@@ -59,14 +39,6 @@ export async function getTelemetryService(): Promise<TelemetryService> {
|
||||
return _initializationPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the telemetry service instance (useful for testing)
|
||||
*/
|
||||
export function resetTelemetryService(): void {
|
||||
_telemetryServiceInstance = null
|
||||
_initializationPromise = null
|
||||
}
|
||||
|
||||
export const telemetryService = new Proxy({} as TelemetryService, {
|
||||
get(_target, prop, _receiver) {
|
||||
// Return a function that will call the method on the actual service
|
||||
|
||||
@@ -8,23 +8,23 @@ import type { ClineAccountUserInfo } from "../../auth/AuthService"
|
||||
/**
|
||||
* JSON-serializable primitive types for telemetry properties
|
||||
*/
|
||||
export type TelemetryPrimitive = string | number | boolean | null | undefined
|
||||
type TelemetryPrimitive = string | number | boolean | null | undefined
|
||||
|
||||
/**
|
||||
* JSON-serializable value types for telemetry properties
|
||||
* Ensures all telemetry data can be properly serialized
|
||||
*/
|
||||
export type TelemetryValue = TelemetryPrimitive | TelemetryObject | TelemetryArray
|
||||
type TelemetryValue = TelemetryPrimitive | TelemetryObject | TelemetryArray
|
||||
|
||||
/**
|
||||
* JSON-serializable object for telemetry properties
|
||||
*/
|
||||
export type TelemetryObject = { [key: string]: TelemetryValue }
|
||||
type TelemetryObject = { [key: string]: TelemetryValue }
|
||||
|
||||
/**
|
||||
* JSON-serializable array for telemetry properties
|
||||
*/
|
||||
export type TelemetryArray = Array<TelemetryValue>
|
||||
type TelemetryArray = Array<TelemetryValue>
|
||||
|
||||
/**
|
||||
* Properties that can be safely passed to telemetry providers
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export function createConsoleLogExporter(): ConsoleLogRecordExporter {
|
||||
return new ConsoleLogRecordExporter()
|
||||
}
|
||||
|
||||
export function ensurePathSuffix(url: URL, suffix: string): void {
|
||||
function ensurePathSuffix(url: URL, suffix: string): void {
|
||||
const pathname = url.pathname
|
||||
const normalizedPathname = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
||||
url.pathname = normalizedPathname
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function validateWorkspacePath(workspacePath: string): Promise<void
|
||||
* Cleans up any existing Git repository in the specified workspace path
|
||||
* @param workspacePath The workspace path to clean up
|
||||
*/
|
||||
export async function cleanupPreviousGit(workspacePath: string): Promise<void> {
|
||||
async function cleanupPreviousGit(workspacePath: string): Promise<void> {
|
||||
const gitDir = path.join(workspacePath, ".git")
|
||||
|
||||
try {
|
||||
|
||||
@@ -18,7 +18,7 @@ let isTestMode = false
|
||||
* Sets the test mode state
|
||||
* @param value Whether test mode is enabled
|
||||
*/
|
||||
export function setTestMode(value: boolean): void {
|
||||
function setTestMode(value: boolean): void {
|
||||
isTestMode = value
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function setTestMode(value: boolean): void {
|
||||
* Checks if the extension is running in test mode
|
||||
* @returns True if in test mode, false otherwise
|
||||
*/
|
||||
export function isInTestMode(): boolean {
|
||||
function isInTestMode(): boolean {
|
||||
return isTestMode
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface OrganizationUsageTransaction {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export interface UserRemoteConfigOrganization {
|
||||
interface UserRemoteConfigOrganization {
|
||||
organizationId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Banner message types for Cline extension
|
||||
*/
|
||||
|
||||
export type BannerSeverity = "info" | "success" | "warning"
|
||||
export type BannerPlacement = "top" | "bottom" | "welcome"
|
||||
type BannerSeverity = "info" | "success" | "warning"
|
||||
type BannerPlacement = "top" | "bottom" | "welcome"
|
||||
|
||||
export interface Banner {
|
||||
id: string
|
||||
@@ -37,7 +37,7 @@ export interface BannersResponse {
|
||||
/**
|
||||
* Audience targeting options
|
||||
*/
|
||||
export type BannerAudience = "all" | "team_admin_only" | "team_members" | "personal_only"
|
||||
type BannerAudience = "all" | "team_admin_only" | "team_members" | "personal_only"
|
||||
|
||||
/**
|
||||
* Rules that can be evaluated for banner targeting
|
||||
@@ -60,26 +60,3 @@ export interface BannerRules {
|
||||
/** Optional actions embedded in rules JSON for banners */
|
||||
actions?: BannerAction[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner event types for telemetry
|
||||
*/
|
||||
export type BannerEventType = "dismiss"
|
||||
|
||||
/**
|
||||
* Banner event payload sent to the telemetry API
|
||||
*/
|
||||
export interface BannerEventPayload {
|
||||
banner_id: string
|
||||
instance_id: string
|
||||
surface: "vscode" | "jetbrains" | "cli"
|
||||
event_type: BannerEventType
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks when a banner was dismissed by a user
|
||||
*/
|
||||
export interface BannerDismissal {
|
||||
bannerId: string
|
||||
dismissedAt: number // timestamp
|
||||
}
|
||||
|
||||
@@ -273,43 +273,8 @@ export interface ClineSayTool {
|
||||
readLineEnd?: number
|
||||
}
|
||||
|
||||
export interface ClineSayHook {
|
||||
hookName: string // Name of the hook (e.g., "PreToolUse", "PostToolUse")
|
||||
toolName?: string // Tool name if applicable (for PreToolUse/PostToolUse)
|
||||
status: "running" | "completed" | "failed" | "cancelled" // Execution status
|
||||
exitCode?: number // Exit code when completed
|
||||
hasJsonResponse?: boolean // Whether a JSON response was parsed
|
||||
// Pending tool information (only present during PreToolUse "running" status)
|
||||
pendingToolInfo?: {
|
||||
tool: string // Tool name (e.g., "write_to_file", "execute_command")
|
||||
path?: string // File path for file operations
|
||||
command?: string // Command for execute_command
|
||||
content?: string // Content preview (first 200 chars)
|
||||
diff?: string // Diff preview (first 200 chars)
|
||||
regex?: string // Regex pattern for search_files
|
||||
url?: string // URL for web_fetch or browser_action
|
||||
mcpTool?: string // MCP tool name
|
||||
mcpServer?: string // MCP server name
|
||||
resourceUri?: string // MCP resource URI
|
||||
}
|
||||
// Structured error information (only present when status is "failed")
|
||||
error?: {
|
||||
type: "timeout" | "validation" | "execution" | "cancellation" // Type of error
|
||||
message: string // User-friendly error message
|
||||
details?: string // Technical details for expansion
|
||||
scriptPath?: string // Path to the hook script
|
||||
}
|
||||
}
|
||||
|
||||
export type HookOutputStreamMeta = {
|
||||
/** Which hook configuration the script originated from (global vs workspace). */
|
||||
source: "global" | "workspace"
|
||||
/** Full path to the hook script that emitted the output. */
|
||||
scriptPath: string
|
||||
}
|
||||
|
||||
// must keep in sync with system prompt
|
||||
export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
|
||||
const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
|
||||
export type BrowserAction = (typeof browserActions)[number]
|
||||
|
||||
export interface ClineSayBrowserAction {
|
||||
@@ -382,10 +347,6 @@ export interface ClineAskQuestion {
|
||||
selected?: string
|
||||
}
|
||||
|
||||
export interface ClineAskNewTask {
|
||||
context: string
|
||||
}
|
||||
|
||||
export interface ClineApiReqInfo {
|
||||
request?: string
|
||||
tokensIn?: number
|
||||
@@ -412,6 +373,6 @@ export interface ClineSubagentUsageInfo {
|
||||
cost: number
|
||||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
|
||||
type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
|
||||
|
||||
@@ -40,7 +40,7 @@ export type LanguageDisplay =
|
||||
|
||||
export const DEFAULT_LANGUAGE_SETTINGS: LanguageKey = "en"
|
||||
|
||||
export const languageOptions: { key: LanguageKey; display: LanguageDisplay }[] = [
|
||||
const languageOptions: { key: LanguageKey; display: LanguageDisplay }[] = [
|
||||
{ key: "en", display: "English" },
|
||||
{ key: "ar", display: "Arabic - العربية" },
|
||||
{ key: "pt-BR", display: "Portuguese - Português (Brasil)" },
|
||||
|
||||
@@ -111,29 +111,12 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo {
|
||||
reasoningEffortOptions: string[]
|
||||
}
|
||||
|
||||
export interface HicapCompatibleModelInfo extends ModelInfo {
|
||||
temperature?: number
|
||||
}
|
||||
|
||||
export const hicapModelInfoSafeDefaults: HicapCompatibleModelInfo = {
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
temperature: 1,
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
|
||||
export type AnthropicModelId = string
|
||||
export const ANTHROPIC_MIN_THINKING_BUDGET = 1_024
|
||||
export const ANTHROPIC_MAX_THINKING_BUDGET = 6_000
|
||||
|
||||
// Claude Code
|
||||
export type ClaudeCodeModelId = string
|
||||
|
||||
// AWS Bedrock
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
|
||||
export type BedrockModelId = string
|
||||
@@ -154,18 +137,6 @@ export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
"Claude Sonnet 4.5 is an Anthropic model for coding, agentic search, and AI agent workflows. It supports planning and implementation tasks across the software development lifecycle.\n\nRead more in the [blog post here](https://www.anthropic.com/claude/sonnet)",
|
||||
}
|
||||
|
||||
// Cline custom model - Devstral
|
||||
export const clineDevstralModelInfo: ModelInfo = {
|
||||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
description: "A stealth model for agentic coding tasks",
|
||||
}
|
||||
|
||||
export type ClinePassModelId = keyof typeof clinePassModels
|
||||
export const clinePassDefaultModelId = "cline-pass/glm-5.1"
|
||||
export const clinePassModelInfoSaneDefaults: ModelInfo = {
|
||||
@@ -218,97 +189,6 @@ export function resolveClinePassModelInfo(modelId: string, modelInfoByName?: Rec
|
||||
)
|
||||
}
|
||||
|
||||
export const OPENROUTER_PROVIDER_PREFERENCES: Record<string, { order: string[]; allow_fallbacks: boolean }> = {
|
||||
// Exacto Providers
|
||||
"moonshotai/kimi-k2:exacto": {
|
||||
order: ["groq", "moonshotai"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"z-ai/glm-4.6:exacto": {
|
||||
order: ["z-ai", "novita"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"deepseek/deepseek-v3.1-terminus:exacto": {
|
||||
order: ["novita", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-coder:exacto": {
|
||||
order: ["baseten"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"openai/gpt-oss-120b:exacto": {
|
||||
order: ["groq", "novita"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
|
||||
// Normal Providers
|
||||
"moonshotai/kimi-k2": {
|
||||
order: ["groq", "fireworks", "baseten", "parasail", "novita", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-coder": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-235b-a22b-thinking-2507": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-235b-a22b-07-25": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-30b-a3b-thinking-2507": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-30b-a3b-instruct-2507": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-30b-a3b:free": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-next-80b-a3b-thinking": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-next-80b-a3b-instruct": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"qwen/qwen3-max": {
|
||||
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"deepseek/deepseek-v3.2-exp": {
|
||||
order: ["deepseek", "novita", "fireworks", "nebius"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"z-ai/glm-4.6": {
|
||||
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"z-ai/glm-4.5v": {
|
||||
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"z-ai/glm-4.5": {
|
||||
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
"z-ai/glm-4.5-air": {
|
||||
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
|
||||
allow_fallbacks: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Vertex AI
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models
|
||||
export type VertexModelId = string
|
||||
|
||||
export const openAiModelInfoSafeDefaults: OpenAiCompatibleModelInfo = {
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
@@ -320,36 +200,17 @@ export const openAiModelInfoSafeDefaults: OpenAiCompatibleModelInfo = {
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
// Gemini
|
||||
// https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
export type GeminiModelId = string
|
||||
|
||||
// OpenAI Native
|
||||
// https://openai.com/api/pricing/
|
||||
export type OpenAiNativeModelId = string
|
||||
|
||||
// OpenAI Codex (ChatGPT Plus/Pro subscription)
|
||||
// Uses OAuth authentication via ChatGPT, routes to chatgpt.com/backend-api/codex/responses
|
||||
// Subscription-based pricing (all costs are $0).
|
||||
//
|
||||
// The Codex catalog and default model id are sourced from the `@cline/llms`
|
||||
// SDK; the re-export here lets callers continue importing them from
|
||||
// `@shared/api` without duplicating model parameters in the extension.
|
||||
// Edit `openai-codex-models.ts` to change adapter behavior.
|
||||
export { getOpenAiCodexDefaultModelId, type OpenAiCodexModelId, openAiCodexModels } from "./openai-codex-models"
|
||||
|
||||
// SDK.
|
||||
// Azure OpenAI
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs
|
||||
export const azureOpenAiDefaultApiVersion = "2024-08-01-preview"
|
||||
|
||||
// DeepSeek model list and default are sourced from the SDK provider catalog;
|
||||
// see src/sdk/model-catalog/ and webview-ui/.../providers/DeepSeekProvider.tsx.
|
||||
|
||||
// Hugging Face Inference Providers
|
||||
// https://huggingface.co/docs/inference-providers/en/index
|
||||
export type HuggingFaceModelId = string
|
||||
|
||||
// Qwen
|
||||
// https://bailian.console.aliyun.com/
|
||||
// The first model in the list is used as the default model for each region
|
||||
@@ -358,64 +219,11 @@ export enum QwenApiRegions {
|
||||
CHINA = "china",
|
||||
INTERNATIONAL = "international",
|
||||
}
|
||||
export type MainlandQwenModelId = string
|
||||
export type InternationalQwenModelId = string
|
||||
// Doubao
|
||||
// https://www.volcengine.com/docs/82379/1298459
|
||||
// https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
|
||||
export type DoubaoModelId = string
|
||||
|
||||
// Mistral
|
||||
// https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
export type MistralModelId = string
|
||||
|
||||
// LiteLLM
|
||||
// https://docs.litellm.ai/docs/
|
||||
export type LiteLLMModelId = string
|
||||
export const liteLlmDefaultModelId = "anthropic/claude-3-7-sonnet-20250219"
|
||||
export interface LiteLLMModelInfo extends ModelInfo {
|
||||
temperature?: number
|
||||
}
|
||||
|
||||
export const liteLlmModelInfoSafeDefaults: LiteLLMModelInfo = {
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
// AskSage Models
|
||||
// https://docs.asksage.ai/
|
||||
export type AskSageModelId = string
|
||||
export const askSageDefaultURL: string = "https://api.asksage.ai/server"
|
||||
export type NebiusModelId = string
|
||||
|
||||
// W&B Inference by CoreWeave
|
||||
// https://docs.wandb.ai/inference/models
|
||||
export type WandbModelId = string
|
||||
|
||||
// X AI
|
||||
// https://docs.x.ai/docs/api-reference
|
||||
export type XAIModelId = string
|
||||
|
||||
// SambaNova
|
||||
// https://docs.sambanova.ai/cloud/docs/get-started/supported-models
|
||||
export type SambanovaModelId = string
|
||||
|
||||
// Cerebras
|
||||
// https://inference-docs.cerebras.ai/api-reference/models
|
||||
export type CerebrasModelId = string
|
||||
|
||||
// Groq
|
||||
// https://console.groq.com/docs/models
|
||||
// https://groq.com/pricing/
|
||||
export type GroqModelId = string
|
||||
|
||||
// Requesty
|
||||
// https://requesty.ai/models
|
||||
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
|
||||
@@ -431,48 +239,3 @@ export const requestyDefaultModelInfo: ModelInfo = {
|
||||
cacheReadsPrice: 0.3,
|
||||
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
|
||||
}
|
||||
|
||||
// SAP AI Core
|
||||
export type SapAiCoreModelId = string
|
||||
|
||||
// Moonshot AI Studio
|
||||
// https://platform.moonshot.ai/docs/pricing/chat
|
||||
export type MoonshotModelId = string
|
||||
|
||||
// Huawei Cloud MaaS
|
||||
// Dify.ai - No model selection needed, models are configured in Dify workflows
|
||||
|
||||
export type HuaweiCloudMaasModelId = string
|
||||
|
||||
// Baseten
|
||||
// https://baseten.co/products/model-apis/
|
||||
// Extended ModelInfo to include supportedFeatures, like tools
|
||||
export interface BasetenModelInfo extends ModelInfo {
|
||||
supportedFeatures?: string[]
|
||||
}
|
||||
|
||||
export type BasetenModelId = string
|
||||
|
||||
// Z AI
|
||||
// https://docs.z.ai/guides/llm/glm-5.1
|
||||
// https://docs.z.ai/guides/llm/glm-5
|
||||
// https://docs.z.ai/guides/overview/pricing
|
||||
export type internationalZAiModelId = string
|
||||
|
||||
export type mainlandZAiModelId = string
|
||||
|
||||
// Fireworks AI
|
||||
export type FireworksModelId = string
|
||||
|
||||
// Qwen Code
|
||||
// https://chat.qwen.ai/
|
||||
export type QwenCodeModelId = string
|
||||
|
||||
// Minimax
|
||||
// https://www.minimax.io/platform/document/text_api_intro
|
||||
// https://www.minimax.io/platform/document/pricing
|
||||
export type MinimaxModelId = string
|
||||
|
||||
// NousResearch
|
||||
// https://inference-api.nousResearch.com
|
||||
export type NousResearchModelId = string
|
||||
|
||||
@@ -20,37 +20,3 @@ export function findLast<T>(array: Array<T>, predicate: (value: T, index: number
|
||||
const index = findLastIndex(array, predicate)
|
||||
return index === -1 ? undefined : array[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a partial or complete stringified array into an actual array.
|
||||
* Handles both complete JSON strings and incomplete array strings.
|
||||
* Splits on the specific tokens: [" ", " "]
|
||||
* @param arrayString A string representation of an array, which may be incomplete
|
||||
* @returns Array of strings parsed from the input
|
||||
*/
|
||||
export function parsePartialArrayString(arrayString: string): string[] {
|
||||
try {
|
||||
// Try parsing as complete JSON first
|
||||
return JSON.parse(arrayString)
|
||||
} catch {
|
||||
// If JSON parsing fails, handle as partial string
|
||||
const trimmed = arrayString.trim()
|
||||
if (!trimmed.startsWith('["')) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Remove leading ["
|
||||
let content = trimmed.slice(2)
|
||||
// Remove trailing "] if it exists
|
||||
content = content.replace(/"]$/, "")
|
||||
if (!content) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Split on ", " token and handle the parts
|
||||
return content
|
||||
.split('", "')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ type URLType = "router" | "app" | "api"
|
||||
const replaceCname = (baseUrl: string, type: URLType): string => {
|
||||
if (type === "router") {
|
||||
return baseUrl
|
||||
} else {
|
||||
return baseUrl.replace("router", type).replace("v1", "")
|
||||
}
|
||||
return baseUrl.replace("router", type).replace("v1", "")
|
||||
}
|
||||
|
||||
export const toRequestyServiceUrl = (baseUrl?: string, service: URLType = "router"): URL | undefined => {
|
||||
@@ -19,7 +18,3 @@ export const toRequestyServiceUrl = (baseUrl?: string, service: URLType = "route
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const toRequestyServiceStringUrl = (baseUrl?: string, service: URLType = "router"): string | undefined => {
|
||||
return toRequestyServiceUrl(baseUrl, service)?.toString()
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export interface ClineExtensionContext {
|
||||
readonly extension: Extension<any>
|
||||
}
|
||||
|
||||
export enum ExtensionKind {
|
||||
enum ExtensionKind {
|
||||
/**
|
||||
* Extension runs where the UI runs.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,4 @@ export * from "./onboarding"
|
||||
|
||||
export enum ClineClient {
|
||||
VSCode = "VSCode Extension",
|
||||
JetBrains = "JetBrains",
|
||||
Cli = "CLI",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ClineRecommendedModel {
|
||||
interface ClineRecommendedModel {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
|
||||
@@ -371,4 +371,4 @@ export function combineHookSequences(messages: ClineMessage[]): ClineMessage[] {
|
||||
return reordered
|
||||
}
|
||||
|
||||
export const HOOK_OUTPUT_STRING = "__HOOK_OUTPUT__"
|
||||
const HOOK_OUTPUT_STRING = "__HOOK_OUTPUT__"
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
/** Maximum content size in bytes (400KB) */
|
||||
export const MAX_CONTENT_SIZE_BYTES = 400 * 1024
|
||||
const MAX_CONTENT_SIZE_BYTES = 400 * 1024
|
||||
|
||||
/**
|
||||
* Format bytes into a human-readable string (e.g., "1.5 MB", "400 KB").
|
||||
*/
|
||||
export function formatBytes(bytes: number): string {
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`
|
||||
}
|
||||
|
||||
@@ -11,33 +11,3 @@
|
||||
export function isFocusChainItem(line: string): boolean {
|
||||
return line.startsWith("- [ ]") || line.startsWith("- [x]") || line.startsWith("- [X]")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a trimmed line is a completed focus chain item
|
||||
* @param line The trimmed line to check
|
||||
* @returns true if the line is a completed focus chain item (- [x] or - [X])
|
||||
*/
|
||||
export function isCompletedFocusChainItem(line: string): boolean {
|
||||
return line.startsWith("- [x]") || line.startsWith("- [X]")
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexible regex pattern for matching focus chain items with spacing variations
|
||||
* Matches patterns like "- [x] text", "- [X] text", "- [ ] text", "- [ ] text", etc.
|
||||
*/
|
||||
export const FOCUS_CHAIN_ITEM_REGEX = /^-\s*\[([ xX])\]\s*(.+)$/
|
||||
|
||||
/**
|
||||
* Parse focus chain item using flexible regex (allows spacing variations)
|
||||
* @param line The trimmed line to parse
|
||||
* @returns Object with checked status and text, or null if not a focus chain item
|
||||
*/
|
||||
export function parseFocusChainItem(line: string): { checked: boolean; text: string } | null {
|
||||
const match = line.match(FOCUS_CHAIN_ITEM_REGEX)
|
||||
if (match) {
|
||||
const checked = match[1] === "x" || match[1] === "X"
|
||||
const text = match[2].trim()
|
||||
return { checked, text }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const CLINE_TRUSTED_TESTER_DOMAINS = ["fibilabs.tech"]
|
||||
* Checks if the given email belongs to a Cline bot user.
|
||||
* E.g. Emails ending with @cline.bot
|
||||
*/
|
||||
export function isClineBotUser(email: string): boolean {
|
||||
function isClineBotUser(email: string): boolean {
|
||||
return email.endsWith("@cline.bot")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
* where each tool name is the combination of the server name + identifier + tool name.
|
||||
* This enables to uniquely identify which MCP server a tool belongs to.
|
||||
*/
|
||||
export const CLINE_MCP_TOOL_IDENTIFIER = "0mcp0"
|
||||
export const DEFAULT_MCP_TIMEOUT_SECONDS = 60 // matches Anthropic's default timeout in their MCP SDK
|
||||
export const MIN_MCP_TIMEOUT_SECONDS = 1
|
||||
export type McpMode = "full" | "server-use-only" | "off"
|
||||
|
||||
export type McpServer = {
|
||||
name: string
|
||||
@@ -60,7 +58,7 @@ export type McpPrompt = {
|
||||
arguments?: McpPromptArgument[]
|
||||
}
|
||||
|
||||
export type McpPromptMessageContent =
|
||||
type McpPromptMessageContent =
|
||||
| {
|
||||
type: "text"
|
||||
text: string
|
||||
@@ -85,7 +83,7 @@ export type McpPromptMessageContent =
|
||||
}
|
||||
}
|
||||
|
||||
export type McpPromptMessage = {
|
||||
type McpPromptMessage = {
|
||||
role: "user" | "assistant"
|
||||
content: McpPromptMessageContent
|
||||
}
|
||||
@@ -167,15 +165,4 @@ export interface McpMarketplaceCatalog {
|
||||
items: McpMarketplaceItem[]
|
||||
}
|
||||
|
||||
export interface McpDownloadResponse {
|
||||
mcpId: string
|
||||
githubUrl: string
|
||||
name: string
|
||||
author: string
|
||||
description: string
|
||||
readmeContent: string
|
||||
llmsInstallationContent: string
|
||||
requiresApiKey: boolean
|
||||
}
|
||||
|
||||
export type McpViewTab = "marketplace" | "addRemote" | "configure"
|
||||
|
||||
@@ -25,42 +25,30 @@ export const REASONING_DETAILS_PROVIDERS = ["cline", "openrouter"];
|
||||
* This ensures backward compatibility where the messages were stored in Anthropic format with additional
|
||||
* fields unknown to Anthropic SDK.
|
||||
*/
|
||||
export interface ClineTextContentBlock
|
||||
extends Anthropic.TextBlockParam,
|
||||
ClineSharedMessageParam {
|
||||
export interface ClineTextContentBlock extends Anthropic.TextBlockParam, ClineSharedMessageParam {
|
||||
// reasoning_details only exists for providers listed in REASONING_DETAILS_PROVIDERS
|
||||
reasoning_details?: ClineReasoningDetailParam[];
|
||||
// Thought Signature associates with Gemini
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface ClineImageContentBlock
|
||||
extends Anthropic.ImageBlockParam,
|
||||
ClineSharedMessageParam {}
|
||||
export interface ClineImageContentBlock extends Anthropic.ImageBlockParam, ClineSharedMessageParam {}
|
||||
|
||||
export interface ClineDocumentContentBlock
|
||||
extends Anthropic.DocumentBlockParam,
|
||||
ClineSharedMessageParam {}
|
||||
export interface ClineDocumentContentBlock extends Anthropic.DocumentBlockParam, ClineSharedMessageParam {}
|
||||
|
||||
export interface ClineUserToolResultContentBlock
|
||||
extends Anthropic.ToolResultBlockParam,
|
||||
ClineSharedMessageParam {}
|
||||
export interface ClineUserToolResultContentBlock extends Anthropic.ToolResultBlockParam, ClineSharedMessageParam {}
|
||||
|
||||
/**
|
||||
* Assistant only content types
|
||||
*/
|
||||
export interface ClineAssistantToolUseBlock
|
||||
extends Anthropic.ToolUseBlockParam,
|
||||
ClineSharedMessageParam {
|
||||
export interface ClineAssistantToolUseBlock extends Anthropic.ToolUseBlockParam, ClineSharedMessageParam {
|
||||
// reasoning_details only exists for providers listed in REASONING_DETAILS_PROVIDERS
|
||||
reasoning_details?: unknown[] | ClineReasoningDetailParam[];
|
||||
// Thought Signature associates with Gemini
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface ClineAssistantThinkingBlock
|
||||
extends Anthropic.ThinkingBlock,
|
||||
ClineSharedMessageParam {
|
||||
export interface ClineAssistantThinkingBlock extends Anthropic.ThinkingBlock, ClineSharedMessageParam {
|
||||
// The summary items returned by OpenAI response API
|
||||
// The reasoning details that will be moved to the text block when finalized
|
||||
summary?: unknown[] | ClineReasoningDetailParam[];
|
||||
@@ -136,9 +124,7 @@ export function convertClineStorageToAnthropicMessage(
|
||||
}
|
||||
|
||||
// Removes thinking block that has no signature (invalid thinking block that's incompatible with Anthropic API)
|
||||
const filteredContent = content.filter(
|
||||
(b) => b.type !== "thinking" || !!b.signature,
|
||||
);
|
||||
const filteredContent = content.filter((b) => b.type !== "thinking" || !!b.signature);
|
||||
|
||||
// Handle array content - strip Cline-specific fields for non-reasoning_details providers
|
||||
const shouldCleanContent = !REASONING_DETAILS_PROVIDERS.includes(provider);
|
||||
@@ -155,9 +141,7 @@ export function convertClineStorageToAnthropicMessage(
|
||||
* narrows to the base64 variant for the transform layer. URL sources are not produced by Cline,
|
||||
* so they degrade to empty values rather than throwing.
|
||||
*/
|
||||
export function getBase64ImageSource(
|
||||
source: Anthropic.ImageBlockParam["source"],
|
||||
): { mediaType: string; data: string } {
|
||||
export function getBase64ImageSource(source: Anthropic.ImageBlockParam["source"]): { mediaType: string; data: string } {
|
||||
if (source.type === "base64") {
|
||||
return { mediaType: source.media_type, data: source.data };
|
||||
}
|
||||
@@ -167,9 +151,7 @@ export function getBase64ImageSource(
|
||||
/**
|
||||
* Builds a base64 data URL from an image block's source. See getBase64ImageSource.
|
||||
*/
|
||||
export function getImageDataUrl(
|
||||
source: Anthropic.ImageBlockParam["source"],
|
||||
): string {
|
||||
export function getImageDataUrl(source: Anthropic.ImageBlockParam["source"]): string {
|
||||
const { mediaType, data } = getBase64ImageSource(source);
|
||||
return `data:${mediaType};base64,${data}`;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
// Core content types
|
||||
export type {
|
||||
ClineAssistantContent,
|
||||
ClineAssistantRedactedThinkingBlock,
|
||||
ClineAssistantThinkingBlock,
|
||||
ClineAssistantToolUseBlock,
|
||||
ClineContent,
|
||||
ClineDocumentContentBlock,
|
||||
ClineImageContentBlock,
|
||||
ClineMessageRole,
|
||||
ClinePromptInputContent,
|
||||
ClineReasoningDetailParam,
|
||||
ClineStorageMessage,
|
||||
ClineTextContentBlock,
|
||||
ClineToolResponseContent,
|
||||
ClineUserContent,
|
||||
ClineUserToolResultContentBlock,
|
||||
} from "./content"
|
||||
export { cleanContentBlock, convertClineStorageToAnthropicMessage, REASONING_DETAILS_PROVIDERS } from "./content"
|
||||
export type { ClineMessageMetricsInfo, ClineMessageModelInfo } from "./metrics"
|
||||
export type { ClineToolResponseContent } from "./content"
|
||||
export type { ClineMessageModelInfo } from "./metrics"
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* OpenAI Codex (ChatGPT Plus/Pro subscription) model catalog.
|
||||
*
|
||||
* Single source of truth: the `@cline/llms` SDK. The SDK's
|
||||
* {@link filterOpenAICodexModels} produces the canonical Codex allowlist by
|
||||
* filtering the generated `openai-native` catalog, and the SDK's builtin
|
||||
* provider registry exposes the canonical default model id.
|
||||
*
|
||||
* The SDK reports real per-token pricing for these models. The Codex
|
||||
* subscription bills per ChatGPT Plus/Pro plan rather than per token, so
|
||||
* `OpenAiCodexProvider.tsx` passes `hideUsageCost` to {@link ModelInfoView}
|
||||
* to suppress the price strip in the settings UI. The runtime handler
|
||||
* separately hard-codes `totalCost: 0` in
|
||||
* {@link OpenAiCodexHandler.normalizeUsage}, so users never see a charge
|
||||
* either way; the SDK pricing is just informational metadata.
|
||||
*/
|
||||
|
||||
import { getProviderCollectionSync } from "@cline/llms"
|
||||
// Relative path: this module is imported by both the extension and
|
||||
// webview-ui builds, which carry different `@/*` path mappings. A
|
||||
// relative import resolves the same way in both contexts.
|
||||
import { adaptSdkModelInfo } from "../sdk/model-catalog/shape-adapter"
|
||||
import type { ModelInfo } from "./api"
|
||||
|
||||
const CODEX_PROVIDER_ID = "openai-codex"
|
||||
|
||||
function buildCodexCatalog(): {
|
||||
models: Record<string, ModelInfo>
|
||||
defaultModelId: string
|
||||
} {
|
||||
const collection = getProviderCollectionSync(CODEX_PROVIDER_ID)
|
||||
if (!collection) {
|
||||
// SDK has no Codex provider registered. In production the SDK
|
||||
// always carries it; this branch is reachable today only under
|
||||
// the test stubs in `apps/vscode/src/test/requires.ts` and
|
||||
// `apps/vscode/test-setup.js`, which return undefined. Surface
|
||||
// an empty catalog so callers see "nothing to offer" rather
|
||||
// than a fabricated value.
|
||||
return { models: {}, defaultModelId: "" }
|
||||
}
|
||||
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
for (const info of Object.values(collection.models)) {
|
||||
models[info.id] = adaptSdkModelInfo(info)
|
||||
}
|
||||
|
||||
return { models, defaultModelId: collection.provider.defaultModelId }
|
||||
}
|
||||
|
||||
// Memoized so the SDK catalog is built at most once, on first access, rather
|
||||
// than at module-evaluation time. Building eagerly here would run during the
|
||||
// `@shared/api` ↔ `shape-adapter` import cycle and touch `shape-adapter`
|
||||
// module constants before they finish initializing (a temporal-dead-zone
|
||||
// crash).
|
||||
let codexCatalog: { models: Record<string, ModelInfo>; defaultModelId: string } | undefined
|
||||
|
||||
function getCodexCatalog(): { models: Record<string, ModelInfo>; defaultModelId: string } {
|
||||
if (!codexCatalog) {
|
||||
codexCatalog = buildCodexCatalog()
|
||||
}
|
||||
return codexCatalog
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical OpenAI Codex model catalog, derived from the `@cline/llms` SDK.
|
||||
*
|
||||
* Keys are SDK model ids (e.g. `"gpt-5.4"`, `"gpt-5.3-codex"`). The set of
|
||||
* keys is determined by {@link filterOpenAICodexModels} in the SDK.
|
||||
*
|
||||
* Lazily built on first access (see {@link getCodexCatalog}).
|
||||
*/
|
||||
export const openAiCodexModels: Record<string, ModelInfo> = new Proxy({} as Record<string, ModelInfo>, {
|
||||
get: (_target, prop: string) => getCodexCatalog().models[prop],
|
||||
has: (_target, prop: string) => prop in getCodexCatalog().models,
|
||||
ownKeys: () => Reflect.ownKeys(getCodexCatalog().models),
|
||||
getOwnPropertyDescriptor: (_target, prop: string) => Object.getOwnPropertyDescriptor(getCodexCatalog().models, prop),
|
||||
})
|
||||
|
||||
/**
|
||||
* Default Codex model id, sourced from the SDK's builtin provider metadata.
|
||||
* Lazily resolved on first access.
|
||||
*/
|
||||
export function getOpenAiCodexDefaultModelId(): string {
|
||||
return getCodexCatalog().defaultModelId
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Codex model id. Plain `string` because the SDK provides Codex
|
||||
* model ids as opaque strings. Use {@link openAiCodexModels} for
|
||||
* membership checks.
|
||||
*/
|
||||
export type OpenAiCodexModelId = string
|
||||
@@ -13,17 +13,3 @@ export function convertSearchResultsToProtoFileInfos(
|
||||
workspaceName: result.workspaceName,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts proto FileInfo objects to domain search result objects
|
||||
*/
|
||||
export function convertProtoFileInfosToSearchResults(
|
||||
protoResults: FileInfo[],
|
||||
): { path: string; type: "file" | "folder"; label?: string; workspaceName?: string }[] {
|
||||
return protoResults.map((protoResult) => ({
|
||||
path: protoResult.path,
|
||||
type: protoResult.type as "file" | "folder",
|
||||
label: protoResult.label,
|
||||
workspaceName: protoResult.workspaceName,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
import { TelemetrySettingEnum } from "@shared/proto/cline/state"
|
||||
import { TelemetrySetting } from "../../TelemetrySetting"
|
||||
|
||||
/**
|
||||
* Converts a domain TelemetrySetting string to a proto TelemetrySettingEnum
|
||||
*/
|
||||
export function convertDomainTelemetrySettingToProto(setting: TelemetrySetting): TelemetrySettingEnum {
|
||||
switch (setting) {
|
||||
case "unset":
|
||||
return TelemetrySettingEnum.UNSET
|
||||
case "enabled":
|
||||
return TelemetrySettingEnum.ENABLED
|
||||
case "disabled":
|
||||
return TelemetrySettingEnum.DISABLED
|
||||
default:
|
||||
return TelemetrySettingEnum.UNSET
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a proto TelemetrySettingEnum to a domain TelemetrySetting string
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
// OpenAI Compatible model schema with per-model settings
|
||||
export const OpenAiCompatibleModelSchema = z.object({
|
||||
const OpenAiCompatibleModelSchema = z.object({
|
||||
id: z.string(), // The model ID is required
|
||||
temperature: z.number().optional(),
|
||||
isR1FormatRequired: z.boolean().optional(),
|
||||
@@ -37,13 +37,13 @@ export const OpenAiCompatibleSchema = z.object({
|
||||
})
|
||||
|
||||
// AWS Bedrock model schema with per-model settings
|
||||
export const AwsBedrockModelSchema = z.object({
|
||||
const AwsBedrockModelSchema = z.object({
|
||||
id: z.string(), // The model ID is required
|
||||
thinkingBudgetTokens: z.number().optional(),
|
||||
})
|
||||
|
||||
// AWS Bedrock custom model schema (separate from regular models)
|
||||
export const AwsBedrockCustomModelSchema = z.object({
|
||||
const AwsBedrockCustomModelSchema = z.object({
|
||||
name: z.string(), // The model name is required
|
||||
baseModelId: z.string(), // The base model ID is required
|
||||
thinkingBudgetTokens: z.number().optional(),
|
||||
@@ -64,7 +64,7 @@ export const AwsBedrockSettingsSchema = z.object({
|
||||
})
|
||||
|
||||
// Cline Provider model schema with per-model settings
|
||||
export const ClineModelSchema = z.object({
|
||||
const ClineModelSchema = z.object({
|
||||
id: z.string(), // The model ID is required
|
||||
})
|
||||
|
||||
@@ -75,36 +75,36 @@ export const ClineSettingsSchema = z.object({
|
||||
})
|
||||
|
||||
// Vertex Provider model schema with per-model settings
|
||||
export const VertexModelSchema = z.object({
|
||||
const VertexModelSchema = z.object({
|
||||
id: z.string(), // The model ID is required
|
||||
thinkingBudgetTokens: z.number().optional(),
|
||||
})
|
||||
|
||||
// GCP Vertex Provider specific settings
|
||||
export const VertexSettingsSchema = z.object({
|
||||
const VertexSettingsSchema = z.object({
|
||||
// A list of the allowed models with their settings
|
||||
models: z.array(VertexModelSchema).optional(),
|
||||
vertexProjectId: z.string().optional(),
|
||||
vertexRegion: z.string().optional(),
|
||||
})
|
||||
|
||||
export const LiteLLMModelSchema = z.object({
|
||||
const LiteLLMModelSchema = z.object({
|
||||
id: z.string(),
|
||||
thinkingBudgetTokens: z.number().optional(),
|
||||
promptCachingEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const LiteLLMSchema = z.object({
|
||||
const LiteLLMSchema = z.object({
|
||||
models: z.array(LiteLLMModelSchema).optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
export const AnthropicModelSchema = z.object({
|
||||
const AnthropicModelSchema = z.object({
|
||||
id: z.string(),
|
||||
thinkingBudgetTokens: z.number().optional(),
|
||||
})
|
||||
|
||||
export const AnthropicSchema = z.object({
|
||||
const AnthropicSchema = z.object({
|
||||
models: z.array(AnthropicModelSchema).optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
})
|
||||
@@ -120,12 +120,12 @@ const ProviderSettingsSchema = z.object({
|
||||
Anthropic: AnthropicSchema.optional(),
|
||||
})
|
||||
|
||||
export const AllowedMCPServerSchema = z.object({
|
||||
const AllowedMCPServerSchema = z.object({
|
||||
// The ID of the MCP is the URL for their github repo.
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const RemoteMCPServerSchema = z.object({
|
||||
const RemoteMCPServerSchema = z.object({
|
||||
// The name of the MCP server
|
||||
name: z.string(),
|
||||
// The URL of the MCP server
|
||||
@@ -137,7 +137,7 @@ export const RemoteMCPServerSchema = z.object({
|
||||
})
|
||||
|
||||
// Settings for a global cline rules or workflow file.
|
||||
export const GlobalInstructionsFileSchema = z.object({
|
||||
const GlobalInstructionsFileSchema = z.object({
|
||||
// When this is enabled, the user cannot turn off this rule or workflow.
|
||||
alwaysEnabled: z.boolean(),
|
||||
// The name of the rules or workflow file.
|
||||
@@ -232,30 +232,30 @@ export const APIKeySchema = z.record(z.string(), z.string())
|
||||
|
||||
// Type inference from schemas
|
||||
export type RemoteConfig = z.infer<typeof RemoteConfigSchema>
|
||||
export type MCPServer = z.infer<typeof AllowedMCPServerSchema>
|
||||
type MCPServer = z.infer<typeof AllowedMCPServerSchema>
|
||||
export type RemoteMCPServer = z.infer<typeof RemoteMCPServerSchema>
|
||||
export type GlobalInstructionsFile = z.infer<typeof GlobalInstructionsFileSchema>
|
||||
|
||||
export type ProviderSettings = z.infer<typeof ProviderSettingsSchema>
|
||||
type ProviderSettings = z.infer<typeof ProviderSettingsSchema>
|
||||
|
||||
export type OpenAiCompatible = z.infer<typeof OpenAiCompatibleSchema>
|
||||
export type OpenAiCompatibleModel = z.infer<typeof OpenAiCompatibleModelSchema>
|
||||
type OpenAiCompatible = z.infer<typeof OpenAiCompatibleSchema>
|
||||
type OpenAiCompatibleModel = z.infer<typeof OpenAiCompatibleModelSchema>
|
||||
|
||||
export type AwsBedrockSettings = z.infer<typeof AwsBedrockSettingsSchema>
|
||||
export type AwsBedrockModel = z.infer<typeof AwsBedrockModelSchema>
|
||||
export type AwsBedrockCustomModel = z.infer<typeof AwsBedrockCustomModelSchema>
|
||||
type AwsBedrockSettings = z.infer<typeof AwsBedrockSettingsSchema>
|
||||
type AwsBedrockModel = z.infer<typeof AwsBedrockModelSchema>
|
||||
type AwsBedrockCustomModel = z.infer<typeof AwsBedrockCustomModelSchema>
|
||||
|
||||
export type VertexSettings = z.infer<typeof VertexSettingsSchema>
|
||||
export type VertexModel = z.infer<typeof VertexModelSchema>
|
||||
type VertexSettings = z.infer<typeof VertexSettingsSchema>
|
||||
type VertexModel = z.infer<typeof VertexModelSchema>
|
||||
|
||||
export type LiteLLMSettings = z.infer<typeof LiteLLMSchema>
|
||||
export type LiteLLMModel = z.infer<typeof LiteLLMModelSchema>
|
||||
type LiteLLMSettings = z.infer<typeof LiteLLMSchema>
|
||||
type LiteLLMModel = z.infer<typeof LiteLLMModelSchema>
|
||||
|
||||
export type AnthropicSettings = z.infer<typeof AnthropicSchema>
|
||||
export type AnthropicModel = z.infer<typeof AnthropicModelSchema>
|
||||
type AnthropicSettings = z.infer<typeof AnthropicSchema>
|
||||
type AnthropicModel = z.infer<typeof AnthropicModelSchema>
|
||||
|
||||
export type APIKeySettings = z.infer<typeof APIKeySchema>
|
||||
|
||||
export type EnterpriseTelemetry = z.infer<typeof EnterpriseTelemetrySchema>
|
||||
export type PromptUploading = z.infer<typeof PromptUploadingSchema>
|
||||
type EnterpriseTelemetry = z.infer<typeof EnterpriseTelemetrySchema>
|
||||
type PromptUploading = z.infer<typeof PromptUploadingSchema>
|
||||
export type S3AccessKeySettings = z.infer<typeof S3AccessKeySettingsSchema>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getTaskTimestamp } from "./utils"
|
||||
/**
|
||||
* Result of a backfill operation for a single task.
|
||||
*/
|
||||
export interface BackfillTaskResult {
|
||||
interface BackfillTaskResult {
|
||||
taskId: string
|
||||
success: boolean
|
||||
filesQueued: string[]
|
||||
@@ -56,7 +56,7 @@ async function listTaskIds(_before?: string, _after?: string): Promise<string[]>
|
||||
*
|
||||
* @param taskId Task identifier
|
||||
*/
|
||||
export async function backfillTask(taskId: string): Promise<BackfillTaskResult> {
|
||||
async function backfillTask(taskId: string): Promise<BackfillTaskResult> {
|
||||
const result: BackfillTaskResult = {
|
||||
taskId,
|
||||
success: false,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SEVEN_DAYS_MS } from "./worker"
|
||||
/**
|
||||
* Sync queue item status.
|
||||
*/
|
||||
export type SyncQueueStatus = "pending" | "synced" | "failed"
|
||||
type SyncQueueStatus = "pending" | "synced" | "failed"
|
||||
|
||||
/**
|
||||
* Represents an item in the sync queue.
|
||||
@@ -209,7 +209,7 @@ export class SyncQueue {
|
||||
* @param key File key
|
||||
* @param remove Whether to remove the item after marking synced (default: false)
|
||||
*/
|
||||
markSynced(taskId: string, key: string, remove: boolean = false): void {
|
||||
markSynced(taskId: string, key: string, remove = false): void {
|
||||
const id = `${taskId}/${key}`
|
||||
if (remove) {
|
||||
delete this.data.items[id]
|
||||
@@ -322,7 +322,7 @@ export class SyncQueue {
|
||||
* @param maxAgeMs Maximum age for failed items in milliseconds (default: 7 days)
|
||||
* @returns Number of items cleaned up
|
||||
*/
|
||||
cleanupFailedItems(maxRetries: number = 5, maxAgeMs: number = SEVEN_DAYS_MS): number {
|
||||
cleanupFailedItems(maxRetries = 5, maxAgeMs: number = SEVEN_DAYS_MS): number {
|
||||
const cutoff = Date.now() - maxAgeMs
|
||||
let count = 0
|
||||
Object.entries(this.data.items).forEach(([key, item]) => {
|
||||
@@ -344,7 +344,7 @@ export class SyncQueue {
|
||||
* @param maxSize Maximum number of items to keep (default: 1000)
|
||||
* @returns Number of items evicted
|
||||
*/
|
||||
enforceMaxSize(maxSize: number = 1000): number {
|
||||
enforceMaxSize(maxSize = 1000): number {
|
||||
const items = Object.entries(this.data.items)
|
||||
if (items.length <= maxSize) {
|
||||
return 0
|
||||
|
||||
@@ -18,12 +18,7 @@ import { SyncQueue } from "./queue"
|
||||
import type { SyncWorkerOptions } from "./worker"
|
||||
import { disposeSyncWorker, initSyncWorker, SyncWorker } from "./worker"
|
||||
|
||||
export type { SyncQueueItem, SyncQueueStatus } from "./queue"
|
||||
// Re-export types and functions
|
||||
export { SyncQueue } from "./queue"
|
||||
export type { SyncWorkerEvent, SyncWorkerOptions } from "./worker"
|
||||
export { disposeSyncWorker, SyncWorker } from "./worker"
|
||||
|
||||
let syncQueueInstance: SyncQueue | null = null
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,55 +40,3 @@ export const BASE_SLASH_COMMANDS: SlashCommand[] = [
|
||||
|
||||
// VS Code-only slash commands
|
||||
export const VSCODE_ONLY_COMMANDS: SlashCommand[] = []
|
||||
|
||||
// CLI-only slash commands (handled locally, not sent to backend)
|
||||
export const CLI_ONLY_COMMANDS: SlashCommand[] = [
|
||||
{
|
||||
name: "help",
|
||||
description: "Learn how to use Cline CLI",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
description: "Change API provider, auto-approve, and feature settings",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "models",
|
||||
description: "Change the model used for the current mode",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "history",
|
||||
description: "Browse and search task history",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "clear",
|
||||
description: "Clear the current task and start fresh",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "exit",
|
||||
description: "Alternative to Ctrl+C",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "q",
|
||||
description: "Alternative to Ctrl+C",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
{
|
||||
name: "skills",
|
||||
description: "View and manage installed skills",
|
||||
section: "default",
|
||||
cliCompatible: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@ export type { BlobStoreSettings } from "./types"
|
||||
* S3/R2/Azure blob storage implementation of ClineStorage.
|
||||
* Uses AWS S3, Cloudflare R2, or Azure Blob Storage as the backend storage.
|
||||
*/
|
||||
export class ClineBlobStorage extends ClineStorage {
|
||||
class ClineBlobStorage extends ClineStorage {
|
||||
override name = "ClineBlobStorage"
|
||||
|
||||
private static store: ClineBlobStorage | null = null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Logger } from "../services/Logger"
|
||||
|
||||
export interface ClineStorageChangeEvent {
|
||||
interface ClineStorageChangeEvent {
|
||||
readonly key: string
|
||||
}
|
||||
|
||||
@@ -210,25 +210,3 @@ export abstract class ClineSyncStorage<T = any> {
|
||||
protected abstract _delete(key: string): void
|
||||
protected abstract _keys(): readonly string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple in-memory implementation of ClineStorage using a Map.
|
||||
*/
|
||||
export class ClineInMemoryStorage extends ClineStorage {
|
||||
/**
|
||||
* A simple in-memory cache to store key-value pairs.
|
||||
*/
|
||||
private readonly _cache = new Map<string, string>()
|
||||
|
||||
protected async _get(key: string): Promise<string | undefined> {
|
||||
return this._cache.get(key)
|
||||
}
|
||||
|
||||
protected async _store(key: string, value: string): Promise<void> {
|
||||
this._cache.set(key, value)
|
||||
}
|
||||
|
||||
protected async _delete(key: string): Promise<void> {
|
||||
this._cache.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user