mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0a9510c1c | |||
| d03957f21d |
+14
-20
@@ -6,12 +6,15 @@ import * as esbuild from "esbuild"
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const production = process.argv.includes("--production") || process.env["IS_DEBUG_BUILD"] === "false"
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const standalone = process.argv.includes("--standalone")
|
||||
const e2eBuild = process.argv.includes("--e2e-build")
|
||||
const destDir = standalone ? "dist-standalone" : "dist"
|
||||
|
||||
// Read package.json to get version for build-time injection
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
@@ -123,29 +126,15 @@ const copyWasmFiles = {
|
||||
},
|
||||
}
|
||||
|
||||
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
|
||||
if (production) {
|
||||
// IS_DEV is always disable in production builds.
|
||||
buildEnvVars["process.env.IS_DEV"] = "false"
|
||||
}
|
||||
// Set the environment and telemetry env vars. The API key env vars need to be populated in the GitHub
|
||||
// workflows from the secrets.
|
||||
if (process.env.CLINE_ENVIRONMENT) {
|
||||
buildEnvVars["process.env.CLINE_ENVIRONMENT"] = JSON.stringify(process.env.CLINE_ENVIRONMENT)
|
||||
}
|
||||
if (process.env.TELEMETRY_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.TELEMETRY_SERVICE_API_KEY"] = JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY)
|
||||
}
|
||||
if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
|
||||
}
|
||||
// Base configuration shared between extension and standalone builds
|
||||
const baseConfig = {
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: buildEnvVars,
|
||||
define: production
|
||||
? { "import.meta.url": "_importMetaUrl", "process.env.IS_DEV": JSON.stringify(!production) }
|
||||
: { "import.meta.url": "_importMetaUrl" },
|
||||
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
@@ -174,9 +163,14 @@ const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/cline-core.ts"],
|
||||
outfile: `${destDir}/cline-core.js`,
|
||||
// These modules need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled. better-sqlite3 is a native module that also cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3"],
|
||||
// Inject version at build time for standalone builds
|
||||
define: {
|
||||
...baseConfig.define,
|
||||
"process.env.CLINE_VERSION": JSON.stringify(packageJson.version),
|
||||
},
|
||||
}
|
||||
|
||||
// E2E build script configuration
|
||||
|
||||
Generated
+30
-22
@@ -36,6 +36,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
@@ -6279,6 +6280,20 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.4.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
"license": "Unlicense",
|
||||
@@ -6326,6 +6341,15 @@
|
||||
"url": "https://bevry.me/fund"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.4.7",
|
||||
"license": "MIT"
|
||||
@@ -7278,7 +7302,6 @@
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
@@ -7292,7 +7315,6 @@
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -7314,7 +7336,6 @@
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
@@ -8266,7 +8287,6 @@
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -8535,6 +8555,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-keys": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
@@ -9104,7 +9130,6 @@
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
@@ -9541,7 +9566,6 @@
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
@@ -11129,7 +11153,6 @@
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
@@ -11357,7 +11380,6 @@
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
@@ -11420,7 +11442,6 @@
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.75.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
@@ -12559,7 +12580,6 @@
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
@@ -12584,7 +12604,6 @@
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
@@ -12594,7 +12613,6 @@
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -12617,12 +12635,10 @@
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -12630,7 +12646,6 @@
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
@@ -12643,7 +12658,6 @@
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||
"version": "2.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
@@ -12654,7 +12668,6 @@
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
@@ -12899,7 +12912,6 @@
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"dev": true,
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
@@ -12924,7 +12936,6 @@
|
||||
},
|
||||
"node_modules/rc/node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -13804,7 +13815,6 @@
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -13823,7 +13833,6 @@
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -14741,7 +14750,6 @@
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
|
||||
@@ -453,6 +453,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
|
||||
+38
-53
@@ -1,14 +1,15 @@
|
||||
syntax = "proto3";
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "cline/models.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
@@ -18,6 +19,7 @@ service StateService {
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
|
||||
}
|
||||
|
||||
message State {
|
||||
@@ -56,7 +58,6 @@ enum OpenaiReasoningEffort {
|
||||
LOW = 0;
|
||||
MEDIUM = 1;
|
||||
HIGH = 2;
|
||||
MINIMAL = 3;
|
||||
}
|
||||
|
||||
enum McpDisplayMode {
|
||||
@@ -107,16 +108,6 @@ message TelemetrySettingRequest {
|
||||
TelemetrySettingEnum setting = 2;
|
||||
}
|
||||
|
||||
// Browser settings for UpdateSettingsRequest
|
||||
message BrowserSettingsUpdate {
|
||||
optional Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
optional string chrome_executable_path = 4;
|
||||
optional bool disable_tool_use = 5;
|
||||
optional string custom_args = 6;
|
||||
}
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
@@ -137,9 +128,6 @@ message UpdateSettingsRequest {
|
||||
optional FocusChainSettings focus_chain_settings = 17;
|
||||
optional bool use_auto_condense = 18;
|
||||
optional string custom_prompt = 19;
|
||||
optional BrowserSettingsUpdate browser_settings = 20;
|
||||
optional string default_terminal_profile = 21;
|
||||
optional bool yolo_mode_toggled = 22;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -151,10 +139,10 @@ message ApiConfiguration {
|
||||
optional string lite_llm_base_url = 4;
|
||||
optional string lite_llm_api_key = 5;
|
||||
optional bool lite_llm_use_prompt_cache = 6;
|
||||
map<string, string> open_ai_headers = 7;
|
||||
optional string openai_headers = 7; // JSON string
|
||||
optional string anthropic_base_url = 8;
|
||||
optional string open_router_api_key = 9;
|
||||
optional string open_router_provider_sorting = 10;
|
||||
optional string openrouter_api_key = 9;
|
||||
optional string openrouter_provider_sorting = 10;
|
||||
optional string aws_access_key = 11;
|
||||
optional string aws_secret_key = 12;
|
||||
optional string aws_session_token = 13;
|
||||
@@ -167,14 +155,14 @@ message ApiConfiguration {
|
||||
optional string claude_code_path = 20;
|
||||
optional string vertex_project_id = 21;
|
||||
optional string vertex_region = 22;
|
||||
optional string open_ai_base_url = 23;
|
||||
optional string open_ai_api_key = 24;
|
||||
optional string openai_base_url = 23;
|
||||
optional string openai_api_key = 24;
|
||||
optional string ollama_base_url = 25;
|
||||
optional string ollama_api_options_ctx_num = 26;
|
||||
optional string lm_studio_base_url = 27;
|
||||
optional string gemini_api_key = 28;
|
||||
optional string gemini_base_url = 29;
|
||||
optional string open_ai_native_api_key = 30;
|
||||
optional string openai_native_api_key = 30;
|
||||
optional string deep_seek_api_key = 31;
|
||||
optional string requesty_api_key = 32;
|
||||
optional string requesty_base_url = 33;
|
||||
@@ -210,65 +198,61 @@ message ApiConfiguration {
|
||||
optional string qwen_code_oauth_path = 63;
|
||||
optional string dify_api_key = 64;
|
||||
optional string dify_base_url = 65;
|
||||
optional string oca_base_url = 66;
|
||||
optional string oca_api_key = 67;
|
||||
optional string oca_refresh_token = 68;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
|
||||
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
|
||||
optional string plan_mode_open_router_model_id = 107;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
|
||||
optional string plan_mode_open_ai_model_id = 109;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
|
||||
optional string plan_mode_openrouter_model_id = 107;
|
||||
optional string plan_mode_openrouter_model_info = 108; // JSON string
|
||||
optional string plan_mode_openai_model_id = 109;
|
||||
optional string plan_mode_openai_model_info = 110; // JSON string
|
||||
optional string plan_mode_ollama_model_id = 111;
|
||||
optional string plan_mode_lm_studio_model_id = 112;
|
||||
optional string plan_mode_lite_llm_model_id = 113;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
|
||||
optional string plan_mode_lite_llm_model_info = 114; // JSON string
|
||||
optional string plan_mode_requesty_model_id = 115;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
|
||||
optional string plan_mode_requesty_model_info = 116; // JSON string
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 120;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 121;
|
||||
optional string plan_mode_huawei_cloud_maas_model_info = 121;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 122;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 123;
|
||||
optional string plan_mode_oca_model_id = 124;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 125;
|
||||
optional string plan_mode_vercel_ai_gateway_model_info = 123;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
optional string act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
|
||||
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
|
||||
optional string act_mode_open_router_model_id = 207;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
|
||||
optional string act_mode_open_ai_model_id = 209;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
|
||||
optional string act_mode_openrouter_model_id = 207;
|
||||
optional string act_mode_openrouter_model_info = 208; // JSON string
|
||||
optional string act_mode_openai_model_id = 209;
|
||||
optional string act_mode_openai_model_info = 210; // JSON string
|
||||
optional string act_mode_ollama_model_id = 211;
|
||||
optional string act_mode_lm_studio_model_id = 212;
|
||||
optional string act_mode_lite_llm_model_id = 213;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
|
||||
optional string act_mode_lite_llm_model_info = 214; // JSON string
|
||||
optional string act_mode_requesty_model_id = 215;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
|
||||
optional string act_mode_requesty_model_info = 216; // JSON string
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 220;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 221;
|
||||
optional string act_mode_huawei_cloud_maas_model_info = 221;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 222;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 223;
|
||||
optional string act_mode_oca_model_id = 224;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 225;
|
||||
optional string act_mode_vercel_ai_gateway_model_info = 223;
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 300;
|
||||
|
||||
// Extension fields for Bedrock Api Keys
|
||||
optional string aws_authentication = 301;
|
||||
@@ -286,11 +270,12 @@ message FocusChainSettings {
|
||||
int32 remind_cline_interval = 2;
|
||||
}
|
||||
|
||||
message Viewport {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
message ProcessInfo {
|
||||
int32 process_id = 1;
|
||||
optional string version = 2;
|
||||
optional int64 uptime_ms = 3;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { ProcessInfo } from "@shared/proto/cline/state"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Gets process information including PID, version, and uptime
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns ProcessInfo with process details
|
||||
*/
|
||||
export async function getProcessInfo(controller: Controller, request: EmptyRequest): Promise<ProcessInfo> {
|
||||
// Get the current state to access the version (same source as webview)
|
||||
const state = await controller.getStateToPostToWebview()
|
||||
|
||||
return ProcessInfo.create({
|
||||
processId: process.pid,
|
||||
version: state.version || "unknown",
|
||||
uptimeMs: Math.floor(process.uptime() * 1000), // Convert seconds to milliseconds
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import Database from "better-sqlite3"
|
||||
import { existsSync, mkdirSync, unlinkSync } from "fs"
|
||||
import * as path from "path"
|
||||
import type { InstanceLockData, SqliteLockManagerOptions } from "./types"
|
||||
|
||||
export class SqliteLockManager {
|
||||
private db!: Database.Database
|
||||
private instanceAddress: string
|
||||
private dbPath: string
|
||||
private readonly STALE_LOCK_TIMEOUT = 1 * 60 * 1000 // 1 minute in milliseconds
|
||||
|
||||
constructor(options: SqliteLockManagerOptions) {
|
||||
this.instanceAddress = options.instanceAddress
|
||||
this.dbPath = options.dbPath
|
||||
|
||||
// Ensure the directory exists before creating the database
|
||||
const dbDir = path.dirname(this.dbPath)
|
||||
try {
|
||||
mkdirSync(dbDir, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error(`CRITICAL ERROR: Failed to create SQLite database directory ${dbDir}:`, error)
|
||||
throw new Error(`Failed to create SQLite database directory: ${error}`)
|
||||
}
|
||||
|
||||
try {
|
||||
this.initializeDatabaseWithLockSync()
|
||||
} catch (error) {
|
||||
console.error(`CRITICAL ERROR: Failed to initialize SQLite database at ${this.dbPath}:`, error)
|
||||
throw new Error(`Failed to initialize SQLite database: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDatabaseWithLockSync(): void {
|
||||
const lockFile = `${this.dbPath}.lock`
|
||||
|
||||
// Clean up stale lock files first
|
||||
this.cleanupStaleLockSync(lockFile)
|
||||
|
||||
try {
|
||||
// Try to acquire exclusive file lock for database creation
|
||||
const fs = require("fs")
|
||||
let fd: number | null = null
|
||||
|
||||
try {
|
||||
fd = fs.openSync(lockFile, "wx") // Exclusive creation - fails if file exists
|
||||
|
||||
// Write timestamp to lock file for stale lock detection
|
||||
fs.writeFileSync(fd, Date.now().toString())
|
||||
|
||||
// Check if database already exists
|
||||
const dbExists = existsSync(this.dbPath)
|
||||
|
||||
if (!dbExists) {
|
||||
// Database doesn't exist, create it
|
||||
this.db = new Database(this.dbPath)
|
||||
this.initializeDatabase()
|
||||
} else {
|
||||
// Database exists, just open it
|
||||
this.db = new Database(this.dbPath)
|
||||
}
|
||||
} finally {
|
||||
// Always clean up the lock file
|
||||
if (fd !== null) {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
try {
|
||||
unlinkSync(lockFile)
|
||||
} catch {} // Ignore errors if file was already deleted
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === "EEXIST") {
|
||||
// Another process is initializing the database, wait and retry
|
||||
const delay = 100 + Math.random() * 100 // Add jitter
|
||||
this.sleepSync(delay)
|
||||
this.initializeDatabaseWithLockSync()
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private sleepSync(ms: number) {
|
||||
// Non-spinning, synchronous sleep using Atomics.wait
|
||||
// Works in Node main thread (since v12.16+) and worker threads.
|
||||
const sab = new SharedArrayBuffer(4)
|
||||
const ia = new Int32Array(sab)
|
||||
Atomics.wait(ia, 0, 0, Math.max(0, Math.floor(ms)))
|
||||
}
|
||||
|
||||
private cleanupStaleLockSync(lockFile: string): void {
|
||||
try {
|
||||
if (!existsSync(lockFile)) {
|
||||
return // Lock file doesn't exist, nothing to clean up
|
||||
}
|
||||
|
||||
const fs = require("fs")
|
||||
|
||||
try {
|
||||
const timestampStr = fs.readFileSync(lockFile, "utf8").trim()
|
||||
const timestamp = parseInt(timestampStr, 10)
|
||||
|
||||
if (isNaN(timestamp) || Date.now() - timestamp > this.STALE_LOCK_TIMEOUT) {
|
||||
// Stale lock, remove it
|
||||
unlinkSync(lockFile)
|
||||
console.warn(`Removed stale database lock file: ${lockFile}`)
|
||||
}
|
||||
} catch (readError) {
|
||||
// If we can't read the timestamp, assume it's stale
|
||||
unlinkSync(lockFile)
|
||||
console.warn(`Removed unreadable database lock file: ${lockFile}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
// Lock file doesn't exist, which is fine
|
||||
console.warn(`Error checking lock file ${lockFile}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDatabase() {
|
||||
// Create the locks table with the unified schema (matches cli/pkg/common/schema.go)
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS locks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
held_by TEXT NOT NULL,
|
||||
lock_type TEXT NOT NULL CHECK (lock_type IN ('file', 'instance', 'folder')),
|
||||
lock_target TEXT NOT NULL,
|
||||
locked_at INTEGER NOT NULL,
|
||||
UNIQUE(lock_type, lock_target)
|
||||
);
|
||||
`)
|
||||
|
||||
// Create indexes for performance (matches cli/pkg/common/schema.go)
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_locks_held_by ON locks(held_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_locks_type ON locks(lock_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_locks_target ON locks(lock_target);
|
||||
`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this instance in the locks table
|
||||
*/
|
||||
async registerInstance(data: {
|
||||
corePort: number
|
||||
hostPort: number
|
||||
version?: string
|
||||
status?: InstanceLockData["status"]
|
||||
}): Promise<void> {
|
||||
const now = Date.now()
|
||||
const hostAddress = `localhost:${data.hostPort}`
|
||||
|
||||
// Create instance lock entry
|
||||
const insertLock = this.db.prepare(`
|
||||
INSERT OR REPLACE INTO locks (held_by, lock_type, lock_target, locked_at)
|
||||
VALUES (?, 'instance', ?, ?)
|
||||
`)
|
||||
|
||||
insertLock.run(this.instanceAddress, hostAddress, now)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timestamp for this instance (touch)
|
||||
*/
|
||||
touchInstance(): void {
|
||||
const now = Date.now()
|
||||
const updateLock = this.db.prepare(`
|
||||
UPDATE locks
|
||||
SET locked_at = ?
|
||||
WHERE held_by = ? AND lock_type = 'instance'
|
||||
`)
|
||||
|
||||
updateLock.run(now, this.instanceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this instance from the locks table
|
||||
*/
|
||||
unregisterInstance(): void {
|
||||
const deleteLock = this.db.prepare(`
|
||||
DELETE FROM locks
|
||||
WHERE held_by = ? AND lock_type = 'instance'
|
||||
`)
|
||||
|
||||
deleteLock.run(this.instanceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the registry for any instance registered on the given port
|
||||
*/
|
||||
getInstanceByPort(port: number): { instanceAddress: string; hostAddress: string } | null {
|
||||
const query = this.db.prepare(`
|
||||
SELECT held_by, lock_target
|
||||
FROM locks
|
||||
WHERE lock_type = 'instance'
|
||||
AND (held_by LIKE '%:' || ? OR lock_target LIKE '%:' || ?)
|
||||
`)
|
||||
|
||||
const result = query.get(port, port) as { held_by: string; lock_target: string } | undefined
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
instanceAddress: result.held_by,
|
||||
hostAddress: result.lock_target,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific instance entry from the registry
|
||||
*/
|
||||
removeInstanceByAddress(instanceAddress: string): void {
|
||||
const deleteLock = this.db.prepare(`
|
||||
DELETE FROM locks
|
||||
WHERE held_by = ? AND lock_type = 'instance'
|
||||
`)
|
||||
|
||||
deleteLock.run(instanceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
this.db.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type LockType = "file" | "instance" | "folder"
|
||||
|
||||
export type LockStatus = "starting" | "healthy" | "unhealthy"
|
||||
|
||||
export interface LockRow {
|
||||
id: number
|
||||
held_by: string // address:port of instance holding the lock
|
||||
lock_type: LockType
|
||||
lock_target: string // varies by type: file path, host address, or folder path
|
||||
locked_at: number // timestamp when lock was acquired
|
||||
}
|
||||
|
||||
export interface InstanceLockData {
|
||||
address: string
|
||||
core_port: number
|
||||
host_port: number
|
||||
status: LockStatus
|
||||
last_seen: string
|
||||
process_pid: number
|
||||
version?: string
|
||||
created_at: string
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface SqliteLockManagerOptions {
|
||||
dbPath: string
|
||||
instanceAddress: string // host:port format
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* TypeScript equivalent of the Go common.RetryOperation utility
|
||||
* Performs an operation with retry logic and timeout handling
|
||||
*/
|
||||
export async function retryOperation<T>(maxRetries: number, timeoutPerAttempt: number, operation: () => Promise<T>): Promise<T> {
|
||||
let lastError: Error | undefined
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Create a timeout promise
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Operation timeout")), timeoutPerAttempt),
|
||||
)
|
||||
|
||||
// Race the operation against timeout
|
||||
const result = await Promise.race([operation(), timeoutPromise])
|
||||
return result // Success - return result
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error))
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Brief delay before retry
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Operation failed after ${maxRetries} attempts: ${lastError?.message}`)
|
||||
}
|
||||
+234
-30
@@ -2,66 +2,211 @@ import { ExternalDiffViewProvider } from "@hosts/external/ExternalDiffviewProvid
|
||||
import { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
|
||||
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
|
||||
import { WebviewProviderType } from "@shared/webview/types"
|
||||
import * as path from "path"
|
||||
import { retryOperation } from "@utils/retry"
|
||||
import os from "os"
|
||||
import { initialize, tearDown } from "@/common"
|
||||
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
import { waitForHostBridgeReady } from "./hostbridge-client"
|
||||
import { startProtobusService } from "./protobus-service"
|
||||
import { log } from "./utils"
|
||||
import { DATA_DIR, EXTENSION_DIR, extensionContext } from "./vscode-context"
|
||||
import { checkPortAvailability } from "./port-checker"
|
||||
import { startProtobusService, waitForHostBridgeReady } from "./protobus-service"
|
||||
import { log, SETTINGS_SUBFOLDER } from "./utils"
|
||||
import { createExtensionContext } from "./vscode-context"
|
||||
|
||||
// Default ports
|
||||
export const DEFAULT_PROTOBUS_PORT = 26040
|
||||
export const DEFAULT_HOSTBRIDGE_PORT = 26041
|
||||
|
||||
// Parse command line arguments
|
||||
interface CliArgs {
|
||||
port?: number
|
||||
hostBridgePort?: number
|
||||
config?: string
|
||||
help?: boolean
|
||||
}
|
||||
|
||||
function parseArgs(): CliArgs {
|
||||
const args: CliArgs = {}
|
||||
const argv = process.argv.slice(2)
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
switch (arg) {
|
||||
case "--port":
|
||||
case "-p":
|
||||
args.port = parseInt(argv[++i], 10)
|
||||
break
|
||||
case "--host-bridge-port":
|
||||
args.hostBridgePort = parseInt(argv[++i], 10)
|
||||
break
|
||||
case "--config":
|
||||
case "-c":
|
||||
args.config = argv[++i]
|
||||
break
|
||||
case "--help":
|
||||
case "-h":
|
||||
args.help = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
Cline Core - Standalone Server
|
||||
|
||||
Usage: node cline-core.js [options]
|
||||
|
||||
Options:
|
||||
-p, --port <port> Port for the main gRPC service (default: ${DEFAULT_PROTOBUS_PORT})
|
||||
--host-bridge-port <port> Port for the host bridge service (default: ${DEFAULT_HOSTBRIDGE_PORT})
|
||||
-c, --config <path> Directory for Cline data storage (default: ~/.cline)
|
||||
-h, --help Show this help message
|
||||
|
||||
Environment Variables:
|
||||
PROTOBUS_ADDRESS Override the main service address (format: host:port)
|
||||
HOSTBRIDGE_ADDRESS Override the host bridge address (format: host:port)
|
||||
`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Parse command line arguments
|
||||
const args = parseArgs()
|
||||
|
||||
// Show help if requested
|
||||
if (args.help) {
|
||||
showHelp()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Configure ports from arguments
|
||||
let protobusPort = DEFAULT_PROTOBUS_PORT
|
||||
let hostBridgePort = DEFAULT_HOSTBRIDGE_PORT
|
||||
|
||||
if (args.port) {
|
||||
protobusPort = args.port
|
||||
// If only port is specified, calculate hostbridge port as port + 1000
|
||||
if (!args.hostBridgePort) {
|
||||
hostBridgePort = protobusPort + 1000
|
||||
}
|
||||
}
|
||||
if (args.hostBridgePort) {
|
||||
hostBridgePort = args.hostBridgePort
|
||||
}
|
||||
|
||||
// Set environment variables for the services to use
|
||||
if (!process.env.PROTOBUS_ADDRESS) {
|
||||
process.env.PROTOBUS_ADDRESS = `localhost:${protobusPort}`
|
||||
}
|
||||
if (!process.env.HOSTBRIDGE_ADDRESS) {
|
||||
process.env.HOSTBRIDGE_ADDRESS = `localhost:${hostBridgePort}`
|
||||
}
|
||||
|
||||
// Configure Cline directory from arguments
|
||||
const clineDir = args.config || `${os.homedir()}/.cline`
|
||||
|
||||
log("\n\n\nStarting cline-core service...\n\n\n")
|
||||
log(`Using Protobus port: ${protobusPort}`)
|
||||
log(`Using Host Bridge port: ${hostBridgePort}`)
|
||||
log(`Using Cline directory: ${clineDir}`)
|
||||
|
||||
await waitForHostBridgeReady()
|
||||
// Initialize SQLite lock manager for instance registration
|
||||
const dbPath = `${clineDir}/${SETTINGS_SUBFOLDER}/locks.db`
|
||||
// Use host:port everywhere (no scheme)
|
||||
const fullAddress = `localhost:${protobusPort}`
|
||||
let lockManager: SqliteLockManager | undefined
|
||||
try {
|
||||
lockManager = new SqliteLockManager({
|
||||
dbPath,
|
||||
instanceAddress: fullAddress,
|
||||
})
|
||||
|
||||
// The host bridge should be available before creating the host provider because it depends on the host bridge.
|
||||
setupHostProvider()
|
||||
// Check port availability before proceeding
|
||||
log(`Checking port availability for ${protobusPort}...`)
|
||||
const portCheck = await checkPortAvailability(protobusPort, lockManager)
|
||||
|
||||
if (!portCheck.canProceed) {
|
||||
log(`STARTUP BLOCKED: ${portCheck.error}`)
|
||||
lockManager.close()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await lockManager.registerInstance({
|
||||
corePort: protobusPort,
|
||||
hostPort: hostBridgePort,
|
||||
version: process.env.CLINE_VERSION,
|
||||
status: "starting",
|
||||
})
|
||||
log(`Registered instance in SQLite locks: ${fullAddress}`)
|
||||
} catch (err) {
|
||||
log(`CRITICAL ERROR: Failed to register instance in SQLite locks: ${String(err)}`)
|
||||
log(`This is a fatal error - cline-core cannot start without proper instance registration`)
|
||||
if (lockManager) {
|
||||
try {
|
||||
lockManager.close()
|
||||
} catch {}
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForHostBridgeReady()
|
||||
log("HostBridge is serving; continuing startup")
|
||||
} catch (err) {
|
||||
log(`ERROR: HostBridge error: ${String(err)}`)
|
||||
// Cleanup lock manager entry if startup fails
|
||||
if (lockManager) {
|
||||
try {
|
||||
lockManager.unregisterInstance()
|
||||
lockManager.close()
|
||||
} catch {}
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create extension context with the configured directory
|
||||
const extensionContext = createExtensionContext(clineDir)
|
||||
|
||||
setupHostProvider(extensionContext)
|
||||
|
||||
// Set up global error handlers to prevent process crashes
|
||||
setupGlobalErrorHandlers()
|
||||
setupGlobalErrorHandlers(lockManager)
|
||||
|
||||
const webviewProvider = await initialize(extensionContext)
|
||||
|
||||
// Enable the localhost HTTP server that handles auth redirects.
|
||||
AuthHandler.getInstance().setEnabled(true)
|
||||
|
||||
startProtobusService(webviewProvider.controller)
|
||||
|
||||
// Mark instance healthy after services are up
|
||||
try {
|
||||
lockManager?.touchInstance()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function setupHostProvider() {
|
||||
function setupHostProvider(extensionContext: any) {
|
||||
const createWebview = (_: WebviewProviderType): WebviewProvider => {
|
||||
return new ExternalWebviewProvider(extensionContext, WebviewProviderType.SIDEBAR)
|
||||
}
|
||||
const createDiffView = (): DiffViewProvider => {
|
||||
return new ExternalDiffViewProvider()
|
||||
}
|
||||
const getCallbackUrl = (): Promise<string> => {
|
||||
return AuthHandler.getInstance().getCallbackUrl()
|
||||
const getCallbackUri = (): Promise<string> => {
|
||||
return AuthHandler.getInstance().getCallbackUri()
|
||||
}
|
||||
// cline-core expects the binaries to be unpacked in the directory where it is running.
|
||||
const getBinaryLocation = async (name: string): Promise<string> => path.join(process.cwd(), name)
|
||||
|
||||
HostProvider.initialize(
|
||||
createWebview,
|
||||
createDiffView,
|
||||
new ExternalHostBridgeClientManager(),
|
||||
log,
|
||||
getCallbackUrl,
|
||||
getBinaryLocation,
|
||||
EXTENSION_DIR,
|
||||
DATA_DIR,
|
||||
)
|
||||
HostProvider.initialize(createWebview, createDiffView, new ExternalHostBridgeClientManager(), log, getCallbackUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up global error handlers to prevent the process from crashing
|
||||
* on unhandled exceptions and promise rejections
|
||||
*/
|
||||
function setupGlobalErrorHandlers() {
|
||||
function setupGlobalErrorHandlers(lockManager?: SqliteLockManager) {
|
||||
// Handle unhandled exceptions
|
||||
process.on("uncaughtException", (error: Error) => {
|
||||
log(`ERROR: Uncaught exception: ${error.message}`)
|
||||
@@ -86,15 +231,74 @@ function setupGlobalErrorHandlers() {
|
||||
// Graceful shutdown handlers
|
||||
process.on("SIGINT", () => {
|
||||
log("Received SIGINT, shutting down gracefully...")
|
||||
process.exit(0)
|
||||
shutdownGracefully(lockManager)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
log("Received SIGTERM, shutting down gracefully...")
|
||||
tearDown()
|
||||
|
||||
process.exit(0)
|
||||
shutdownGracefully(lockManager)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Request host bridge shutdown with retry logic and timeout handling.
|
||||
* Uses best-effort approach - logs failures but doesn't block shutdown.
|
||||
*/
|
||||
async function requestHostBridgeShutdown(): Promise<void> {
|
||||
try {
|
||||
await retryOperation(3, 2000, async () => {
|
||||
await HostProvider.env.shutdown({})
|
||||
})
|
||||
log("Host bridge shutdown requested successfully")
|
||||
} catch (error) {
|
||||
log(`Warning: Failed to request host bridge shutdown: ${error}`)
|
||||
log("Proceeding with cleanup")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shutdown the cline-core process by:
|
||||
* 1. Calling shutdown RPC on the paired host bridge
|
||||
* 2. Cleaning up the lock manager entry
|
||||
* 3. Tearing down services
|
||||
* 4. Exiting the process
|
||||
*/
|
||||
async function shutdownGracefully(lockManager?: SqliteLockManager) {
|
||||
try {
|
||||
// Step 1: Tell the paired host bridge to shut down
|
||||
log("Requesting host bridge shutdown...")
|
||||
if (HostProvider.isInitialized()) {
|
||||
await requestHostBridgeShutdown()
|
||||
} else {
|
||||
log("Warning: HostProvider not initialized, cannot request shutdown")
|
||||
}
|
||||
|
||||
// Step 2: Clean up lock manager entry
|
||||
log("Cleaning up lock manager entry...")
|
||||
try {
|
||||
lockManager?.unregisterInstance()
|
||||
lockManager?.close()
|
||||
log("Lock manager entry cleaned up successfully")
|
||||
} catch (error) {
|
||||
log(`Warning: Failed to clean up lock manager: ${error}`)
|
||||
}
|
||||
|
||||
// Step 3: Tear down services
|
||||
log("Tearing down services...")
|
||||
try {
|
||||
tearDown()
|
||||
log("Services torn down successfully")
|
||||
} catch (error) {
|
||||
log(`Warning: Failed to tear down services: ${error}`)
|
||||
}
|
||||
|
||||
log("Graceful shutdown completed")
|
||||
} catch (error) {
|
||||
log(`Error during graceful shutdown: ${error}`)
|
||||
} finally {
|
||||
// Step 4: Exit the process
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as health from "grpc-health-check"
|
||||
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
|
||||
import { log } from "./utils"
|
||||
|
||||
const SERVING_STATUS = 1
|
||||
|
||||
interface PortCheckResult {
|
||||
canProceed: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a gRPC health client for the given address
|
||||
*/
|
||||
function createHealthClient(address: string): any {
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const grpcObj = grpc.loadPackageDefinition(healthDef) as unknown as any
|
||||
const Health = grpcObj.grpc.health.v1.Health
|
||||
return new Health(address, grpc.credentials.createInsecure())
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single health check on the given address
|
||||
*/
|
||||
async function checkHealthOnce(address: string): Promise<{ success: boolean; status?: number; error?: Error }> {
|
||||
const client = createHealthClient(address)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
try {
|
||||
client.close?.()
|
||||
} catch {}
|
||||
resolve({ success: false, error: new Error("Health check timeout") })
|
||||
}, 5000) // 5 second timeout
|
||||
|
||||
client.check({ service: "" }, (err: unknown, resp: any) => {
|
||||
clearTimeout(timeout)
|
||||
try {
|
||||
client.close?.()
|
||||
} catch {}
|
||||
|
||||
if (err) {
|
||||
resolve({ success: false, error: err as Error })
|
||||
} else {
|
||||
resolve({ success: true, status: resp?.status })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to shut down a host bridge instance
|
||||
*/
|
||||
async function shutdownHostBridge(hostAddress: string): Promise<boolean> {
|
||||
try {
|
||||
log(`Attempting to shutdown host bridge at ${hostAddress}`)
|
||||
|
||||
// This would need to be implemented - we need a way to send shutdown to a specific host
|
||||
// For now, we'll just log that we would do this
|
||||
log(`Would send shutdown command to host bridge at ${hostAddress}`)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
log(`Failed to shutdown host bridge at ${hostAddress}: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a port is available for binding, following the registry-first approach
|
||||
*/
|
||||
export async function checkPortAvailability(port: number, lockManager: SqliteLockManager): Promise<PortCheckResult> {
|
||||
log(`Checking port availability for port ${port}`)
|
||||
|
||||
// Step 1: Check registry first
|
||||
const registryEntry = lockManager.getInstanceByPort(port)
|
||||
|
||||
if (!registryEntry) {
|
||||
log(`No registry entry found for port ${port}, free to bind`)
|
||||
return { canProceed: true }
|
||||
}
|
||||
|
||||
log(`Found registry entry for port ${port}: instance=${registryEntry.instanceAddress}, host=${registryEntry.hostAddress}`)
|
||||
|
||||
// Step 2: Perform health check on the registered instance
|
||||
const coreAddress = registryEntry.instanceAddress
|
||||
|
||||
const performHealthCheck = async (): Promise<{ success: boolean; status?: number; error?: Error }> => {
|
||||
return await checkHealthOnce(coreAddress)
|
||||
}
|
||||
|
||||
// First health check attempt
|
||||
let healthResult = await performHealthCheck()
|
||||
|
||||
if (!healthResult.success) {
|
||||
// Health check ERROR - not our process
|
||||
log(`Health check failed for ${coreAddress}: ${healthResult.error?.message}`)
|
||||
log(`This indicates a non-Cline process is using port ${port}`)
|
||||
|
||||
// Attempt to shutdown the registered host bridge
|
||||
const shutdownSuccess = await shutdownHostBridge(registryEntry.hostAddress)
|
||||
if (shutdownSuccess) {
|
||||
log(`Successfully requested shutdown of host bridge ${registryEntry.hostAddress}`)
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
lockManager.removeInstanceByAddress(registryEntry.instanceAddress)
|
||||
log(`Removed stale registry entry for ${registryEntry.instanceAddress}`)
|
||||
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Port ${port} is occupied by a non-Cline process. Registry has been cleaned up.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Health check succeeded - it's our process
|
||||
if (healthResult.status === SERVING_STATUS) {
|
||||
// Healthy Cline instance already running
|
||||
log(`Healthy Cline instance already running on port ${port}`)
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `A healthy Cline instance is already running on port ${port}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Health check succeeded but status is not SERVING - unhealthy Cline instance
|
||||
log(`Unhealthy Cline instance detected on port ${port} (status: ${healthResult.status}), retrying in 1 second`)
|
||||
|
||||
// Wait 1 second and retry
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// Second health check attempt
|
||||
healthResult = await performHealthCheck()
|
||||
|
||||
if (!healthResult.success) {
|
||||
// Now it's erroring - something changed
|
||||
log(`Health check now failing after retry for ${coreAddress}: ${healthResult.error?.message}`)
|
||||
|
||||
// Clean up registry since the instance is no longer responding
|
||||
lockManager.removeInstanceByAddress(registryEntry.instanceAddress)
|
||||
log(`Removed non-responsive registry entry for ${registryEntry.instanceAddress}`)
|
||||
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Port ${port} had an unhealthy Cline instance that is now non-responsive. Registry cleaned up.`,
|
||||
}
|
||||
}
|
||||
|
||||
if (healthResult.status === SERVING_STATUS) {
|
||||
// Instance recovered
|
||||
log(`Cline instance on port ${port} has recovered and is now healthy`)
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Cline instance on port ${port} has recovered and is now serving`,
|
||||
}
|
||||
}
|
||||
|
||||
// Still unhealthy after retry
|
||||
log(`Cline instance on port ${port} remains unhealthy after retry (status: ${healthResult.status})`)
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Cline instance on port ${port} is unhealthy and did not recover after retry`,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import * as fs from "fs"
|
||||
import * as health from "grpc-health-check"
|
||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
||||
|
||||
// WE WILL HAVE TO MIGRATE THIS FROM DATA TO v1 LATER
|
||||
const SETTINGS_SUBFOLDER = "data"
|
||||
|
||||
const log = (...args: unknown[]) => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
@@ -20,6 +23,7 @@ const log = (...args: unknown[]) => {
|
||||
|
||||
function getPackageDefinition() {
|
||||
// Load service definitions.
|
||||
// When running as standalone, the descriptor set is in the same directory as the standalone.js file
|
||||
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
|
||||
const options = { longs: Number } // Encode int64 fields as numbers
|
||||
const descriptorDefs = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet, options)
|
||||
@@ -51,4 +55,4 @@ async function asyncIteratorToCallbacks<T>(stream: AsyncIterable<T>, callbacks:
|
||||
}
|
||||
}
|
||||
|
||||
export { getPackageDefinition, log, asyncIteratorToCallbacks }
|
||||
export { getPackageDefinition, log, asyncIteratorToCallbacks, SETTINGS_SUBFOLDER }
|
||||
|
||||
@@ -1,69 +1,89 @@
|
||||
import { mkdirSync } from "node:fs"
|
||||
import os from "os"
|
||||
import { mkdirSync } from "fs"
|
||||
import path from "path"
|
||||
import type { Extension, ExtensionContext } from "vscode"
|
||||
import { ExtensionKind, ExtensionMode } from "vscode"
|
||||
import { URI } from "vscode-uri"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
// @ts-ignore
|
||||
import { StandaloneTerminalManager } from "../../standalone/runtime-files/vscode/enhanced-terminal"
|
||||
import { log } from "./utils"
|
||||
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
|
||||
import { EnvironmentVariableCollection, MementoStore, SecretStore } from "./vscode-context-utils"
|
||||
|
||||
log("Running standalone cline", ExtensionRegistryInfo.version)
|
||||
log(`CLINE_ENVIRONMENT: ${process.env.CLINE_ENVIRONMENT}`)
|
||||
|
||||
export const CLINE_DIR = process.env.CLINE_DIR || `${os.homedir()}/.cline`
|
||||
export const DATA_DIR = path.join(CLINE_DIR, "data")
|
||||
const INSTALL_DIR = process.env.INSTALL_DIR || __dirname
|
||||
const WORKSPACE_STORAGE_DIR = process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspace")
|
||||
|
||||
mkdirSync(DATA_DIR, { recursive: true })
|
||||
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
|
||||
log("Using settings dir:", DATA_DIR)
|
||||
|
||||
export const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
|
||||
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
|
||||
|
||||
const extension: Extension<void> = {
|
||||
id: ExtensionRegistryInfo.id,
|
||||
isActive: true,
|
||||
extensionPath: EXTENSION_DIR,
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
packageJSON: readJson(path.join(EXTENSION_DIR, "package.json")),
|
||||
exports: undefined, // There are no API exports in the standalone version.
|
||||
activate: async () => {},
|
||||
extensionKind: ExtensionKind.UI,
|
||||
function getPackageVersion(): string {
|
||||
// Use build-time injected version (only method)
|
||||
return process.env.CLINE_VERSION || "unknown"
|
||||
}
|
||||
|
||||
const extensionContext: ExtensionContext = {
|
||||
extension: extension,
|
||||
extensionMode: EXTENSION_MODE,
|
||||
const VERSION = getPackageVersion()
|
||||
log("Running standalone cline ", VERSION)
|
||||
|
||||
// Set up KV stores.
|
||||
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
|
||||
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
|
||||
function createExtensionContext(clineDir: string): ExtensionContext {
|
||||
const DATA_DIR = path.join(clineDir, "data")
|
||||
const INSTALL_DIR = process.env.INSTALL_DIR || path.join(clineDir, "core", VERSION)
|
||||
mkdirSync(DATA_DIR, { recursive: true })
|
||||
log("Using settings dir:", DATA_DIR)
|
||||
|
||||
// Set up URIs.
|
||||
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
|
||||
storagePath: WORKSPACE_STORAGE_DIR, // Deprecated, not used in cline.
|
||||
globalStorageUri: URI.file(DATA_DIR),
|
||||
globalStoragePath: DATA_DIR, // Deprecated, not used in cline.
|
||||
const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
|
||||
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
|
||||
|
||||
// Logs are global per extension, not per workspace.
|
||||
logUri: URI.file(DATA_DIR),
|
||||
logPath: DATA_DIR, // Deprecated, not used in cline.
|
||||
// Static package.json data for extension context (no filesystem reading)
|
||||
function getExtensionPackageJson(): any {
|
||||
return {
|
||||
name: "claude-dev",
|
||||
displayName: "Cline",
|
||||
version: VERSION,
|
||||
publisher: "saoudrizwan",
|
||||
}
|
||||
}
|
||||
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
extensionPath: EXTENSION_DIR, // Deprecated, not used in cline.
|
||||
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
|
||||
const extension: Extension<void> = {
|
||||
id: "saoudrizwan.claude-dev",
|
||||
isActive: true,
|
||||
extensionPath: EXTENSION_DIR,
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
packageJSON: getExtensionPackageJson(),
|
||||
exports: undefined, // There are no API exports in the standalone version.
|
||||
activate: async () => {},
|
||||
extensionKind: ExtensionKind.UI,
|
||||
}
|
||||
|
||||
subscriptions: [], // These need to be destroyed when the extension is deactivated.
|
||||
const extensionContext: ExtensionContext = {
|
||||
extension: extension,
|
||||
extensionMode: EXTENSION_MODE,
|
||||
|
||||
environmentVariableCollection: new EnvironmentVariableCollection(),
|
||||
// Set up KV stores.
|
||||
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
|
||||
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
|
||||
|
||||
// Workspace state is per project/workspace when WORKSPACE_STORAGE_DIR is provided by the host.
|
||||
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
|
||||
// Set up URIs.
|
||||
storageUri: URI.file(DATA_DIR),
|
||||
storagePath: DATA_DIR, // Deprecated, not used in cline.
|
||||
globalStorageUri: URI.file(DATA_DIR),
|
||||
globalStoragePath: DATA_DIR, // Deprecated, not used in cline.
|
||||
|
||||
logUri: URI.file(DATA_DIR),
|
||||
logPath: DATA_DIR, // Deprecated, not used in cline.
|
||||
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
extensionPath: EXTENSION_DIR, // Deprecated, not used in cline.
|
||||
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
|
||||
|
||||
subscriptions: [], // These need to be destroyed when the extension is deactivated.
|
||||
|
||||
environmentVariableCollection: new EnvironmentVariableCollection(),
|
||||
|
||||
// TODO(sjf): Workspace state needs to be per project/workspace.
|
||||
workspaceState: new MementoStore(path.join(DATA_DIR, "workspaceState.json")),
|
||||
}
|
||||
|
||||
return extensionContext
|
||||
}
|
||||
|
||||
// Initialize the standalone terminal manager for use by Task instances
|
||||
const standaloneTerminalManager = new StandaloneTerminalManager()
|
||||
|
||||
// Set it as a global so Task constructor can access it
|
||||
;(global as any).standaloneTerminalManager = standaloneTerminalManager
|
||||
|
||||
console.log("Finished loading vscode context...")
|
||||
|
||||
export { extensionContext }
|
||||
export { createExtensionContext }
|
||||
|
||||
Reference in New Issue
Block a user