mirror of
https://github.com/cline/cline.git
synced 2026-09-05 14:14:01 +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 __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
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 watch = process.argv.includes("--watch")
|
||||||
const standalone = process.argv.includes("--standalone")
|
const standalone = process.argv.includes("--standalone")
|
||||||
const e2eBuild = process.argv.includes("--e2e-build")
|
const e2eBuild = process.argv.includes("--e2e-build")
|
||||||
const destDir = standalone ? "dist-standalone" : "dist"
|
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}
|
* @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
|
// Base configuration shared between extension and standalone builds
|
||||||
const baseConfig = {
|
const baseConfig = {
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: production,
|
minify: production,
|
||||||
sourcemap: !production,
|
sourcemap: !production,
|
||||||
logLevel: "silent",
|
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"),
|
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||||
plugins: [
|
plugins: [
|
||||||
copyWasmFiles,
|
copyWasmFiles,
|
||||||
@@ -174,9 +163,14 @@ const standaloneConfig = {
|
|||||||
...baseConfig,
|
...baseConfig,
|
||||||
entryPoints: ["src/standalone/cline-core.ts"],
|
entryPoints: ["src/standalone/cline-core.ts"],
|
||||||
outfile: `${destDir}/cline-core.js`,
|
outfile: `${destDir}/cline-core.js`,
|
||||||
// These modules need to load files from the module directory at runtime,
|
// These gRPC protos need to load files from the module directory at runtime,
|
||||||
// so they cannot be bundled.
|
// 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"],
|
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
|
// E2E build script configuration
|
||||||
|
|||||||
Generated
+30
-22
@@ -36,6 +36,7 @@
|
|||||||
"@vscode/codicons": "^0.0.36",
|
"@vscode/codicons": "^0.0.36",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"chrome-launcher": "^1.1.2",
|
"chrome-launcher": "^1.1.2",
|
||||||
@@ -6279,6 +6280,20 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/big-integer": {
|
||||||
"version": "1.6.52",
|
"version": "1.6.52",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
@@ -6326,6 +6341,15 @@
|
|||||||
"url": "https://bevry.me/fund"
|
"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": {
|
"node_modules/bluebird": {
|
||||||
"version": "3.4.7",
|
"version": "3.4.7",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -7278,7 +7302,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-response": "^3.1.0"
|
"mimic-response": "^3.1.0"
|
||||||
@@ -7292,7 +7315,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -7314,7 +7336,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/deep-extend": {
|
"node_modules/deep-extend": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
@@ -8266,7 +8287,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/expand-template": {
|
"node_modules/expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"dev": true,
|
|
||||||
"license": "(MIT OR WTFPL)",
|
"license": "(MIT OR WTFPL)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -8535,6 +8555,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/fill-keys": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9104,7 +9130,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/github-from-package": {
|
"node_modules/github-from-package": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
@@ -9541,7 +9566,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/ini": {
|
"node_modules/ini": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
@@ -11129,7 +11153,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/mkdirp-classic": {
|
"node_modules/mkdirp-classic": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mocha": {
|
"node_modules/mocha": {
|
||||||
@@ -11357,7 +11380,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/napi-build-utils": {
|
"node_modules/napi-build-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
@@ -11420,7 +11442,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/node-abi": {
|
"node_modules/node-abi": {
|
||||||
"version": "3.75.0",
|
"version": "3.75.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.3.5"
|
"semver": "^7.3.5"
|
||||||
@@ -12559,7 +12580,6 @@
|
|||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.0",
|
"detect-libc": "^2.0.0",
|
||||||
@@ -12584,7 +12604,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/bl": {
|
"node_modules/prebuild-install/node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
@@ -12594,7 +12613,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/buffer": {
|
"node_modules/prebuild-install/node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -12617,12 +12635,10 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/chownr": {
|
"node_modules/prebuild-install/node_modules/chownr": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/detect-libc": {
|
"node_modules/prebuild-install/node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -12630,7 +12646,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/readable-stream": {
|
"node_modules/prebuild-install/node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -12643,7 +12658,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^1.1.1",
|
"chownr": "^1.1.1",
|
||||||
@@ -12654,7 +12668,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/tar-stream": {
|
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
@@ -12899,7 +12912,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/rc": {
|
"node_modules/rc": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"dev": true,
|
|
||||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-extend": "^0.6.0",
|
"deep-extend": "^0.6.0",
|
||||||
@@ -12924,7 +12936,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/rc/node_modules/strip-json-comments": {
|
"node_modules/rc/node_modules/strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -13804,7 +13815,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/simple-concat": {
|
"node_modules/simple-concat": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -13823,7 +13833,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/simple-get": {
|
"node_modules/simple-get": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -14741,7 +14750,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/tunnel-agent": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
|
|||||||
@@ -453,6 +453,7 @@
|
|||||||
"@vscode/codicons": "^0.0.36",
|
"@vscode/codicons": "^0.0.36",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"chrome-launcher": "^1.1.2",
|
"chrome-launcher": "^1.1.2",
|
||||||
|
|||||||
+38
-53
@@ -1,14 +1,15 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package cline;
|
package cline;
|
||||||
import "cline/common.proto";
|
import "cline/common.proto";
|
||||||
import "cline/models.proto";
|
|
||||||
option java_package = "bot.cline.proto";
|
option java_package = "bot.cline.proto";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
option go_package = "github.com/cline/grpc-go/cline";
|
||||||
|
|
||||||
service StateService {
|
service StateService {
|
||||||
rpc getLatestState(EmptyRequest) returns (State);
|
rpc getLatestState(EmptyRequest) returns (State);
|
||||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||||
|
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||||
@@ -18,6 +19,7 @@ service StateService {
|
|||||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||||
|
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
message State {
|
message State {
|
||||||
@@ -56,7 +58,6 @@ enum OpenaiReasoningEffort {
|
|||||||
LOW = 0;
|
LOW = 0;
|
||||||
MEDIUM = 1;
|
MEDIUM = 1;
|
||||||
HIGH = 2;
|
HIGH = 2;
|
||||||
MINIMAL = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum McpDisplayMode {
|
enum McpDisplayMode {
|
||||||
@@ -107,16 +108,6 @@ message TelemetrySettingRequest {
|
|||||||
TelemetrySettingEnum setting = 2;
|
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 for updating settings
|
||||||
message UpdateSettingsRequest {
|
message UpdateSettingsRequest {
|
||||||
Metadata metadata = 1;
|
Metadata metadata = 1;
|
||||||
@@ -137,9 +128,6 @@ message UpdateSettingsRequest {
|
|||||||
optional FocusChainSettings focus_chain_settings = 17;
|
optional FocusChainSettings focus_chain_settings = 17;
|
||||||
optional bool use_auto_condense = 18;
|
optional bool use_auto_condense = 18;
|
||||||
optional string custom_prompt = 19;
|
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
|
// Complete API Configuration message
|
||||||
@@ -151,10 +139,10 @@ message ApiConfiguration {
|
|||||||
optional string lite_llm_base_url = 4;
|
optional string lite_llm_base_url = 4;
|
||||||
optional string lite_llm_api_key = 5;
|
optional string lite_llm_api_key = 5;
|
||||||
optional bool lite_llm_use_prompt_cache = 6;
|
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 anthropic_base_url = 8;
|
||||||
optional string open_router_api_key = 9;
|
optional string openrouter_api_key = 9;
|
||||||
optional string open_router_provider_sorting = 10;
|
optional string openrouter_provider_sorting = 10;
|
||||||
optional string aws_access_key = 11;
|
optional string aws_access_key = 11;
|
||||||
optional string aws_secret_key = 12;
|
optional string aws_secret_key = 12;
|
||||||
optional string aws_session_token = 13;
|
optional string aws_session_token = 13;
|
||||||
@@ -167,14 +155,14 @@ message ApiConfiguration {
|
|||||||
optional string claude_code_path = 20;
|
optional string claude_code_path = 20;
|
||||||
optional string vertex_project_id = 21;
|
optional string vertex_project_id = 21;
|
||||||
optional string vertex_region = 22;
|
optional string vertex_region = 22;
|
||||||
optional string open_ai_base_url = 23;
|
optional string openai_base_url = 23;
|
||||||
optional string open_ai_api_key = 24;
|
optional string openai_api_key = 24;
|
||||||
optional string ollama_base_url = 25;
|
optional string ollama_base_url = 25;
|
||||||
optional string ollama_api_options_ctx_num = 26;
|
optional string ollama_api_options_ctx_num = 26;
|
||||||
optional string lm_studio_base_url = 27;
|
optional string lm_studio_base_url = 27;
|
||||||
optional string gemini_api_key = 28;
|
optional string gemini_api_key = 28;
|
||||||
optional string gemini_base_url = 29;
|
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 deep_seek_api_key = 31;
|
||||||
optional string requesty_api_key = 32;
|
optional string requesty_api_key = 32;
|
||||||
optional string requesty_base_url = 33;
|
optional string requesty_base_url = 33;
|
||||||
@@ -210,65 +198,61 @@ message ApiConfiguration {
|
|||||||
optional string qwen_code_oauth_path = 63;
|
optional string qwen_code_oauth_path = 63;
|
||||||
optional string dify_api_key = 64;
|
optional string dify_api_key = 64;
|
||||||
optional string dify_base_url = 65;
|
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
|
// 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 string plan_mode_api_model_id = 101;
|
||||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||||
optional string plan_mode_reasoning_effort = 103;
|
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 bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
|
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
|
||||||
optional string plan_mode_open_router_model_id = 107;
|
optional string plan_mode_openrouter_model_id = 107;
|
||||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
|
optional string plan_mode_openrouter_model_info = 108; // JSON string
|
||||||
optional string plan_mode_open_ai_model_id = 109;
|
optional string plan_mode_openai_model_id = 109;
|
||||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
|
optional string plan_mode_openai_model_info = 110; // JSON string
|
||||||
optional string plan_mode_ollama_model_id = 111;
|
optional string plan_mode_ollama_model_id = 111;
|
||||||
optional string plan_mode_lm_studio_model_id = 112;
|
optional string plan_mode_lm_studio_model_id = 112;
|
||||||
optional string plan_mode_lite_llm_model_id = 113;
|
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 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_together_model_id = 117;
|
||||||
optional string plan_mode_fireworks_model_id = 118;
|
optional string plan_mode_fireworks_model_id = 118;
|
||||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||||
optional string plan_mode_huawei_cloud_maas_model_id = 120;
|
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 string plan_mode_vercel_ai_gateway_model_id = 122;
|
||||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 123;
|
optional string plan_mode_vercel_ai_gateway_model_info = 123;
|
||||||
optional string plan_mode_oca_model_id = 124;
|
|
||||||
optional OcaModelInfo plan_mode_oca_model_info = 125;
|
|
||||||
|
|
||||||
// Act mode configurations
|
// 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 string act_mode_api_model_id = 201;
|
||||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||||
optional string act_mode_reasoning_effort = 203;
|
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 bool act_mode_aws_bedrock_custom_selected = 205;
|
||||||
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
|
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
|
||||||
optional string act_mode_open_router_model_id = 207;
|
optional string act_mode_openrouter_model_id = 207;
|
||||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
|
optional string act_mode_openrouter_model_info = 208; // JSON string
|
||||||
optional string act_mode_open_ai_model_id = 209;
|
optional string act_mode_openai_model_id = 209;
|
||||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
|
optional string act_mode_openai_model_info = 210; // JSON string
|
||||||
optional string act_mode_ollama_model_id = 211;
|
optional string act_mode_ollama_model_id = 211;
|
||||||
optional string act_mode_lm_studio_model_id = 212;
|
optional string act_mode_lm_studio_model_id = 212;
|
||||||
optional string act_mode_lite_llm_model_id = 213;
|
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 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_together_model_id = 217;
|
||||||
optional string act_mode_fireworks_model_id = 218;
|
optional string act_mode_fireworks_model_id = 218;
|
||||||
optional string act_mode_sap_ai_core_model_id = 219;
|
optional string act_mode_sap_ai_core_model_id = 219;
|
||||||
optional string act_mode_huawei_cloud_maas_model_id = 220;
|
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 string act_mode_vercel_ai_gateway_model_id = 222;
|
||||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 223;
|
optional string act_mode_vercel_ai_gateway_model_info = 223;
|
||||||
optional string act_mode_oca_model_id = 224;
|
|
||||||
optional OcaModelInfo act_mode_oca_model_info = 225;
|
// Favorited model IDs
|
||||||
|
repeated string favorited_model_ids = 300;
|
||||||
|
|
||||||
// Extension fields for Bedrock Api Keys
|
// Extension fields for Bedrock Api Keys
|
||||||
optional string aws_authentication = 301;
|
optional string aws_authentication = 301;
|
||||||
@@ -286,11 +270,12 @@ message FocusChainSettings {
|
|||||||
int32 remind_cline_interval = 2;
|
int32 remind_cline_interval = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Viewport {
|
|
||||||
int32 width = 1;
|
|
||||||
int32 height = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UpdateTerminalConnectionTimeoutResponse {
|
message UpdateTerminalConnectionTimeoutResponse {
|
||||||
optional int32 timeout_ms = 1;
|
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 { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
|
||||||
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
|
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
|
||||||
import { WebviewProviderType } from "@shared/webview/types"
|
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 { initialize, tearDown } from "@/common"
|
||||||
|
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
|
||||||
import { WebviewProvider } from "@/core/webview"
|
import { WebviewProvider } from "@/core/webview"
|
||||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||||
import { HostProvider } from "@/hosts/host-provider"
|
import { HostProvider } from "@/hosts/host-provider"
|
||||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||||
import { waitForHostBridgeReady } from "./hostbridge-client"
|
import { checkPortAvailability } from "./port-checker"
|
||||||
import { startProtobusService } from "./protobus-service"
|
import { startProtobusService, waitForHostBridgeReady } from "./protobus-service"
|
||||||
import { log } from "./utils"
|
import { log, SETTINGS_SUBFOLDER } from "./utils"
|
||||||
import { DATA_DIR, EXTENSION_DIR, extensionContext } from "./vscode-context"
|
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() {
|
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("\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.
|
// Check port availability before proceeding
|
||||||
setupHostProvider()
|
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
|
// Set up global error handlers to prevent process crashes
|
||||||
setupGlobalErrorHandlers()
|
setupGlobalErrorHandlers(lockManager)
|
||||||
|
|
||||||
const webviewProvider = await initialize(extensionContext)
|
const webviewProvider = await initialize(extensionContext)
|
||||||
|
|
||||||
// Enable the localhost HTTP server that handles auth redirects.
|
|
||||||
AuthHandler.getInstance().setEnabled(true)
|
AuthHandler.getInstance().setEnabled(true)
|
||||||
|
|
||||||
startProtobusService(webviewProvider.controller)
|
startProtobusService(webviewProvider.controller)
|
||||||
|
|
||||||
|
// Mark instance healthy after services are up
|
||||||
|
try {
|
||||||
|
lockManager?.touchInstance()
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupHostProvider() {
|
function setupHostProvider(extensionContext: any) {
|
||||||
const createWebview = (_: WebviewProviderType): WebviewProvider => {
|
const createWebview = (_: WebviewProviderType): WebviewProvider => {
|
||||||
return new ExternalWebviewProvider(extensionContext, WebviewProviderType.SIDEBAR)
|
return new ExternalWebviewProvider(extensionContext, WebviewProviderType.SIDEBAR)
|
||||||
}
|
}
|
||||||
const createDiffView = (): DiffViewProvider => {
|
const createDiffView = (): DiffViewProvider => {
|
||||||
return new ExternalDiffViewProvider()
|
return new ExternalDiffViewProvider()
|
||||||
}
|
}
|
||||||
const getCallbackUrl = (): Promise<string> => {
|
const getCallbackUri = (): Promise<string> => {
|
||||||
return AuthHandler.getInstance().getCallbackUrl()
|
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(
|
HostProvider.initialize(createWebview, createDiffView, new ExternalHostBridgeClientManager(), log, getCallbackUri)
|
||||||
createWebview,
|
|
||||||
createDiffView,
|
|
||||||
new ExternalHostBridgeClientManager(),
|
|
||||||
log,
|
|
||||||
getCallbackUrl,
|
|
||||||
getBinaryLocation,
|
|
||||||
EXTENSION_DIR,
|
|
||||||
DATA_DIR,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up global error handlers to prevent the process from crashing
|
* Sets up global error handlers to prevent the process from crashing
|
||||||
* on unhandled exceptions and promise rejections
|
* on unhandled exceptions and promise rejections
|
||||||
*/
|
*/
|
||||||
function setupGlobalErrorHandlers() {
|
function setupGlobalErrorHandlers(lockManager?: SqliteLockManager) {
|
||||||
// Handle unhandled exceptions
|
// Handle unhandled exceptions
|
||||||
process.on("uncaughtException", (error: Error) => {
|
process.on("uncaughtException", (error: Error) => {
|
||||||
log(`ERROR: Uncaught exception: ${error.message}`)
|
log(`ERROR: Uncaught exception: ${error.message}`)
|
||||||
@@ -86,15 +231,74 @@ function setupGlobalErrorHandlers() {
|
|||||||
// Graceful shutdown handlers
|
// Graceful shutdown handlers
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
log("Received SIGINT, shutting down gracefully...")
|
log("Received SIGINT, shutting down gracefully...")
|
||||||
process.exit(0)
|
shutdownGracefully(lockManager)
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
log("Received SIGTERM, shutting down gracefully...")
|
log("Received SIGTERM, shutting down gracefully...")
|
||||||
tearDown()
|
shutdownGracefully(lockManager)
|
||||||
|
|
||||||
process.exit(0)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
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 * as health from "grpc-health-check"
|
||||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
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 log = (...args: unknown[]) => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const year = now.getFullYear()
|
const year = now.getFullYear()
|
||||||
@@ -20,6 +23,7 @@ const log = (...args: unknown[]) => {
|
|||||||
|
|
||||||
function getPackageDefinition() {
|
function getPackageDefinition() {
|
||||||
// Load service definitions.
|
// 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 descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
|
||||||
const options = { longs: Number } // Encode int64 fields as numbers
|
const options = { longs: Number } // Encode int64 fields as numbers
|
||||||
const descriptorDefs = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet, options)
|
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 { mkdirSync } from "fs"
|
||||||
import os from "os"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { Extension, ExtensionContext } from "vscode"
|
import type { Extension, ExtensionContext } from "vscode"
|
||||||
import { ExtensionKind, ExtensionMode } from "vscode"
|
import { ExtensionKind, ExtensionMode } from "vscode"
|
||||||
import { URI } from "vscode-uri"
|
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 { 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)
|
function getPackageVersion(): string {
|
||||||
log(`CLINE_ENVIRONMENT: ${process.env.CLINE_ENVIRONMENT}`)
|
// Use build-time injected version (only method)
|
||||||
|
return process.env.CLINE_VERSION || "unknown"
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensionContext: ExtensionContext = {
|
const VERSION = getPackageVersion()
|
||||||
extension: extension,
|
log("Running standalone cline ", VERSION)
|
||||||
extensionMode: EXTENSION_MODE,
|
|
||||||
|
|
||||||
// Set up KV stores.
|
function createExtensionContext(clineDir: string): ExtensionContext {
|
||||||
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
|
const DATA_DIR = path.join(clineDir, "data")
|
||||||
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
|
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.
|
const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
|
||||||
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
|
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
|
||||||
storagePath: WORKSPACE_STORAGE_DIR, // Deprecated, not used in cline.
|
|
||||||
globalStorageUri: URI.file(DATA_DIR),
|
|
||||||
globalStoragePath: DATA_DIR, // Deprecated, not used in cline.
|
|
||||||
|
|
||||||
// Logs are global per extension, not per workspace.
|
// Static package.json data for extension context (no filesystem reading)
|
||||||
logUri: URI.file(DATA_DIR),
|
function getExtensionPackageJson(): any {
|
||||||
logPath: DATA_DIR, // Deprecated, not used in cline.
|
return {
|
||||||
|
name: "claude-dev",
|
||||||
|
displayName: "Cline",
|
||||||
|
version: VERSION,
|
||||||
|
publisher: "saoudrizwan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extensionUri: URI.file(EXTENSION_DIR),
|
const extension: Extension<void> = {
|
||||||
extensionPath: EXTENSION_DIR, // Deprecated, not used in cline.
|
id: "saoudrizwan.claude-dev",
|
||||||
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
|
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.
|
// Set up URIs.
|
||||||
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
|
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...")
|
console.log("Finished loading vscode context...")
|
||||||
|
|
||||||
export { extensionContext }
|
export { createExtensionContext }
|
||||||
|
|||||||
Reference in New Issue
Block a user