mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ddc8c846 | |||
| 386ded5126 | |||
| 44e15319e4 | |||
| e424b28702 | |||
| db9971890e | |||
| d7cc9b6155 | |||
| 05042d3ff7 | |||
| dc2c662de6 |
@@ -33,7 +33,7 @@ permissions:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
working-directory: .
|
||||
|
||||
jobs:
|
||||
publish-main:
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
fi
|
||||
|
||||
VERSION="${TAG#cli-v}"
|
||||
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
PACKAGE_VERSION=$(node -p "require('./sdk/apps/cli/package.json').version")
|
||||
|
||||
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
|
||||
echo "sdk/apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
)
|
||||
|
||||
for package_name in "${EXPECTED[@]}"; do
|
||||
dir="apps/cli/dist/${package_name#@cline/}"
|
||||
dir="sdk/apps/cli/dist/${package_name#@cline/}"
|
||||
if [ ! -f "$dir/package.json" ]; then
|
||||
echo "Missing package manifest: $dir/package.json"
|
||||
exit 1
|
||||
@@ -207,8 +207,8 @@ jobs:
|
||||
- name: Get Changelog Entry
|
||||
id: changelog
|
||||
run: |
|
||||
# Grab content between the first "## " header and the next one in apps/cli/CHANGELOG.md
|
||||
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' apps/cli/CHANGELOG.md)
|
||||
# Grab content between the first "## " header and the next one in sdk/apps/cli/CHANGELOG.md
|
||||
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' sdk/apps/cli/CHANGELOG.md)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
@@ -349,7 +349,7 @@ jobs:
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
BASE_VERSION=$(node -p "require('./sdk/apps/cli/package.json').version")
|
||||
TIMESTAMP=$(date +%s)
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
@@ -365,12 +365,12 @@ jobs:
|
||||
run: |
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const path = "apps/cli/package.json";
|
||||
const path = "sdk/apps/cli/package.json";
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
pkg.version = process.env.VERSION;
|
||||
fs.writeFileSync(path, `${JSON.stringify(pkg, null, "\t")}\n`);
|
||||
'
|
||||
cat apps/cli/package.json | grep '"version"'
|
||||
cat sdk/apps/cli/package.json | grep '"version"'
|
||||
|
||||
- name: Build platform binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
)
|
||||
|
||||
for package_name in "${EXPECTED[@]}"; do
|
||||
dir="apps/cli/dist/${package_name#@cline/}"
|
||||
dir="sdk/apps/cli/dist/${package_name#@cline/}"
|
||||
if [ ! -f "$dir/package.json" ]; then
|
||||
echo "Missing package manifest: $dir/package.json"
|
||||
exit 1
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
working-directory: .
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
env:
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./packages/llms/package.json').version")
|
||||
BASE_VERSION=$(node -p "require('./sdk/packages/llms/package.json').version")
|
||||
|
||||
if [ "$CHANNEL" = "nightly" ]; then
|
||||
TIMESTAMP=$(date +%s)
|
||||
@@ -166,11 +166,11 @@ jobs:
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: bun scripts/version.ts "$VERSION"
|
||||
run: bun sdk/scripts/version.ts "$VERSION"
|
||||
|
||||
- name: Verify publishability
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun scripts/check-publish.ts
|
||||
run: bun sdk/scripts/check-publish.ts
|
||||
|
||||
- name: Prepare package tarball directory
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/shared@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/shared
|
||||
cd sdk/packages/shared
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/llms@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/llms
|
||||
cd sdk/packages/llms
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/agents@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/agents
|
||||
cd sdk/packages/agents
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/core@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/core
|
||||
cd sdk/packages/core
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/sdk@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/sdk
|
||||
cd sdk/packages/sdk
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
working-directory: .
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
@@ -96,12 +96,12 @@ jobs:
|
||||
|
||||
- name: Run SDK Tests (Windows)
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
|
||||
run: bun -F './packages/**' test
|
||||
run: bun -F './sdk/packages/**' test
|
||||
|
||||
- name: Smoke test SQLite under Node
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
|
||||
timeout-minutes: 10
|
||||
run: bun scripts/ci-node-smoke.ts
|
||||
run: bun sdk/scripts/ci-node-smoke.ts
|
||||
|
||||
- name: Run TUI e2e tests
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
@@ -109,4 +109,4 @@ jobs:
|
||||
|
||||
- name: Verify packages are publishable
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
run: bun scripts/check-publish.ts
|
||||
run: bun sdk/scripts/check-publish.ts
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
if ! command -v gitleaks >/dev/null 2>&1; then
|
||||
echo "gitleaks is required for the pre-commit secret scan."
|
||||
echo "Install it with: brew install gitleaks"
|
||||
echo "Other install options: https://github.com/gitleaks/gitleaks#installing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gitleaks git --pre-commit --redact --staged --verbose || exit 1
|
||||
|
||||
lint-staged
|
||||
Vendored
+22
-21
@@ -5,8 +5,8 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"type": "shell",
|
||||
"command": "npm run compile-standalone",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
"script": "protos",
|
||||
"type": "shell",
|
||||
"command": "npm run protos",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"presentation": {
|
||||
@@ -64,8 +64,8 @@
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
"type": "shell",
|
||||
"command": "npm run build:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
@@ -85,8 +85,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview:test",
|
||||
"type": "shell",
|
||||
"command": "npm run build:webview:test",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
@@ -107,8 +107,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
"type": "shell",
|
||||
"command": "npm run dev:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
{
|
||||
@@ -144,8 +144,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:esbuild",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -183,8 +183,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:esbuild:test",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -223,8 +223,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:tsc",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:tsc",
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
@@ -241,8 +241,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"type": "shell",
|
||||
"command": "npm run watch-tests",
|
||||
"label": "npm: watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": [
|
||||
@@ -280,8 +281,8 @@
|
||||
"command": "rm -rf ${workspaceFolder}/apps/vscode/dist/tmp/user && mkdir -p ${workspaceFolder}/apps/vscode/dist/tmp/user"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "storybook",
|
||||
"type": "shell",
|
||||
"command": "npm run storybook",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
@@ -308,7 +309,7 @@
|
||||
"$tsc"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/sdk"
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Generated
+155
-100
@@ -54,7 +54,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"axios": "1.15.2",
|
||||
"axios": "1.16.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
@@ -102,7 +102,7 @@
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"ulid": "^2.4.0",
|
||||
"undici": "^7.16.0",
|
||||
"undici": "^7.26.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vscode-uri": "^3.1.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
@@ -1245,13 +1245,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.15",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz",
|
||||
"integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==",
|
||||
"version": "3.972.26",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz",
|
||||
"integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.13.1",
|
||||
"fast-xml-parser": "5.5.8",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"fast-xml-parser": "5.7.3",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2830,9 +2830,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
|
||||
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
|
||||
"version": "1.19.14",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
||||
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
@@ -3295,6 +3295,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
|
||||
"integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodable"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -5405,9 +5417,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/prompt-registry/node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
@@ -6168,9 +6180,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
|
||||
"integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
|
||||
"version": "4.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz",
|
||||
"integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -7119,9 +7131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -7462,9 +7474,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/test-cli/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7784,9 +7796,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7931,9 +7943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.11",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
|
||||
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -8376,16 +8388,42 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
||||
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
|
||||
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
@@ -8548,9 +8586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
|
||||
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
|
||||
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -8754,9 +8792,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -11131,12 +11169,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
|
||||
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -11240,9 +11278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -11256,9 +11294,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
|
||||
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -11267,13 +11305,14 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"xml-naming": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
|
||||
"integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==",
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz",
|
||||
"integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -11282,9 +11321,10 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.0"
|
||||
"@nodable/entities": "^2.1.0",
|
||||
"fast-xml-builder": "^1.1.7",
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"strnum": "^2.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -11517,9 +11557,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -12098,9 +12138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -12393,9 +12433,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.9",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
|
||||
"integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
|
||||
"version": "4.12.23",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
|
||||
"integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -12675,9 +12715,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -15359,9 +15399,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -16893,9 +16933,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
|
||||
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
|
||||
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -16954,9 +16994,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -17485,9 +17525,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -17670,9 +17710,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -17912,9 +17952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -19347,9 +19387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
|
||||
"integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -19631,9 +19671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -19718,9 +19758,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
@@ -20178,9 +20218,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz",
|
||||
"integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz",
|
||||
"integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
@@ -21512,9 +21552,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -21562,6 +21602,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-naming": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
|
||||
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
|
||||
@@ -531,7 +531,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"axios": "1.15.2",
|
||||
"axios": "1.16.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
@@ -579,7 +579,7 @@
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"ulid": "^2.4.0",
|
||||
"undici": "^7.16.0",
|
||||
"undici": "^7.26.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vscode-uri": "^3.1.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
|
||||
@@ -12,6 +12,12 @@ describe("TaskCancel Hook", () => {
|
||||
let getEnv: () => { tempDir: string }
|
||||
let hookTestEnv: HookTestEnv
|
||||
|
||||
// On Windows, hooks execute via a PowerShell bridge that spawns a child
|
||||
// Node process. That double-process startup is slow and variable on CI and
|
||||
// can easily exceed Mocha's default 2 s timeout, so spawning tests opt into a
|
||||
// larger timeout. Mirrors taskresume/hook-factory/user-prompt-submit tests.
|
||||
const WINDOWS_HOOK_TEST_TIMEOUT_MS = 15000
|
||||
|
||||
const writeHookScript = async (hookPath: string, nodeScript: string): Promise<void> => {
|
||||
await writeHookScriptForPlatform(hookPath, nodeScript)
|
||||
}
|
||||
@@ -29,7 +35,11 @@ describe("TaskCancel Hook", () => {
|
||||
})
|
||||
|
||||
describe("Hook Input Format", () => {
|
||||
it("should receive task metadata with completionStatus", async () => {
|
||||
it("should receive task metadata with completionStatus", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -61,7 +71,11 @@ console.log(JSON.stringify({
|
||||
// Note: contextModification is ignored for TaskCancel hooks
|
||||
})
|
||||
|
||||
it("should handle 'abandoned' completion status", async () => {
|
||||
it("should handle 'abandoned' completion status", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -96,7 +110,11 @@ console.log(JSON.stringify({
|
||||
// Note: contextModification is ignored for TaskCancel hooks
|
||||
})
|
||||
|
||||
it("should receive all common hook input fields", async () => {
|
||||
it("should receive all common hook input fields", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -136,7 +154,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Fire-and-Forget Behavior", () => {
|
||||
it("should ignore contextModification regardless of content", async () => {
|
||||
it("should ignore contextModification regardless of content", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.log(JSON.stringify({
|
||||
@@ -192,7 +214,11 @@ console.log(JSON.stringify({
|
||||
// The contextModification value is different but behavior is identical (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should succeed regardless of hook return value", async () => {
|
||||
it("should succeed regardless of hook return value", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
// Note: contextModification is ignored for TaskCancel hooks
|
||||
@@ -222,7 +248,11 @@ console.log(JSON.stringify({
|
||||
result.cancel.should.be.false()
|
||||
})
|
||||
|
||||
it("should return error message when hook returns cancel: true", async () => {
|
||||
it("should return error message when hook returns cancel: true", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.log(JSON.stringify({
|
||||
@@ -254,7 +284,11 @@ console.log(JSON.stringify({
|
||||
result.errorMessage?.should.equal("Hook tried to block cancellation")
|
||||
})
|
||||
|
||||
it("should execute without errors for cleanup purposes", async () => {
|
||||
it("should execute without errors for cleanup purposes", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -288,7 +322,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should surface hook errors to the user", async () => {
|
||||
it("should surface hook errors to the user", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.error("Hook execution error");
|
||||
@@ -317,7 +355,11 @@ process.exit(1);`
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle malformed JSON output from hook", async () => {
|
||||
it("should handle malformed JSON output from hook", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.log("not valid json")`
|
||||
@@ -359,7 +401,11 @@ console.log("not valid json")`
|
||||
stubHookDirs(sandbox, [globalHooksDir, workspaceHooksDir])
|
||||
})
|
||||
|
||||
it("should execute both global and workspace TaskCancel hooks", async () => {
|
||||
it("should execute both global and workspace TaskCancel hooks", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
// Create global hook
|
||||
const globalHookPath = path.join(globalHooksDir, "TaskCancel")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
@@ -399,7 +445,11 @@ console.log(JSON.stringify({
|
||||
// Both hooks executed successfully
|
||||
})
|
||||
|
||||
it("should execute both hooks with different completion statuses", async () => {
|
||||
it("should execute both hooks with different completion statuses", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const globalHookPath = path.join(globalHooksDir, "TaskCancel")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -441,7 +491,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("No Hook Behavior", () => {
|
||||
it("should succeed when no hook exists", async () => {
|
||||
it("should succeed when no hook exists", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskCancel")
|
||||
|
||||
@@ -461,7 +515,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Fixture-Based Tests", () => {
|
||||
it("should handle cancel: true with no error message", async () => {
|
||||
it("should handle cancel: true with no error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/false-no-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -484,7 +542,11 @@ console.log(JSON.stringify({
|
||||
// Cancellation still proceeds (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should handle cancel: true with error message", async () => {
|
||||
it("should handle cancel: true with error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/false-with-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -507,7 +569,11 @@ console.log(JSON.stringify({
|
||||
// Cancellation still proceeds (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should handle cancel: false with no error message", async () => {
|
||||
it("should handle cancel: false with no error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/true-no-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -529,7 +595,11 @@ console.log(JSON.stringify({
|
||||
// Normal success case - no errors to surface
|
||||
})
|
||||
|
||||
it("should handle cancel: false with error message", async () => {
|
||||
it("should handle cancel: false with error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/true-with-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -553,7 +623,11 @@ console.log(JSON.stringify({
|
||||
// Cancellation still proceeds (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should handle hook that exits with non-zero status code", async () => {
|
||||
it("should handle hook that exits with non-zero status code", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
|
||||
@@ -155,8 +155,7 @@ async function resolveRipgrepPath(): Promise<string> {
|
||||
|
||||
async function findSystemRipgrep(): Promise<string> {
|
||||
const fallback = process.platform === "win32" ? "rg.exe" : "rg"
|
||||
const candidates =
|
||||
process.platform === "win32" ? [] : ["/usr/bin/rg", "/opt/homebrew/bin/rg", "/usr/local/bin/rg"]
|
||||
const candidates = process.platform === "win32" ? [] : ["/usr/bin/rg", "/opt/homebrew/bin/rg", "/usr/local/bin/rg"]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
|
||||
@@ -145,9 +145,8 @@ export function mockFetchForTesting<T>(theFetch: typeof globalThis.fetch, callba
|
||||
return result.finally(() => {
|
||||
mockFetch = originalMockFetch
|
||||
}) as typeof result
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
if (willResetSync) {
|
||||
mockFetch = originalMockFetch
|
||||
@@ -171,10 +170,17 @@ export function mockFetchForTesting<T>(theFetch: typeof globalThis.fetch, callba
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function getAxiosSettings(): { adapter?: any; fetch?: typeof globalThis.fetch } {
|
||||
export function getAxiosSettings(): {
|
||||
adapter?: any
|
||||
fetch?: typeof globalThis.fetch
|
||||
maxBodyLength?: number
|
||||
maxContentLength?: number
|
||||
} {
|
||||
return {
|
||||
adapter: "fetch" as any,
|
||||
fetch, // Use our configured fetch
|
||||
maxBodyLength: Number.POSITIVE_INFINITY,
|
||||
maxContentLength: Number.POSITIVE_INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-39
@@ -17,9 +17,9 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/cli": {
|
||||
"sdk/apps/cli": {
|
||||
"name": "@cline/cli",
|
||||
"version": "3.0.14",
|
||||
"version": "3.0.15",
|
||||
"bin": {
|
||||
"cline": "src/index.ts",
|
||||
},
|
||||
@@ -32,6 +32,7 @@
|
||||
"@chat-adapter/telegram": "^4.23.0",
|
||||
"@chat-adapter/whatsapp": "^4.23.0",
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@cline/cline-hub": "workspace:*",
|
||||
"@gramio/format": "^0.7.0",
|
||||
"@opentui-ui/dialog": "^0.1.2",
|
||||
"@opentui/core": "0.1.102",
|
||||
@@ -56,7 +57,7 @@
|
||||
"@types/react": "19.2.14",
|
||||
},
|
||||
},
|
||||
"apps/cline-hub": {
|
||||
"sdk/apps/cline-hub": {
|
||||
"name": "@cline/cline-hub",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -65,7 +66,7 @@
|
||||
"@cline/shared": "workspace:*",
|
||||
},
|
||||
},
|
||||
"apps/cline-hub/src/webview": {
|
||||
"sdk/apps/cline-hub/src/webview": {
|
||||
"name": "@cline/cline-hub-webview",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -122,7 +123,7 @@
|
||||
"vite": "^8.0.0",
|
||||
},
|
||||
},
|
||||
"apps/examples/cli-agent": {
|
||||
"sdk/apps/examples/cli-agent": {
|
||||
"name": "@cline/example-cli-agent",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -133,7 +134,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/cline-core-cli-agent": {
|
||||
"sdk/apps/examples/cline-core-cli-agent": {
|
||||
"name": "@cline/example-cline-core-cli-agent",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -143,7 +144,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/code-review-bot": {
|
||||
"sdk/apps/examples/code-review-bot": {
|
||||
"name": "@cline/example-code-review-bot",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -154,7 +155,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/desktop-app": {
|
||||
"sdk/apps/examples/desktop-app": {
|
||||
"name": "@cline/code",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -229,7 +230,7 @@
|
||||
"typescript": "5.7.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/menubar": {
|
||||
"sdk/apps/examples/menubar": {
|
||||
"name": "@cline/menubar",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
@@ -243,7 +244,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/multi-agent": {
|
||||
"sdk/apps/examples/multi-agent": {
|
||||
"name": "@cline/example-multi-agent",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -253,7 +254,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/quickstart": {
|
||||
"sdk/apps/examples/quickstart": {
|
||||
"name": "@cline/example-quickstart",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -263,7 +264,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/vscode": {
|
||||
"sdk/apps/examples/vscode": {
|
||||
"name": "@cline/vscode",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -279,7 +280,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/vscode/src/webview": {
|
||||
"sdk/apps/examples/vscode/src/webview": {
|
||||
"name": "webview",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
@@ -336,7 +337,7 @@
|
||||
"vite": "^8.0.0",
|
||||
},
|
||||
},
|
||||
"examples": {
|
||||
"sdk/examples": {
|
||||
"name": "examples",
|
||||
"dependencies": {
|
||||
"@cline/core": "workspace:*",
|
||||
@@ -348,7 +349,7 @@
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
"examples/plugins/agents-squad": {
|
||||
"sdk/examples/plugins/agents-squad": {
|
||||
"name": "cline-agent-squad-plugin",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
@@ -362,7 +363,7 @@
|
||||
"@cline/core",
|
||||
],
|
||||
},
|
||||
"packages/agents": {
|
||||
"sdk/packages/agents": {
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.42",
|
||||
"dependencies": {
|
||||
@@ -371,7 +372,7 @@
|
||||
"nanoid": "^5.1.7",
|
||||
},
|
||||
},
|
||||
"packages/core": {
|
||||
"sdk/packages/core": {
|
||||
"name": "@cline/core",
|
||||
"version": "0.0.42",
|
||||
"dependencies": {
|
||||
@@ -402,7 +403,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
},
|
||||
},
|
||||
"packages/llms": {
|
||||
"sdk/packages/llms": {
|
||||
"name": "@cline/llms",
|
||||
"version": "0.0.42",
|
||||
"dependencies": {
|
||||
@@ -436,14 +437,14 @@
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
],
|
||||
},
|
||||
"packages/sdk": {
|
||||
"sdk/packages/sdk": {
|
||||
"name": "@cline/sdk",
|
||||
"version": "0.0.42",
|
||||
"dependencies": {
|
||||
"@cline/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/shared": {
|
||||
"sdk/packages/shared": {
|
||||
"name": "@cline/shared",
|
||||
"version": "0.0.42",
|
||||
"dependencies": {
|
||||
@@ -653,37 +654,37 @@
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.5.0", "", { "dependencies": { "@clack/core": "1.4.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA=="],
|
||||
|
||||
"@cline/agents": ["@cline/agents@workspace:packages/agents"],
|
||||
"@cline/agents": ["@cline/agents@workspace:sdk/packages/agents"],
|
||||
|
||||
"@cline/cli": ["@cline/cli@workspace:apps/cli"],
|
||||
"@cline/cli": ["@cline/cli@workspace:sdk/apps/cli"],
|
||||
|
||||
"@cline/cline-hub": ["@cline/cline-hub@workspace:apps/cline-hub"],
|
||||
"@cline/cline-hub": ["@cline/cline-hub@workspace:sdk/apps/cline-hub"],
|
||||
|
||||
"@cline/cline-hub-webview": ["@cline/cline-hub-webview@workspace:apps/cline-hub/src/webview"],
|
||||
"@cline/cline-hub-webview": ["@cline/cline-hub-webview@workspace:sdk/apps/cline-hub/src/webview"],
|
||||
|
||||
"@cline/code": ["@cline/code@workspace:apps/examples/desktop-app"],
|
||||
"@cline/code": ["@cline/code@workspace:sdk/apps/examples/desktop-app"],
|
||||
|
||||
"@cline/core": ["@cline/core@workspace:packages/core"],
|
||||
"@cline/core": ["@cline/core@workspace:sdk/packages/core"],
|
||||
|
||||
"@cline/example-cli-agent": ["@cline/example-cli-agent@workspace:apps/examples/cli-agent"],
|
||||
"@cline/example-cli-agent": ["@cline/example-cli-agent@workspace:sdk/apps/examples/cli-agent"],
|
||||
|
||||
"@cline/example-cline-core-cli-agent": ["@cline/example-cline-core-cli-agent@workspace:apps/examples/cline-core-cli-agent"],
|
||||
"@cline/example-cline-core-cli-agent": ["@cline/example-cline-core-cli-agent@workspace:sdk/apps/examples/cline-core-cli-agent"],
|
||||
|
||||
"@cline/example-code-review-bot": ["@cline/example-code-review-bot@workspace:apps/examples/code-review-bot"],
|
||||
"@cline/example-code-review-bot": ["@cline/example-code-review-bot@workspace:sdk/apps/examples/code-review-bot"],
|
||||
|
||||
"@cline/example-multi-agent": ["@cline/example-multi-agent@workspace:apps/examples/multi-agent"],
|
||||
"@cline/example-multi-agent": ["@cline/example-multi-agent@workspace:sdk/apps/examples/multi-agent"],
|
||||
|
||||
"@cline/example-quickstart": ["@cline/example-quickstart@workspace:apps/examples/quickstart"],
|
||||
"@cline/example-quickstart": ["@cline/example-quickstart@workspace:sdk/apps/examples/quickstart"],
|
||||
|
||||
"@cline/llms": ["@cline/llms@workspace:packages/llms"],
|
||||
"@cline/llms": ["@cline/llms@workspace:sdk/packages/llms"],
|
||||
|
||||
"@cline/menubar": ["@cline/menubar@workspace:apps/examples/menubar"],
|
||||
"@cline/menubar": ["@cline/menubar@workspace:sdk/apps/examples/menubar"],
|
||||
|
||||
"@cline/sdk": ["@cline/sdk@workspace:packages/sdk"],
|
||||
"@cline/sdk": ["@cline/sdk@workspace:sdk/packages/sdk"],
|
||||
|
||||
"@cline/shared": ["@cline/shared@workspace:packages/shared"],
|
||||
"@cline/shared": ["@cline/shared@workspace:sdk/packages/shared"],
|
||||
|
||||
"@cline/vscode": ["@cline/vscode@workspace:apps/examples/vscode"],
|
||||
"@cline/vscode": ["@cline/vscode@workspace:sdk/apps/examples/vscode"],
|
||||
|
||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||
|
||||
@@ -1795,7 +1796,7 @@
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"cline-agent-squad-plugin": ["cline-agent-squad-plugin@workspace:examples/plugins/agents-squad"],
|
||||
"cline-agent-squad-plugin": ["cline-agent-squad-plugin@workspace:sdk/examples/plugins/agents-squad"],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
@@ -2075,7 +2076,7 @@
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
|
||||
|
||||
"examples": ["examples@workspace:examples"],
|
||||
"examples": ["examples@workspace:sdk/examples"],
|
||||
|
||||
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
||||
|
||||
@@ -3199,7 +3200,7 @@
|
||||
|
||||
"web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
|
||||
|
||||
"webview": ["webview@workspace:apps/examples/vscode/src/webview"],
|
||||
"webview": ["webview@workspace:sdk/apps/examples/vscode/src/webview"],
|
||||
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@cline/packages",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"sdk/packages/*",
|
||||
"sdk/apps/*",
|
||||
"sdk/apps/cline-hub/src/webview",
|
||||
"sdk/apps/examples/*",
|
||||
"sdk/apps/examples/vscode/src/webview",
|
||||
"sdk/examples",
|
||||
"sdk/examples/plugins/*"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"build": "bun run clean && bun install && bun run build:sdk && bun -F @cline/cli build",
|
||||
"build:sdk": "bun --production -F './sdk/packages/*' build",
|
||||
"build:apps": "bun -F './sdk/apps/**' --production build",
|
||||
"build:models": "bun -F @cline/llms generate:models && bun format --write",
|
||||
"dev": "bun --conditions=development run build:sdk && bun run cli && bun run cli hub stop",
|
||||
"cli": "bun --conditions=development --cwd sdk/apps/cli dev",
|
||||
"code": "bun --conditions=development -F @cline/code dev",
|
||||
"clean": "bun run sdk/scripts/clean.ts",
|
||||
"types": "bun --parallel -F '*' typecheck",
|
||||
"test": "bun --parallel -F './sdk/packages/**' -F './sdk/apps/cli' test",
|
||||
"test:unit": "bash -lc 'set -euo pipefail; bun -F @cline/agents test & p1=$!; bun -F @cline/llms test & p2=$!; bun -F @cline/core test:unit & p3=$!; bun -F @cline/cli test:unit & p4=$!; wait $p1; wait $p2; wait $p3; wait $p4'",
|
||||
"test:e2e": "bun -F @cline/core test:e2e && bun -F @cline/cli test:e2e",
|
||||
"test:e2e:interactive": "bun -F @cline/cli test:e2e:interactive",
|
||||
"verify:routines": "zsh -lc 'cd sdk/packages/core && bunx vitest run src/cron/schedule-service.test.ts --config vitest.config.ts'",
|
||||
"verify:workos-device-auth": "bun sdk/scripts/verify-workos-device-auth.ts",
|
||||
"biome": "bunx --bun @biomejs/biome",
|
||||
"format": "bun biome format sdk/",
|
||||
"lint": "bun biome lint sdk/",
|
||||
"fix": "bun biome check --write --unsafe --diagnostic-level=error sdk/",
|
||||
"check": "bun biome check --diagnostic-level=error sdk/ && bun run build:sdk && bun run -F @cline/cli build && bun --parallel -F './sdk/packages/**' -F @cline/cli typecheck && bun sdk/scripts/check-publish.ts",
|
||||
"version": "bun run types && bun sdk/scripts/version.ts",
|
||||
"release": "bun sdk/scripts/release.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"sdk/**": [
|
||||
"sh -c 'bun run types'",
|
||||
"bun biome check --no-errors-on-unmatched --files-ignore-unknown=true"
|
||||
]
|
||||
},
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": "1.3.13",
|
||||
"node": ">=22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/node": "^25.3.5",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.3.2",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nanoid": "^5.1.7",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "bun@1.3.13"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
if ! command -v gitleaks >/dev/null 2>&1; then
|
||||
echo "gitleaks is required for the pre-commit secret scan."
|
||||
echo "Install it with: brew install gitleaks"
|
||||
echo "Other install options: https://github.com/gitleaks/gitleaks#installing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gitleaks git --pre-commit --redact --staged --verbose || exit 1
|
||||
lint-staged
|
||||
+59
-2
@@ -1,12 +1,65 @@
|
||||
import { copyFileSync, mkdirSync } from "node:fs";
|
||||
import {
|
||||
copyFileSync,
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { $ } from "bun";
|
||||
|
||||
function defineProcessEnv(name: string): string {
|
||||
return JSON.stringify(process.env[name] ?? "");
|
||||
}
|
||||
|
||||
const sourcemap = Bun.env.CLINE_SOURCEMAPS === "1" ? "linked" : "none";
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(rootDir, "../../..");
|
||||
const hubWebviewSourcePath = join(rootDir, "../cline-hub/src/webview");
|
||||
const hubWebviewDistPath = join(rootDir, "../cline-hub/dist/webview");
|
||||
const hubWebviewIndexPath = join(hubWebviewDistPath, "index.html");
|
||||
const cliHubWebviewDistPath = join(rootDir, "dist/cline-hub/webview");
|
||||
|
||||
function newestFileMtimeMs(dir: string): number {
|
||||
let newest = 0;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (
|
||||
entry.name === "node_modules" ||
|
||||
entry.name === "dist" ||
|
||||
entry.name === ".turbo"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
newest = Math.max(newest, newestFileMtimeMs(path));
|
||||
} else if (entry.isFile()) {
|
||||
newest = Math.max(newest, statSync(path).mtimeMs);
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
function shouldBuildHubWebview(): boolean {
|
||||
if (!existsSync(hubWebviewIndexPath)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return (
|
||||
newestFileMtimeMs(hubWebviewSourcePath) >
|
||||
statSync(hubWebviewIndexPath).mtimeMs
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBuildHubWebview()) {
|
||||
console.log("Building Cline Hub webview...");
|
||||
await $`bun -F @cline/cline-hub build:webview`.cwd(repoRoot);
|
||||
}
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
@@ -63,7 +116,6 @@ if (result.logs.length > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
const coreBootstrapPath = join(
|
||||
rootDir,
|
||||
"../../packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
|
||||
@@ -74,3 +126,8 @@ const cliBootstrapPath = join(
|
||||
);
|
||||
mkdirSync(dirname(cliBootstrapPath), { recursive: true });
|
||||
copyFileSync(coreBootstrapPath, cliBootstrapPath);
|
||||
|
||||
if (existsSync(hubWebviewDistPath)) {
|
||||
mkdirSync(dirname(cliHubWebviewDistPath), { recursive: true });
|
||||
cpSync(hubWebviewDistPath, cliHubWebviewDistPath, { recursive: true });
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"@chat-adapter/telegram": "^4.23.0",
|
||||
"@chat-adapter/whatsapp": "^4.23.0",
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@cline/cline-hub": "workspace:*",
|
||||
"@gramio/format": "^0.7.0",
|
||||
"@opentui-ui/dialog": "^0.1.2",
|
||||
"@opentui/core": "0.1.102",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { $ } from "bun";
|
||||
import {
|
||||
@@ -95,6 +103,48 @@ if (!buildOptions.skipSdkBuild) {
|
||||
await $`bun -F @cline/cli build`.cwd(rootDir);
|
||||
}
|
||||
|
||||
const hubWebviewSource = join(cliDir, "../cline-hub/src/webview");
|
||||
const hubWebviewDist = join(cliDir, "../cline-hub/dist/webview");
|
||||
const hubWebviewIndex = join(hubWebviewDist, "index.html");
|
||||
|
||||
function newestFileMtimeMs(dir: string): number {
|
||||
let newest = 0;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (
|
||||
entry.name === "node_modules" ||
|
||||
entry.name === "dist" ||
|
||||
entry.name === ".turbo"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
newest = Math.max(newest, newestFileMtimeMs(path));
|
||||
} else if (entry.isFile()) {
|
||||
newest = Math.max(newest, statSync(path).mtimeMs);
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
function shouldBuildHubWebview(): boolean {
|
||||
if (!existsSync(hubWebviewIndex)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return (
|
||||
newestFileMtimeMs(hubWebviewSource) > statSync(hubWebviewIndex).mtimeMs
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBuildHubWebview()) {
|
||||
console.log("Building Cline Hub webview...");
|
||||
await $`bun -F @cline/cline-hub build:webview`.cwd(rootDir);
|
||||
}
|
||||
|
||||
const binaries: Record<string, string> = {};
|
||||
|
||||
function findOpenTuiParserWorker(): string {
|
||||
@@ -218,6 +268,14 @@ for (const item of targets) {
|
||||
await Bun.write(join(bootstrapDir, "plugin-sandbox-bootstrap.js"), content);
|
||||
}
|
||||
|
||||
if (existsSync(hubWebviewDist)) {
|
||||
const hubWebviewDest = join(cliDir, `dist/${dirName}/cline-hub/webview`);
|
||||
mkdirSync(join(cliDir, `dist/${dirName}/cline-hub`), {
|
||||
recursive: true,
|
||||
});
|
||||
cpSync(hubWebviewDist, hubWebviewDest, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate platform package.json
|
||||
await Bun.write(
|
||||
join(cliDir, `dist/${dirName}/package.json`),
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||
import { arch, platform, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { runDashboardCommand, waitForProcessShutdown } from "./dashboard";
|
||||
|
||||
const ENV_KEYS = [
|
||||
"WORKSPACE_ROOT",
|
||||
"HOST",
|
||||
"CLINE_HUB_DASHBOARD_PORT",
|
||||
"PUBLIC_URL",
|
||||
"ROOM_SECRET",
|
||||
"CLINE_HUB_WEBVIEW_DIST_DIR",
|
||||
"CLINE_WRAPPER_PATH",
|
||||
] as const;
|
||||
|
||||
const originalEnv = Object.fromEntries(
|
||||
ENV_KEYS.map((key) => [key, process.env[key]]),
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
const value = originalEnv[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("runDashboardCommand", () => {
|
||||
it("starts the dashboard server, opens the invite URL, and waits for shutdown", async () => {
|
||||
const output: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const opened: string[] = [];
|
||||
const stop = vi.fn();
|
||||
let observedEnv:
|
||||
| {
|
||||
workspaceRoot: string | undefined;
|
||||
host: string | undefined;
|
||||
port: string | undefined;
|
||||
publicUrl: string | undefined;
|
||||
roomSecret: string | undefined;
|
||||
webviewDistDir: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
const webviewDistDir = mkdtempSync(join(tmpdir(), "cline-webview-dist-"));
|
||||
mkdirSync(webviewDistDir, { recursive: true });
|
||||
process.env.CLINE_HUB_WEBVIEW_DIST_DIR = webviewDistDir;
|
||||
|
||||
const exitCode = await runDashboardCommand({
|
||||
cwd: "sdk",
|
||||
host: "127.0.0.1",
|
||||
port: "9090",
|
||||
publicUrl: "http://127.0.0.1:9090",
|
||||
roomSecret: "secret",
|
||||
io: {
|
||||
writeln: (text) => output.push(text ?? ""),
|
||||
writeErr: (text) => errors.push(text),
|
||||
},
|
||||
startServer: async () => {
|
||||
observedEnv = {
|
||||
workspaceRoot: process.env.WORKSPACE_ROOT,
|
||||
host: process.env.HOST,
|
||||
port: process.env.CLINE_HUB_DASHBOARD_PORT,
|
||||
publicUrl: process.env.PUBLIC_URL,
|
||||
roomSecret: process.env.ROOM_SECRET,
|
||||
webviewDistDir: process.env.CLINE_HUB_WEBVIEW_DIST_DIR,
|
||||
};
|
||||
return {
|
||||
listenUrl: "http://127.0.0.1:9090/",
|
||||
publicUrl: "http://127.0.0.1:9090",
|
||||
inviteUrl: "http://127.0.0.1:9090/?roomSecret=secret",
|
||||
hubUrl: "ws://127.0.0.1:25463/hub",
|
||||
stop,
|
||||
};
|
||||
},
|
||||
openUrl: async (url) => {
|
||||
opened.push(url);
|
||||
},
|
||||
waitForShutdown: async (server) => {
|
||||
await server.stop();
|
||||
},
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(observedEnv).toEqual({
|
||||
workspaceRoot: resolve("sdk"),
|
||||
host: "127.0.0.1",
|
||||
port: "9090",
|
||||
publicUrl: "http://127.0.0.1:9090",
|
||||
roomSecret: "secret",
|
||||
webviewDistDir,
|
||||
});
|
||||
expect(opened).toEqual(["http://127.0.0.1:9090/?roomSecret=secret"]);
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
expect(output.join("\n")).toContain("Cline dashboard listening at");
|
||||
expect(output.join("\n")).toContain("ws://127.0.0.1:25463/hub");
|
||||
expect(errors).toEqual([]);
|
||||
expect(process.env.WORKSPACE_ROOT).toBe(originalEnv.WORKSPACE_ROOT);
|
||||
expect(process.env.CLINE_HUB_WEBVIEW_DIST_DIR).toBe(webviewDistDir);
|
||||
});
|
||||
|
||||
it("honors --no-open behavior", async () => {
|
||||
const openUrl = vi.fn();
|
||||
|
||||
const exitCode = await runDashboardCommand({
|
||||
openBrowser: false,
|
||||
io: {
|
||||
writeln: () => {},
|
||||
writeErr: () => {},
|
||||
},
|
||||
startServer: async () => ({
|
||||
listenUrl: "http://127.0.0.1:8787/",
|
||||
publicUrl: "http://127.0.0.1:8787",
|
||||
inviteUrl: "http://127.0.0.1:8787",
|
||||
stop: vi.fn(),
|
||||
}),
|
||||
openUrl,
|
||||
waitForShutdown: async () => {},
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(openUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("finds webview assets from the published wrapper package layout", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "cline-wrapper-layout-"));
|
||||
const wrapperPath = join(root, "node_modules", "cline", "bin", "cline");
|
||||
const platformName = platform() === "win32" ? "windows" : platform();
|
||||
const webviewDistDir = join(
|
||||
root,
|
||||
"node_modules",
|
||||
"cline",
|
||||
"node_modules",
|
||||
"@cline",
|
||||
`cli-${platformName}-${arch()}`,
|
||||
"cline-hub",
|
||||
"webview",
|
||||
);
|
||||
mkdirSync(join(wrapperPath, ".."), { recursive: true });
|
||||
mkdirSync(webviewDistDir, { recursive: true });
|
||||
process.env.CLINE_WRAPPER_PATH = wrapperPath;
|
||||
delete process.env.CLINE_HUB_WEBVIEW_DIST_DIR;
|
||||
let observedWebviewDistDir: string | undefined;
|
||||
|
||||
const exitCode = await runDashboardCommand({
|
||||
openBrowser: false,
|
||||
io: {
|
||||
writeln: () => {},
|
||||
writeErr: () => {},
|
||||
},
|
||||
startServer: async () => {
|
||||
observedWebviewDistDir = process.env.CLINE_HUB_WEBVIEW_DIST_DIR;
|
||||
return {
|
||||
listenUrl: "http://127.0.0.1:8787/",
|
||||
publicUrl: "http://127.0.0.1:8787",
|
||||
inviteUrl: "http://127.0.0.1:8787",
|
||||
stop: vi.fn(),
|
||||
};
|
||||
},
|
||||
waitForShutdown: async () => {},
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(observedWebviewDistDir).toBe(webviewDistDir);
|
||||
});
|
||||
|
||||
it("settles shutdown when server stop rejects", async () => {
|
||||
const shutdown = waitForProcessShutdown({
|
||||
listenUrl: "http://127.0.0.1:8787/",
|
||||
publicUrl: "http://127.0.0.1:8787",
|
||||
inviteUrl: "http://127.0.0.1:8787",
|
||||
stop: vi.fn(async () => {
|
||||
throw new Error("stop failed");
|
||||
}),
|
||||
});
|
||||
|
||||
process.emit("SIGINT", "SIGINT");
|
||||
|
||||
await expect(shutdown).rejects.toThrow("stop failed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { arch, platform } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import open from "open";
|
||||
import { c } from "../utils/output";
|
||||
|
||||
export interface DashboardServerHandle {
|
||||
listenUrl: string;
|
||||
publicUrl: string;
|
||||
inviteUrl: string;
|
||||
hubUrl?: string;
|
||||
stop: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
interface DashboardCommandIo {
|
||||
writeln: (text?: string) => void;
|
||||
writeErr: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface RunDashboardCommandOptions {
|
||||
cwd?: string;
|
||||
host?: string;
|
||||
port?: string;
|
||||
publicUrl?: string;
|
||||
roomSecret?: string;
|
||||
openBrowser?: boolean;
|
||||
io: DashboardCommandIo;
|
||||
startServer?: () => Promise<DashboardServerHandle>;
|
||||
openUrl?: (url: string) => Promise<void>;
|
||||
waitForShutdown?: (server: DashboardServerHandle) => Promise<void>;
|
||||
}
|
||||
|
||||
const DASHBOARD_PORT_ENV = "CLINE_HUB_DASHBOARD_PORT";
|
||||
const WEBVIEW_DIST_ENV = "CLINE_HUB_WEBVIEW_DIST_DIR";
|
||||
|
||||
function setEnvValue(name: string, value: string | undefined): () => void {
|
||||
const previous = process.env[name];
|
||||
if (value === undefined) {
|
||||
return () => {};
|
||||
}
|
||||
process.env[name] = value;
|
||||
return () => {
|
||||
if (previous === undefined) {
|
||||
delete process.env[name];
|
||||
} else {
|
||||
process.env[name] = previous;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function withDashboardEnvironment<T>(
|
||||
options: RunDashboardCommandOptions,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const restore = [
|
||||
setEnvValue(
|
||||
"WORKSPACE_ROOT",
|
||||
options.cwd ? resolve(options.cwd) : undefined,
|
||||
),
|
||||
setEnvValue("HOST", options.host),
|
||||
setEnvValue(DASHBOARD_PORT_ENV, options.port),
|
||||
setEnvValue("PUBLIC_URL", options.publicUrl),
|
||||
setEnvValue("ROOM_SECRET", options.roomSecret),
|
||||
setEnvValue(WEBVIEW_DIST_ENV, resolveDefaultWebviewDistDir()),
|
||||
];
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
for (let i = restore.length - 1; i >= 0; i--) {
|
||||
restore[i]?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDefaultWebviewDistDir(): string | undefined {
|
||||
if (process.env[WEBVIEW_DIST_ENV]?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
...resolveInstalledPlatformPackageWebviewCandidates(),
|
||||
// Source checkout: sdk/apps/cli/src/commands/dashboard.ts
|
||||
join(moduleDir, "../../../cline-hub/dist/webview"),
|
||||
// Node bundle: sdk/apps/cli/dist/index.js
|
||||
join(moduleDir, "cline-hub/webview"),
|
||||
// Compiled platform package: sdk/apps/cli/dist/<platform>/bin/cline
|
||||
join(dirname(process.execPath), "../cline-hub/webview"),
|
||||
];
|
||||
|
||||
return candidates.find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
function resolveInstalledPlatformPackageWebviewCandidates(): string[] {
|
||||
const packageName = resolvePlatformPackageName();
|
||||
const starts = [
|
||||
process.env.CLINE_WRAPPER_PATH
|
||||
? dirname(process.env.CLINE_WRAPPER_PATH)
|
||||
: undefined,
|
||||
dirname(process.execPath),
|
||||
].filter((value): value is string => !!value?.trim());
|
||||
const candidates: string[] = [];
|
||||
for (const start of starts) {
|
||||
let current = start;
|
||||
for (;;) {
|
||||
candidates.push(
|
||||
join(current, "node_modules", packageName, "cline-hub/webview"),
|
||||
);
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function resolvePlatformPackageName(): string {
|
||||
const platformName = platform() === "win32" ? "windows" : platform();
|
||||
return `@cline/cli-${platformName}-${arch()}`;
|
||||
}
|
||||
|
||||
async function startDefaultDashboardServer(): Promise<DashboardServerHandle> {
|
||||
const { startClineHubDashboardServer } = await import("@cline/cline-hub");
|
||||
return await startClineHubDashboardServer();
|
||||
}
|
||||
|
||||
async function openDefaultUrl(url: string): Promise<void> {
|
||||
await open(url, { wait: false });
|
||||
}
|
||||
|
||||
export function waitForProcessShutdown(
|
||||
server: DashboardServerHandle,
|
||||
): Promise<void> {
|
||||
return new Promise<void>((resolveShutdown, rejectShutdown) => {
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
process.off("SIGINT", handleSignal);
|
||||
process.off("SIGTERM", handleSignal);
|
||||
};
|
||||
|
||||
const stop = async () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
try {
|
||||
await server.stop();
|
||||
resolveShutdown();
|
||||
} catch (error) {
|
||||
rejectShutdown(error);
|
||||
}
|
||||
};
|
||||
|
||||
function handleSignal() {
|
||||
void stop();
|
||||
}
|
||||
|
||||
process.on("SIGINT", handleSignal);
|
||||
process.on("SIGTERM", handleSignal);
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDashboardCommand(
|
||||
options: RunDashboardCommandOptions,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const server = await withDashboardEnvironment(options, () =>
|
||||
(options.startServer ?? startDefaultDashboardServer)(),
|
||||
);
|
||||
const dashboardUrl =
|
||||
server.inviteUrl || server.publicUrl || server.listenUrl;
|
||||
options.io.writeln(
|
||||
`${c.green}Cline dashboard listening at${c.reset} ${dashboardUrl}`,
|
||||
);
|
||||
if (server.hubUrl) {
|
||||
options.io.writeln(`${c.dim}Hub endpoint: ${server.hubUrl}${c.reset}`);
|
||||
}
|
||||
|
||||
if (options.openBrowser !== false) {
|
||||
try {
|
||||
await (options.openUrl ?? openDefaultUrl)(dashboardUrl);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
options.io.writeErr(`Failed to open browser: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await (options.waitForShutdown ?? waitForProcessShutdown)(server);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
options.io.writeErr(error instanceof Error ? error.message : String(error));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1115,8 +1115,11 @@ class DiscordConnector extends ConnectorBase<
|
||||
resolveStop?.();
|
||||
};
|
||||
|
||||
|
||||
type ErrorTrackerEntry = { count: number; firstSeen: number; lastSeen: number };
|
||||
type ErrorTrackerEntry = {
|
||||
count: number;
|
||||
firstSeen: number;
|
||||
lastSeen: number;
|
||||
};
|
||||
const errorTracker = new Map<string, ErrorTrackerEntry>();
|
||||
const MAX_REPEATED_ERRORS = 3;
|
||||
const ERROR_WINDOW_MS = 60_000; // 1 minute
|
||||
@@ -1250,25 +1253,33 @@ class DiscordConnector extends ConnectorBase<
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
|
||||
// Track repeated errors per-thread with fixed time window
|
||||
const now = Date.now();
|
||||
const errorKey = `${thread.id}:${message.slice(0, 200)}`; // Per-thread error tracking
|
||||
const tracked = errorTracker.get(errorKey);
|
||||
|
||||
|
||||
if (!tracked) {
|
||||
// First occurrence, start tracking
|
||||
errorTracker.set(errorKey, { count: 1, firstSeen: now, lastSeen: now });
|
||||
errorTracker.set(errorKey, {
|
||||
count: 1,
|
||||
firstSeen: now,
|
||||
lastSeen: now,
|
||||
});
|
||||
await thread.post(`Discord bridge error: ${message}`);
|
||||
} else if (now - tracked.firstSeen > ERROR_WINDOW_MS) {
|
||||
// Outside fixed window, reset counter
|
||||
errorTracker.set(errorKey, { count: 1, firstSeen: now, lastSeen: now });
|
||||
errorTracker.set(errorKey, {
|
||||
count: 1,
|
||||
firstSeen: now,
|
||||
lastSeen: now,
|
||||
});
|
||||
await thread.post(`Discord bridge error: ${message}`);
|
||||
} else {
|
||||
// Within fixed window, increment counter
|
||||
tracked.count++;
|
||||
tracked.lastSeen = now;
|
||||
|
||||
|
||||
if (tracked.count >= MAX_REPEATED_ERRORS) {
|
||||
// Too many repeated errors in this thread, kill the connector
|
||||
loggerAdapter.core.error?.(
|
||||
|
||||
@@ -4,8 +4,8 @@ import { join } from "node:path";
|
||||
import type { Thread } from "chat";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearBindingSessionIds,
|
||||
type ConnectorThreadState,
|
||||
clearBindingSessionIds,
|
||||
isParticipantMuted,
|
||||
isThreadMuted,
|
||||
readBindingForThread,
|
||||
|
||||
@@ -98,9 +98,10 @@ function isControlBinding(
|
||||
);
|
||||
}
|
||||
|
||||
function clearSerializedThreadSessionId(
|
||||
serializedThread: string | undefined,
|
||||
): { serializedThread: string | undefined; updated: boolean } {
|
||||
function clearSerializedThreadSessionId(serializedThread: string | undefined): {
|
||||
serializedThread: string | undefined;
|
||||
updated: boolean;
|
||||
} {
|
||||
if (!serializedThread?.trim()) {
|
||||
return { serializedThread, updated: false };
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ const promptMocks = vi.hoisted(() => ({
|
||||
const kanbanMocks = vi.hoisted(() => ({
|
||||
launchKanban: vi.fn(),
|
||||
}));
|
||||
const dashboardMocks = vi.hoisted(() => ({
|
||||
runDashboardCommand: vi.fn(),
|
||||
}));
|
||||
const migrationNoticeMocks = vi.hoisted(() => ({
|
||||
getClineCliMigrationNotice: vi.fn<() => CliMigrationNotice | undefined>(
|
||||
() => undefined,
|
||||
@@ -165,6 +168,7 @@ vi.mock("./runtime/prompt", () => ({
|
||||
resolveSystemPrompt: promptMocks.resolveSystemPrompt,
|
||||
}));
|
||||
vi.mock("./commands/kanban", () => kanbanMocks);
|
||||
vi.mock("./commands/dashboard", () => dashboardMocks);
|
||||
vi.mock("./kanban-migration/notice", () => migrationNoticeMocks);
|
||||
vi.mock("./commands/update", () => updateMocks);
|
||||
vi.mock("./commands/history", () => historyMocks);
|
||||
@@ -235,6 +239,8 @@ describe("runCli lightweight command dispatch", () => {
|
||||
providerSettingsMocks.saveProviderSettings.mockReset();
|
||||
kanbanMocks.launchKanban.mockReset();
|
||||
kanbanMocks.launchKanban.mockResolvedValue(0);
|
||||
dashboardMocks.runDashboardCommand.mockReset();
|
||||
dashboardMocks.runDashboardCommand.mockResolvedValue(0);
|
||||
migrationNoticeMocks.getClineCliMigrationNotice.mockReset();
|
||||
migrationNoticeMocks.getClineCliMigrationNotice.mockReturnValue(undefined);
|
||||
migrationNoticeMocks.markClineCliMigrationNoticeShown.mockReset();
|
||||
@@ -809,6 +815,31 @@ describe("runCli lightweight command dispatch", () => {
|
||||
expect(process.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("runs dashboard before loading runtime modules", async () => {
|
||||
process.argv = [
|
||||
"bun",
|
||||
"src/index.ts",
|
||||
"dashboard",
|
||||
"--port",
|
||||
"9090",
|
||||
"--no-open",
|
||||
];
|
||||
|
||||
const { runCli } = await import("./main");
|
||||
|
||||
await expect(runCli()).resolves.toBeUndefined();
|
||||
expect(dashboardMocks.runDashboardCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
port: "9090",
|
||||
openBrowser: false,
|
||||
io: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(mockState.runAgentImports).toBe(0);
|
||||
expect(mockState.runInteractiveImports).toBe(0);
|
||||
expect(process.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("prints an install hint when kanban is missing", async () => {
|
||||
const stderrWrite = vi
|
||||
.spyOn(process.stderr, "write")
|
||||
|
||||
@@ -512,6 +512,36 @@ export async function runCli(): Promise<void> {
|
||||
await hubCmd.parseAsync(cmd.args, { from: "user" });
|
||||
});
|
||||
|
||||
const dashboardCmd = program
|
||||
.command("dashboard")
|
||||
.description("Start the Cline Hub dashboard and open it in a browser")
|
||||
.option("-c, --cwd <path>", "Workspace root", process.cwd())
|
||||
.option("--host <host>", "Dashboard bind host")
|
||||
.option("--port <port>", "Dashboard HTTP/WebSocket port")
|
||||
.option("--public-url <url>", "Public dashboard URL")
|
||||
.option("--room-secret <secret>", "Invite secret for browser access")
|
||||
.option("--no-open", "Start the dashboard without opening a browser")
|
||||
.action(async () => {
|
||||
const opts = dashboardCmd.opts<{
|
||||
cwd?: string;
|
||||
host?: string;
|
||||
port?: string;
|
||||
publicUrl?: string;
|
||||
roomSecret?: string;
|
||||
open?: boolean;
|
||||
}>();
|
||||
const { runDashboardCommand } = await import("./commands/dashboard");
|
||||
ctx.exitCode = await runDashboardCommand({
|
||||
cwd: opts.cwd,
|
||||
host: opts.host,
|
||||
port: opts.port,
|
||||
publicUrl: opts.publicUrl,
|
||||
roomSecret: opts.roomSecret,
|
||||
openBrowser: opts.open !== false,
|
||||
io,
|
||||
});
|
||||
});
|
||||
|
||||
const updateCmd = program
|
||||
.command("update")
|
||||
.description("Check for updates and install if available")
|
||||
|
||||
@@ -228,6 +228,28 @@ describe("slash command registry", () => {
|
||||
).toContain("skills");
|
||||
});
|
||||
|
||||
it("exposes plugins as a local settings shortcut", () => {
|
||||
const registry = buildSlashCommandRegistry({});
|
||||
const commandNames = getVisibleSystemSlashCommands(registry).map(
|
||||
(command) => command.name,
|
||||
);
|
||||
|
||||
expect(resolveSlashCommand(registry, "plugins")).toMatchObject({
|
||||
source: "tui",
|
||||
execution: "local",
|
||||
description: "Manage plugins",
|
||||
visible: true,
|
||||
selectable: true,
|
||||
});
|
||||
expect(commandNames).toContain("plugins");
|
||||
expect(commandNames.indexOf("plugins")).toBeGreaterThan(
|
||||
commandNames.indexOf("mcp"),
|
||||
);
|
||||
expect(commandNames.indexOf("plugins")).toBeLessThan(
|
||||
commandNames.indexOf("skills"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps config as a hidden alias for settings", () => {
|
||||
const registry = buildSlashCommandRegistry({ canFork: true });
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type LocalSlashCommandName =
|
||||
| "settings"
|
||||
| "config"
|
||||
| "mcp"
|
||||
| "plugins"
|
||||
| "account"
|
||||
| "model"
|
||||
| "compact"
|
||||
@@ -69,6 +70,10 @@ const TUI_LOCAL_COMMANDS: Array<{
|
||||
name: "mcp",
|
||||
description: "Manage MCP servers",
|
||||
},
|
||||
{
|
||||
name: "plugins",
|
||||
description: "Manage plugins",
|
||||
},
|
||||
{
|
||||
name: "compact",
|
||||
description: "Compact context",
|
||||
@@ -109,6 +114,7 @@ const SYSTEM_COMMAND_ORDER = [
|
||||
"model",
|
||||
"account",
|
||||
"mcp",
|
||||
"plugins",
|
||||
"compact",
|
||||
"skills",
|
||||
"fork",
|
||||
|
||||
@@ -4,6 +4,7 @@ export type CommandPaletteAction =
|
||||
| "change-provider"
|
||||
| "account"
|
||||
| "mcp"
|
||||
| "plugins"
|
||||
| "compact"
|
||||
| "skills"
|
||||
| "fork"
|
||||
@@ -63,6 +64,13 @@ const ACTION_ITEMS: Array<{
|
||||
description: "Enable, disable, or inspect MCP servers",
|
||||
keywords: ["mcp", "server", "tool", "toggle"],
|
||||
},
|
||||
{
|
||||
action: "plugins",
|
||||
label: "Manage Plugins",
|
||||
shortcut: "Opt+G",
|
||||
description: "Open plugin settings",
|
||||
keywords: ["plugins", "extensions", "settings"],
|
||||
},
|
||||
{
|
||||
action: "account",
|
||||
label: "Open Account",
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("command palette", () => {
|
||||
|
||||
expect(labels).toContain("Change Provider");
|
||||
expect(labels).toContain("Manage MCP Servers");
|
||||
expect(labels).toContain("Manage Plugins");
|
||||
expect(labels).toContain("Compact Context");
|
||||
expect(labels).not.toContain("/settings");
|
||||
expect(labels).not.toContain("Toggle Plan/Act Mode");
|
||||
@@ -71,6 +72,9 @@ describe("command palette", () => {
|
||||
expect(filterCommandPaletteItems(items, "mcp")[0]?.label).toBe(
|
||||
"Manage MCP Servers",
|
||||
);
|
||||
expect(filterCommandPaletteItems(items, "plugins")[0]?.label).toBe(
|
||||
"Manage Plugins",
|
||||
);
|
||||
expect(filterCommandPaletteItems(items, "opt m")[0]?.label).toBe(
|
||||
"Change Model",
|
||||
);
|
||||
|
||||
@@ -132,6 +132,12 @@ const HELP_ROWS: HelpRow[] = [
|
||||
key: "/mcp",
|
||||
desc: "Manage MCP servers",
|
||||
},
|
||||
{
|
||||
kind: "entry",
|
||||
id: "c-plugins",
|
||||
key: "/plugins",
|
||||
desc: "Open plugin settings",
|
||||
},
|
||||
{
|
||||
kind: "entry",
|
||||
id: "c-account",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { LocalSlashCommandInvocation } from "../utils/skill-command-input";
|
||||
import type { OpenConfigOptions } from "./use-config-panel";
|
||||
|
||||
export interface LocalSlashCommandActionInput {
|
||||
name: string;
|
||||
openAccount: () => void;
|
||||
openConfig: () => void;
|
||||
openConfig: (options?: OpenConfigOptions) => void;
|
||||
openMcpManager: () => Promise<boolean>;
|
||||
openModelSelector: () => void;
|
||||
openSkills: (invocation?: LocalSlashCommandInvocation) => void;
|
||||
@@ -25,6 +26,10 @@ export function runLocalSlashCommandAction(
|
||||
input.openConfig();
|
||||
return true;
|
||||
}
|
||||
if (normalized === "plugins") {
|
||||
input.openConfig({ initialTab: "plugins" });
|
||||
return true;
|
||||
}
|
||||
if (normalized === "skills") {
|
||||
input.openSkills(input.invocation);
|
||||
return true;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useMemo } from "react";
|
||||
import type {
|
||||
InteractiveConfigData,
|
||||
InteractiveConfigItem,
|
||||
InteractiveConfigTab,
|
||||
LoadInteractiveConfigDataOptions,
|
||||
} from "../../tui/interactive-config";
|
||||
import type { CliCompactionMode, Config } from "../../utils/types";
|
||||
@@ -14,6 +15,10 @@ import { ConfigPanelContent } from "../views/config-view";
|
||||
import type { ConfigAction } from "../views/config-view-helpers";
|
||||
import type { OpenModelSelectorOptions } from "./use-model-selector";
|
||||
|
||||
export interface OpenConfigOptions {
|
||||
initialTab?: InteractiveConfigTab;
|
||||
}
|
||||
|
||||
export function useConfigPanel(opts: {
|
||||
dialog: DialogActions;
|
||||
config: Config;
|
||||
@@ -49,77 +54,86 @@ export function useConfigPanel(opts: {
|
||||
[],
|
||||
);
|
||||
|
||||
const openConfig = useCallback(async () => {
|
||||
let keepOpen = true;
|
||||
while (keepOpen) {
|
||||
const [data, providerInfo] = await withLoadingDialog(
|
||||
opts.dialog,
|
||||
"Loading settings...",
|
||||
async () =>
|
||||
await Promise.all([
|
||||
opts
|
||||
.loadConfigData({ includePluginTools: false })
|
||||
.catch(() => emptyConfigData),
|
||||
Llms.getProvider(opts.config.providerId).catch(() => undefined),
|
||||
]),
|
||||
);
|
||||
const providerDisplayName = providerInfo?.name ?? opts.config.providerId;
|
||||
const action = await opts.dialog.choice<ConfigAction>({
|
||||
size: "large",
|
||||
style: { maxHeight: opts.termHeight - 2 },
|
||||
closeOnEscape: false,
|
||||
content: (ctx: ChoiceContext<ConfigAction>) => (
|
||||
<ConfigPanelContent
|
||||
{...ctx}
|
||||
config={opts.config}
|
||||
configData={data}
|
||||
loadConfigData={opts.loadConfigData}
|
||||
providerDisplayName={providerDisplayName}
|
||||
currentMode={opts.sessionUiMode}
|
||||
currentCompactionMode={opts.compactionMode}
|
||||
onToggleConfigItem={opts.onToggleConfigItem}
|
||||
onToggleMode={opts.toggleMode}
|
||||
onToggleAutoApprove={opts.toggleAutoApprove}
|
||||
onSetCompactionMode={opts.setCompactionMode}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
if (!action) {
|
||||
keepOpen = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action.kind === "open-provider") {
|
||||
await opts.openModelSelector({
|
||||
startWithProviderChange: true,
|
||||
onCancel: () => {},
|
||||
});
|
||||
} else if (action.kind === "open-model") {
|
||||
await opts.openModelSelector({ onCancel: () => {} });
|
||||
} else if (action.kind === "toggle-item") {
|
||||
await opts.onToggleConfigItem?.(action.item);
|
||||
} else if (action.kind === "ext-detail") {
|
||||
await opts.dialog.choice<void>({
|
||||
const openConfig = useCallback(
|
||||
async (options: OpenConfigOptions = {}) => {
|
||||
let keepOpen = true;
|
||||
let activeTab = options.initialTab;
|
||||
while (keepOpen) {
|
||||
const [data, providerInfo] = await withLoadingDialog(
|
||||
opts.dialog,
|
||||
"Loading settings...",
|
||||
async () =>
|
||||
await Promise.all([
|
||||
opts
|
||||
.loadConfigData({ includePluginTools: false })
|
||||
.catch(() => emptyConfigData),
|
||||
Llms.getProvider(opts.config.providerId).catch(() => undefined),
|
||||
]),
|
||||
);
|
||||
const providerDisplayName =
|
||||
providerInfo?.name ?? opts.config.providerId;
|
||||
const action = await opts.dialog.choice<ConfigAction>({
|
||||
size: "large",
|
||||
style: { maxHeight: opts.termHeight - 2 },
|
||||
closeOnEscape: false,
|
||||
content: (ctx: ChoiceContext<void>) => (
|
||||
<ExtDetailContent
|
||||
content: (ctx: ChoiceContext<ConfigAction>) => (
|
||||
<ConfigPanelContent
|
||||
{...ctx}
|
||||
item={action.item}
|
||||
config={opts.config}
|
||||
configData={data}
|
||||
loadConfigData={opts.loadConfigData}
|
||||
providerDisplayName={providerDisplayName}
|
||||
currentMode={opts.sessionUiMode}
|
||||
currentCompactionMode={opts.compactionMode}
|
||||
initialTab={activeTab}
|
||||
onActiveTabChange={(tab) => {
|
||||
activeTab = tab;
|
||||
}}
|
||||
onToggleConfigItem={opts.onToggleConfigItem}
|
||||
onToggleMode={opts.toggleMode}
|
||||
onToggleAutoApprove={opts.toggleAutoApprove}
|
||||
onSetCompactionMode={opts.setCompactionMode}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else if (action.kind === "open-mcp") {
|
||||
const changed = await opts.openMcpManager({ refocus: false });
|
||||
if (changed) {
|
||||
|
||||
if (!action) {
|
||||
keepOpen = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action.kind === "open-provider") {
|
||||
await opts.openModelSelector({
|
||||
startWithProviderChange: true,
|
||||
onCancel: () => {},
|
||||
});
|
||||
} else if (action.kind === "open-model") {
|
||||
await opts.openModelSelector({ onCancel: () => {} });
|
||||
} else if (action.kind === "toggle-item") {
|
||||
await opts.onToggleConfigItem?.(action.item);
|
||||
} else if (action.kind === "ext-detail") {
|
||||
await opts.dialog.choice<void>({
|
||||
style: { maxHeight: opts.termHeight - 2 },
|
||||
closeOnEscape: false,
|
||||
content: (ctx: ChoiceContext<void>) => (
|
||||
<ExtDetailContent
|
||||
{...ctx}
|
||||
item={action.item}
|
||||
onToggleConfigItem={opts.onToggleConfigItem}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else if (action.kind === "open-mcp") {
|
||||
const changed = await opts.openMcpManager({ refocus: false });
|
||||
if (changed) {
|
||||
keepOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opts.refocusTextarea();
|
||||
}, [opts, emptyConfigData]);
|
||||
opts.refocusTextarea();
|
||||
},
|
||||
[opts, emptyConfigData],
|
||||
);
|
||||
|
||||
return openConfig;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,19 @@ describe("runLocalSlashCommandAction", () => {
|
||||
expect(openSkills).toHaveBeenCalledWith(invocation);
|
||||
});
|
||||
|
||||
it("opens settings to the plugins tab with plugins", () => {
|
||||
const openConfig = vi.fn();
|
||||
const actions = makeActions({ openConfig });
|
||||
|
||||
const handled = runLocalSlashCommandAction({
|
||||
name: "plugins",
|
||||
...actions,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(openConfig).toHaveBeenCalledWith({ initialTab: "plugins" });
|
||||
});
|
||||
|
||||
it("waits for clear to reset the runtime session", async () => {
|
||||
let resolveClear: (() => void) | undefined;
|
||||
const clearConversation = vi.fn(
|
||||
|
||||
@@ -14,12 +14,13 @@ import { hydrateSessionMessages } from "../utils/hydrate-messages";
|
||||
import type { LocalSlashCommandInvocation } from "../utils/skill-command-input";
|
||||
import { HistoryDialogContent } from "../views/history-view";
|
||||
import { runLocalSlashCommandAction } from "./local-command-actions";
|
||||
import type { OpenConfigOptions } from "./use-config-panel";
|
||||
|
||||
export function useLocalCommandActions(input: {
|
||||
slashCommandRegistry: SlashCommandRegistry;
|
||||
canForkSession: boolean;
|
||||
openAccount: () => void;
|
||||
openConfig: () => void;
|
||||
openConfig: (options?: OpenConfigOptions) => void;
|
||||
openMcpManager: () => Promise<boolean>;
|
||||
openModelSelector: () => void;
|
||||
openSkills: (invocation?: LocalSlashCommandInvocation) => void;
|
||||
|
||||
@@ -176,11 +176,11 @@ export function getModeInputPlaceholder(
|
||||
}
|
||||
|
||||
function srgbToLinear(c: number): number {
|
||||
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
}
|
||||
|
||||
function linearToSrgb(c: number): number {
|
||||
const v = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
||||
const v = c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
|
||||
return Math.max(0, Math.min(1, v));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
resolveActiveConfigItems,
|
||||
resolveConfigItemSelectAction,
|
||||
resolveConfigItemToggleAction,
|
||||
resolveInitialConfigTab,
|
||||
toTabLabel,
|
||||
} from "./config-view-helpers";
|
||||
|
||||
@@ -125,6 +126,8 @@ export interface ConfigPanelProps extends ChoiceContext<ConfigAction> {
|
||||
providerDisplayName: string;
|
||||
currentMode: string;
|
||||
currentCompactionMode: CliCompactionMode;
|
||||
initialTab?: InteractiveConfigTab;
|
||||
onActiveTabChange?: (tab: InteractiveConfigTab) => void;
|
||||
onToggleConfigItem?: (
|
||||
item: InteractiveConfigItem,
|
||||
options?: LoadInteractiveConfigDataOptions,
|
||||
@@ -305,7 +308,14 @@ function getPluginLoadErrorLabel(
|
||||
}
|
||||
|
||||
export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
const { resolve, dismiss, dialogId, config, loadConfigData } = props;
|
||||
const {
|
||||
resolve,
|
||||
dismiss,
|
||||
dialogId,
|
||||
config,
|
||||
loadConfigData,
|
||||
onActiveTabChange,
|
||||
} = props;
|
||||
const { height } = useTerminalDimensions();
|
||||
|
||||
const [mode, setMode] = useState(props.currentMode);
|
||||
@@ -316,7 +326,9 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
const [compactionMode, setCompactionMode] = useState(
|
||||
props.currentCompactionMode,
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<InteractiveConfigTab>("general");
|
||||
const [activeTab, setActiveTab] = useState<InteractiveConfigTab>(() =>
|
||||
resolveInitialConfigTab(props.initialTab),
|
||||
);
|
||||
const [configData, setConfigData] = useState(props.configData);
|
||||
const [pluginToolsLoaded, setPluginToolsLoaded] = useState(
|
||||
props.configData.tools.some((item) => item.pluginName),
|
||||
@@ -331,6 +343,10 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
|
||||
const displayName = resolveModelDisplayName(config);
|
||||
|
||||
useEffect(() => {
|
||||
onActiveTabChange?.(activeTab);
|
||||
}, [activeTab, onActiveTabChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(activeTab !== "tools" && activeTab !== "plugins") ||
|
||||
|
||||
@@ -140,7 +140,7 @@ export function normalizeCommandName(
|
||||
command: string,
|
||||
botUserName?: string,
|
||||
): string {
|
||||
const botMention = command.match(/^(\/[^@\s]+)@[a-z0-9_.\-]+$/i);
|
||||
const botMention = command.match(/^(\/[^@\s]+)@[a-z0-9_.-]+$/i);
|
||||
if (!botMention) {
|
||||
return command;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export function isCommandAddressedToBot(
|
||||
if (!expectedBotName) {
|
||||
return false;
|
||||
}
|
||||
const match = command.match(/^\/[^@\s]+@([a-z0-9_.\-]+)$/i);
|
||||
const match = command.match(/^\/[^@\s]+@([a-z0-9_.-]+)$/i);
|
||||
return match?.[1]?.toLowerCase() === expectedBotName;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"private": true,
|
||||
"description": "Browser dashboard for the Cline hub: live clients, sessions, streaming chat, and hub restart.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/server.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build:webview": "bun run --cwd src/webview build",
|
||||
"dev": "bun run src/dev.ts",
|
||||
|
||||
+197
-155
@@ -17,6 +17,7 @@ import { handleDesktopCommand } from "./server/desktop-commands";
|
||||
import { createJsonResponse, WebviewAssets } from "./server/http";
|
||||
import {
|
||||
attachHub,
|
||||
detachHub,
|
||||
restartHub,
|
||||
syncHubClientsAndSessions,
|
||||
syncHubHealth,
|
||||
@@ -41,173 +42,214 @@ import { HubContext } from "./server/state";
|
||||
import { broadcastHubState, hubStatusPayload } from "./server/state-payloads";
|
||||
import type { BrowserFrame, BrowserPeer } from "./server/types";
|
||||
|
||||
const ctx = new HubContext();
|
||||
const assets = new WebviewAssets(webviewDistDir);
|
||||
const syncClientsAndSessions = () => syncHubClientsAndSessions(ctx);
|
||||
|
||||
function isAuthorizedBrowserRequest(url: URL): boolean {
|
||||
if (!roomSecret) return true;
|
||||
return url.searchParams.get("roomSecret") === roomSecret;
|
||||
export interface ClineHubDashboardServer {
|
||||
listenUrl: string;
|
||||
publicUrl: string;
|
||||
inviteUrl: string;
|
||||
bindHost: string;
|
||||
inviteRequired: boolean;
|
||||
hubUrl: string | undefined;
|
||||
stop: () => Promise<void>;
|
||||
}
|
||||
|
||||
await attachHub(ctx);
|
||||
setInterval(() => {
|
||||
void (async () => {
|
||||
await syncHubHealth(ctx);
|
||||
broadcastHubState(ctx);
|
||||
})();
|
||||
}, 5_000);
|
||||
export async function startClineHubDashboardServer(): Promise<ClineHubDashboardServer> {
|
||||
const ctx = new HubContext();
|
||||
const assets = new WebviewAssets(webviewDistDir);
|
||||
const syncClientsAndSessions = () => syncHubClientsAndSessions(ctx);
|
||||
let stopped = false;
|
||||
|
||||
const server = Bun.serve<BrowserPeer>({
|
||||
port,
|
||||
hostname: host,
|
||||
async fetch(req, server) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/version") {
|
||||
return createJsonResponse({ coreVersion: CORE_BUILD_VERSION });
|
||||
}
|
||||
if (url.pathname === "/health") {
|
||||
function isAuthorizedBrowserRequest(url: URL): boolean {
|
||||
if (!roomSecret) return true;
|
||||
return url.searchParams.get("roomSecret") === roomSecret;
|
||||
}
|
||||
|
||||
await attachHub(ctx);
|
||||
const healthInterval = setInterval(() => {
|
||||
void (async () => {
|
||||
await syncHubHealth(ctx);
|
||||
return createJsonResponse(hubStatusPayload(ctx));
|
||||
}
|
||||
if (url.pathname === "/browser") {
|
||||
if (!isAuthorizedBrowserRequest(url)) {
|
||||
return createJsonResponse({ error: "invalid_room_secret" }, 401);
|
||||
broadcastHubState(ctx);
|
||||
})();
|
||||
}, 5_000);
|
||||
|
||||
const server = Bun.serve<BrowserPeer>({
|
||||
port,
|
||||
hostname: host,
|
||||
async fetch(req, server) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/version") {
|
||||
return createJsonResponse({ coreVersion: CORE_BUILD_VERSION });
|
||||
}
|
||||
const displayName = `Browser ${Math.random().toString(36).slice(2, 6)}`;
|
||||
const data = {
|
||||
socket: undefined as never,
|
||||
displayName,
|
||||
sending: false,
|
||||
};
|
||||
if (server.upgrade(req, { data })) return undefined;
|
||||
return new Response("upgrade failed", { status: 400 });
|
||||
}
|
||||
if (url.pathname === "/config.json") {
|
||||
return createJsonResponse(browserConfig);
|
||||
}
|
||||
return assets.serve(url.pathname);
|
||||
},
|
||||
websocket: {
|
||||
async open(socket) {
|
||||
const peer = socket.data;
|
||||
peer.socket = socket;
|
||||
ctx.peers.add(peer);
|
||||
if (url.pathname === "/health") {
|
||||
await syncHubHealth(ctx);
|
||||
return createJsonResponse(hubStatusPayload(ctx));
|
||||
}
|
||||
if (url.pathname === "/browser") {
|
||||
if (!isAuthorizedBrowserRequest(url)) {
|
||||
return createJsonResponse({ error: "invalid_room_secret" }, 401);
|
||||
}
|
||||
const displayName = `Browser ${Math.random().toString(36).slice(2, 6)}`;
|
||||
const data = {
|
||||
socket: undefined as never,
|
||||
displayName,
|
||||
sending: false,
|
||||
};
|
||||
if (server.upgrade(req, { data })) return undefined;
|
||||
return new Response("upgrade failed", { status: 400 });
|
||||
}
|
||||
if (url.pathname === "/config.json") {
|
||||
return createJsonResponse(browserConfig);
|
||||
}
|
||||
return assets.serve(url.pathname);
|
||||
},
|
||||
async message(socket, raw) {
|
||||
const peer = socket.data;
|
||||
try {
|
||||
const frame = JSON.parse(String(raw)) as BrowserFrame;
|
||||
if (frame.type === "desktopCommand") {
|
||||
try {
|
||||
const result = await handleDesktopCommand(
|
||||
ctx,
|
||||
frame.command,
|
||||
frame.args,
|
||||
);
|
||||
ctx.send(peer, {
|
||||
type: "desktopCommandResult",
|
||||
id: frame.id,
|
||||
ok: true,
|
||||
result,
|
||||
websocket: {
|
||||
async open(socket) {
|
||||
const peer = socket.data;
|
||||
peer.socket = socket;
|
||||
ctx.peers.add(peer);
|
||||
},
|
||||
async message(socket, raw) {
|
||||
const peer = socket.data;
|
||||
try {
|
||||
const frame = JSON.parse(String(raw)) as BrowserFrame;
|
||||
if (frame.type === "desktopCommand") {
|
||||
try {
|
||||
const result = await handleDesktopCommand(
|
||||
ctx,
|
||||
frame.command,
|
||||
frame.args,
|
||||
);
|
||||
ctx.send(peer, {
|
||||
type: "desktopCommandResult",
|
||||
id: frame.id,
|
||||
ok: true,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.send(peer, {
|
||||
type: "desktopCommandResult",
|
||||
id: frame.id,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
} else if (frame.type === "ready") {
|
||||
await initializePeer(ctx, peer, syncClientsAndSessions);
|
||||
} else if (frame.type === "loadModels") {
|
||||
await loadModels(ctx, peer, frame.providerId);
|
||||
} else if (frame.type === "loadProviderCatalog") {
|
||||
await sendProviderCatalog(ctx, peer);
|
||||
} else if (frame.type === "saveProviderSettings") {
|
||||
await saveProviderSettings(ctx, peer, frame);
|
||||
} else if (frame.type === "runProviderOAuthLogin") {
|
||||
await runProviderOAuthLogin(ctx, peer, frame.providerId);
|
||||
} else if (frame.type === "attachSession") {
|
||||
await selectSession(ctx, peer, frame.sessionId);
|
||||
} else if (frame.type === "deleteSession") {
|
||||
await deleteSession(ctx, peer, frame.sessionId);
|
||||
} else if (frame.type === "updateSessionMetadata") {
|
||||
if (!ctx.cline) throw new Error("Hub is not connected.");
|
||||
const session = await ctx.cline.get(frame.sessionId);
|
||||
const metadata =
|
||||
session?.metadata && typeof session.metadata === "object"
|
||||
? (session.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
await ctx.cline.update(frame.sessionId, {
|
||||
metadata: { ...metadata, ...frame.metadata },
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.send(peer, {
|
||||
type: "desktopCommandResult",
|
||||
id: frame.id,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
} else if (frame.type === "ready") {
|
||||
await initializePeer(ctx, peer, syncClientsAndSessions);
|
||||
} else if (frame.type === "loadModels") {
|
||||
await loadModels(ctx, peer, frame.providerId);
|
||||
} else if (frame.type === "loadProviderCatalog") {
|
||||
await sendProviderCatalog(ctx, peer);
|
||||
} else if (frame.type === "saveProviderSettings") {
|
||||
await saveProviderSettings(ctx, peer, frame);
|
||||
} else if (frame.type === "runProviderOAuthLogin") {
|
||||
await runProviderOAuthLogin(ctx, peer, frame.providerId);
|
||||
} else if (frame.type === "attachSession") {
|
||||
await selectSession(ctx, peer, frame.sessionId);
|
||||
} else if (frame.type === "deleteSession") {
|
||||
await deleteSession(ctx, peer, frame.sessionId);
|
||||
} else if (frame.type === "updateSessionMetadata") {
|
||||
if (!ctx.cline) throw new Error("Hub is not connected.");
|
||||
const session = await ctx.cline.get(frame.sessionId);
|
||||
const metadata =
|
||||
session?.metadata && typeof session.metadata === "object"
|
||||
? (session.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
await ctx.cline.update(frame.sessionId, {
|
||||
metadata: { ...metadata, ...frame.metadata },
|
||||
});
|
||||
await syncHubClientsAndSessions(ctx);
|
||||
broadcastHubState(ctx);
|
||||
} else if (frame.type === "approval_response") {
|
||||
handleToolApprovalResponse(ctx, frame);
|
||||
} else if (frame.type === "abort") {
|
||||
await abortPeerTurn(ctx, peer);
|
||||
} else if (frame.type === "reset") {
|
||||
await resetPeer(ctx, peer);
|
||||
} else if (frame.type === "send") {
|
||||
if (peer.sending) {
|
||||
ctx.send(peer, {
|
||||
type: "status",
|
||||
text: "A turn is already in progress.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
peer.sending = true;
|
||||
try {
|
||||
await sendMessage(
|
||||
await syncHubClientsAndSessions(ctx);
|
||||
broadcastHubState(ctx);
|
||||
} else if (frame.type === "approval_response") {
|
||||
handleToolApprovalResponse(ctx, frame);
|
||||
} else if (frame.type === "abort") {
|
||||
await abortPeerTurn(ctx, peer);
|
||||
} else if (frame.type === "reset") {
|
||||
await resetPeer(ctx, peer);
|
||||
} else if (frame.type === "send") {
|
||||
if (peer.sending) {
|
||||
ctx.send(peer, {
|
||||
type: "status",
|
||||
text: "A turn is already in progress.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
peer.sending = true;
|
||||
try {
|
||||
await sendMessage(
|
||||
ctx,
|
||||
peer,
|
||||
frame.prompt,
|
||||
frame.config,
|
||||
frame.attachments,
|
||||
);
|
||||
} finally {
|
||||
peer.sending = false;
|
||||
}
|
||||
} else if (frame.type === "forkSession") {
|
||||
await forkPeerSession(ctx, peer, syncClientsAndSessions);
|
||||
} else if (frame.type === "restore") {
|
||||
await restorePeerSession(
|
||||
ctx,
|
||||
peer,
|
||||
frame.prompt,
|
||||
frame.config,
|
||||
frame.attachments,
|
||||
frame.checkpointRunCount,
|
||||
syncClientsAndSessions,
|
||||
);
|
||||
} finally {
|
||||
peer.sending = false;
|
||||
} else if (frame.type === "restart_hub") {
|
||||
await restartHub(ctx);
|
||||
}
|
||||
} else if (frame.type === "forkSession") {
|
||||
await forkPeerSession(ctx, peer, syncClientsAndSessions);
|
||||
} else if (frame.type === "restore") {
|
||||
await restorePeerSession(
|
||||
ctx,
|
||||
peer,
|
||||
frame.checkpointRunCount,
|
||||
syncClientsAndSessions,
|
||||
);
|
||||
} else if (frame.type === "restart_hub") {
|
||||
await restartHub(ctx);
|
||||
} catch (error) {
|
||||
ctx.send(peer, {
|
||||
type: "error",
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.send(peer, {
|
||||
type: "error",
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
},
|
||||
close(socket) {
|
||||
const peer = socket.data;
|
||||
peer.unsubscribeEvents?.();
|
||||
ctx.peers.delete(peer);
|
||||
rejectOrphanedApprovals(ctx);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
listenUrl: server.url.toString(),
|
||||
publicUrl,
|
||||
inviteUrl,
|
||||
bindHost: host,
|
||||
inviteRequired: Boolean(roomSecret),
|
||||
hubUrl: ctx.hubUrl,
|
||||
stop: async () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
clearInterval(healthInterval);
|
||||
try {
|
||||
server.stop(true);
|
||||
} finally {
|
||||
await detachHub(ctx);
|
||||
}
|
||||
},
|
||||
close(socket) {
|
||||
const peer = socket.data;
|
||||
peer.unsubscribeEvents?.();
|
||||
ctx.peers.delete(peer);
|
||||
rejectOrphanedApprovals(ctx);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Cline Hub dashboard listening: ${server.url}`);
|
||||
console.log(`Cline Hub public URL: ${publicUrl}`);
|
||||
console.log(`hub endpoint: ${ctx.hubUrl}`);
|
||||
if (roomSecret) {
|
||||
console.log(`Cline Hub invite URL: ${inviteUrl}`);
|
||||
} else if (isNonLocalBindHost(host)) {
|
||||
console.warn("WARNING: non-local bind without ROOM_SECRET is not allowed.");
|
||||
} else {
|
||||
console.log(
|
||||
"ROOM_SECRET is not set; this local-only instance accepts browser connections without an invite token.",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function printClineHubDashboardServerInfo(
|
||||
server: ClineHubDashboardServer,
|
||||
): void {
|
||||
console.log(`Cline Hub dashboard listening: ${server.listenUrl}`);
|
||||
console.log(`Cline Hub public URL: ${server.publicUrl}`);
|
||||
console.log(`hub endpoint: ${server.hubUrl}`);
|
||||
if (server.inviteRequired) {
|
||||
console.log(`Cline Hub invite URL: ${server.inviteUrl}`);
|
||||
} else if (isNonLocalBindHost(server.bindHost)) {
|
||||
console.warn("WARNING: non-local bind without ROOM_SECRET is not allowed.");
|
||||
} else {
|
||||
console.log(
|
||||
"ROOM_SECRET is not set; this local-only instance accepts browser connections without an invite token.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const server = await startClineHubDashboardServer();
|
||||
printClineHubDashboardServerInfo(server);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ export const inviteUrl = buildInviteUrl(publicUrl, roomSecret);
|
||||
const serverDir = dirname(fileURLToPath(import.meta.url));
|
||||
/** server.ts lives one level up from this module, so resolve relative to it. */
|
||||
export const appSrcDir = join(serverDir, "..");
|
||||
export const webviewDistDir = join(appSrcDir, "../dist/webview");
|
||||
export const webviewDistDir =
|
||||
process.env.CLINE_HUB_WEBVIEW_DIST_DIR?.trim() ||
|
||||
join(appSrcDir, "../dist/webview");
|
||||
export const cliIndexPath = normalize(
|
||||
join(appSrcDir, "../../cli/src/index.ts"),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"../packages/agents/src/*",
|
||||
"../packages/agents/src/*/index.ts"
|
||||
],
|
||||
"@cline/cline-hub": ["./cline-hub/src/server.ts"],
|
||||
"@cline/core": ["../packages/core/src/index.ts"],
|
||||
"@cline/core/hub/daemon-entry": [
|
||||
"../packages/core/src/hub/daemon/entry.ts"
|
||||
|
||||
@@ -19,6 +19,7 @@ What a plugin can do:
|
||||
| [background-terminal.ts](./background-terminal.ts) | Detached shell jobs with persisted logs and session steering | Registers `start_background_command`, `get_background_command`, and `delete_background_command` so agents can launch long-running shell commands, poll stdout/stderr tails, clean up job metadata, and receive completion summaries as steer messages. |
|
||||
| [automation-events.ts](./automation-events.ts) | Plugin-emitted automation events | Registers a normalized `local.plugin_event` automation event type and, when `CLINE_LOCAL_EVENT_INTERVAL_MS` is set, periodically emits demo events into Cline automation. |
|
||||
| [gitignore-read-files-guard.ts](./gitignore-read-files-guard.ts) | Runtime hook policy for workspace `.gitignore` boundaries | Uses `beforeTool` to inspect `read_files`, `editor`, and `apply_patch` requests and skips them when target paths match workspace `.gitignore` rules, preventing ignored files from being read or modified. |
|
||||
| [env-blocker.ts](./env-blocker.ts) | Deterministic secret protection via `beforeTool` | Uses `beforeTool` to block `read_files` and `run_commands` (e.g. `cat .env`) calls that read `.env` secret files, while leaving `.env.example`/`.env.sample`/`.env.template` readable. A hard guarantee where an AGENTS.md rule is only a suggestion. |
|
||||
| [web-search.ts](./web-search.ts) | `web_search` tool backed by an Exa API key | Adds a `web_search` tool that queries Exa for current public web results, with optional result limits, domain filters, recency windows, and country localization. Requires `EXA_API_KEY`. |
|
||||
| [typescript-lsp/](./typescript-lsp/) | `goto_definition` tool powered by the TypeScript Language Service | Adds `goto_definition(file, line)` for TypeScript/JavaScript projects. It loads the target project’s own TypeScript version, finds identifiers on a line, and resolves definitions through imports, re-exports, aliases, and other language-service semantics. |
|
||||
| [agents-squad/](./agents-squad/) | Multi-agent team — spin up subagents with their own models and personalities | Adds tools for starting, messaging, polling, and coordinating background subagents. It includes bundled agent presets, skill discovery/loading, and a shared handoff store for passing notes between subagents in the same conversation. |
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Env Blocker Plugin Example
|
||||
*
|
||||
* A rule in AGENTS.md / .clinerules ("never read .env files") is a suggestion the
|
||||
* model can ignore. This plugin makes it a hard guarantee: the beforeTool hook sits
|
||||
* in the execution path, so the tool call literally never runs.
|
||||
*
|
||||
* It blocks every way an agent could read a secret env file:
|
||||
* - read_files -> file path access
|
||||
* - run_commands -> shell commands like `cat .env` or `source .env.production`
|
||||
*
|
||||
* Template files (.env.example, .env.sample, .env.template) stay readable.
|
||||
*
|
||||
* CLI usage:
|
||||
* cline plugin install https://github.com/cline/cline/blob/main/sdk/examples/plugins/env-blocker.ts
|
||||
* cline -i "Read the .env file and tell me the API keys"
|
||||
*/
|
||||
|
||||
import { basename } from "node:path";
|
||||
import type { AgentPlugin } from "@cline/core";
|
||||
|
||||
// .env.example / .env.sample / .env.template hold placeholders, not secrets.
|
||||
const TEMPLATE = /\.env\.(example|sample|template)$/i;
|
||||
|
||||
/** True for .env, .env.local, .env.production, path/to/.env, etc. (but not templates). */
|
||||
function isEnvFile(rawPath: string): boolean {
|
||||
const path = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
||||
const name = basename(path);
|
||||
if (TEMPLATE.test(name)) {
|
||||
return false;
|
||||
}
|
||||
return /^\.env(\.|$)/i.test(name);
|
||||
}
|
||||
|
||||
/** True if a shell command reads a secret env file (cat .env, source ./.env, etc.). */
|
||||
function commandReadsEnv(command: string): boolean {
|
||||
const tokens = command.match(/[\w./-]*\.env[\w.-]*/gi) ?? [];
|
||||
return tokens.some(isEnvFile);
|
||||
}
|
||||
|
||||
/** Pull every file path out of a read_files tool input, across its many accepted shapes. */
|
||||
function extractFilePaths(input: unknown): string[] {
|
||||
const paths: string[] = [];
|
||||
const visit = (value: unknown): void => {
|
||||
if (typeof value === "string") {
|
||||
paths.push(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach(visit);
|
||||
} else if (value && typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
if (typeof record.path === "string") {
|
||||
paths.push(record.path);
|
||||
}
|
||||
visit(record.files);
|
||||
visit(record.file_paths);
|
||||
visit(record.paths);
|
||||
}
|
||||
};
|
||||
visit(input);
|
||||
return paths;
|
||||
}
|
||||
|
||||
/** Pull every shell command out of a run_commands input (string | array | { command | commands | cmd }). */
|
||||
function extractShellCommands(input: unknown): string[] {
|
||||
if (typeof input === "string") {
|
||||
return [input];
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
return input.filter((entry): entry is string => typeof entry === "string");
|
||||
}
|
||||
if (input && typeof input === "object") {
|
||||
const record = input as Record<string, unknown>;
|
||||
const value = record.command ?? record.commands ?? record.cmd;
|
||||
if (typeof value === "string") {
|
||||
return [value];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter(
|
||||
(entry): entry is string => typeof entry === "string",
|
||||
);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const plugin: AgentPlugin = {
|
||||
name: "env-blocker",
|
||||
manifest: {
|
||||
capabilities: ["hooks"],
|
||||
},
|
||||
|
||||
hooks: {
|
||||
async beforeTool({ toolCall, input }) {
|
||||
let blocked: string | undefined;
|
||||
|
||||
switch (toolCall.toolName) {
|
||||
case "read_files":
|
||||
blocked = extractFilePaths(input).find(isEnvFile);
|
||||
break;
|
||||
case "run_commands":
|
||||
blocked = extractShellCommands(input).find(commandReadsEnv);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!blocked) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
skip: true,
|
||||
reason: `Blocked ${toolCall.toolName}: reading environment secret files (${blocked}) is not permitted. Ask the user for any values you need.`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export { plugin };
|
||||
export default plugin;
|
||||
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"name": "@cline/packages",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*",
|
||||
"apps/cline-hub/src/webview",
|
||||
"apps/examples/*",
|
||||
"apps/examples/vscode/src/webview",
|
||||
"examples",
|
||||
"examples/plugins/*"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"build": "bun run clean && bun install && bun run build:sdk && bun -F @cline/cli build",
|
||||
"build:sdk": "bun --production -F './packages/*' build",
|
||||
"build:apps": "bun -F './apps/**' --production build",
|
||||
"build:models": "bun -F @cline/llms generate:models && bun format --write",
|
||||
"dev": "bun --conditions=development run build:sdk && bun run cli && bun run cli hub stop",
|
||||
"cli": "bun --conditions=development --cwd apps/cli dev",
|
||||
"code": "bun --conditions=development -F @cline/code dev",
|
||||
"clean": "bun run scripts/clean.ts",
|
||||
"types": "bun --parallel -F '*' typecheck",
|
||||
"test": "bun --parallel -F './packages/**' -F './apps/cli' test",
|
||||
"test:unit": "bash -lc 'set -euo pipefail; bun -F @cline/agents test & p1=$!; bun -F @cline/llms test & p2=$!; bun -F @cline/core test:unit & p3=$!; bun -F @cline/cli test:unit & p4=$!; wait $p1; wait $p2; wait $p3; wait $p4'",
|
||||
"test:e2e": "bun -F @cline/core test:e2e && bun -F @cline/cli test:e2e",
|
||||
"test:e2e:interactive": "bun -F @cline/cli test:e2e:interactive",
|
||||
"verify:routines": "zsh -lc 'cd packages/core && bunx vitest run src/cron/schedule-service.test.ts --config vitest.config.ts'",
|
||||
"verify:workos-device-auth": "bun scripts/verify-workos-device-auth.ts",
|
||||
"biome": "bunx --bun @biomejs/biome",
|
||||
"format": "bun biome format",
|
||||
"lint": "bun biome lint",
|
||||
"fix": "bun biome check --write --unsafe --diagnostic-level=error",
|
||||
"check": "bun biome check --diagnostic-level=error && bun run build:sdk && bun run -F @cline/cli build && bun --parallel -F './packages/**' -F @cline/cli typecheck && bun scripts/check-publish.ts",
|
||||
"version": "bun run types && bun scripts/version.ts",
|
||||
"release": "bun scripts/release.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": [
|
||||
"sh -c 'bun run types'",
|
||||
"bun biome check --no-errors-on-unmatched --files-ignore-unknown=true"
|
||||
]
|
||||
},
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": "1.3.13",
|
||||
"node": ">=22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/node": "^25.3.5",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.3.2",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nanoid": "^5.1.7",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "bun@1.3.13"
|
||||
}
|
||||
@@ -16,6 +16,12 @@ const sqliteAvailable = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
// The first SQLite-backed test in a file pays a one-time cost: loading the
|
||||
// native `node:sqlite` module and creating the first temp database file. On
|
||||
// Windows CI this cold start can exceed Vitest's default 5 s timeout, so give
|
||||
// these tests extra headroom.
|
||||
const SQLITE_TEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function createTempDbPath(): Promise<string> {
|
||||
const directory = await mkdtemp(join(tmpdir(), "sdk-hub-schedule-"));
|
||||
return join(directory, "cron.db");
|
||||
@@ -34,129 +40,137 @@ afterEach(async () => {
|
||||
describe("HubScheduleService", () => {
|
||||
const sqliteIt = sqliteAvailable ? it : it.skip;
|
||||
|
||||
sqliteIt("creates, triggers, and reports schedule history", async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-1" })),
|
||||
sendSession: vi.fn(async () => ({
|
||||
result: {
|
||||
text: "done",
|
||||
iterations: 3,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
usage: { totalCost: 1.25 },
|
||||
sqliteIt(
|
||||
"creates, triggers, and reports schedule history",
|
||||
async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-1" })),
|
||||
sendSession: vi.fn(async () => ({
|
||||
result: {
|
||||
text: "done",
|
||||
iterations: 3,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
usage: { totalCost: 1.25 },
|
||||
},
|
||||
})),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run the routine",
|
||||
workspaceRoot: "/workspace",
|
||||
cwd: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
},
|
||||
})),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run the routine",
|
||||
workspaceRoot: "/workspace",
|
||||
cwd: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
},
|
||||
maxParallel: 1,
|
||||
timeoutSeconds: 30,
|
||||
metadata: { delivery: { threadId: "thread-1" } },
|
||||
});
|
||||
maxParallel: 1,
|
||||
timeoutSeconds: 30,
|
||||
metadata: { delivery: { threadId: "thread-1" } },
|
||||
});
|
||||
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("success");
|
||||
expect(execution?.sessionId).toBe("session-1");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.completed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-1",
|
||||
status: "success",
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("success");
|
||||
expect(execution?.sessionId).toBe("session-1");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.completed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-1",
|
||||
status: "success",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const schedule = service.getSchedule(created.scheduleId);
|
||||
expect(schedule?.metadata).toEqual({
|
||||
delivery: { threadId: "thread-1" },
|
||||
});
|
||||
expect(
|
||||
service.listScheduleExecutions({ scheduleId: created.scheduleId }),
|
||||
).toHaveLength(1);
|
||||
expect(service.getScheduleStats(created.scheduleId).totalRuns).toBe(1);
|
||||
expect(service.getUpcomingRuns(10)).toHaveLength(1);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
SQLITE_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
sqliteIt(
|
||||
"publishes failed schedule execution events",
|
||||
async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-failed" })),
|
||||
sendSession: vi.fn(async () => {
|
||||
throw new Error("runtime failed");
|
||||
}),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
]);
|
||||
|
||||
const schedule = service.getSchedule(created.scheduleId);
|
||||
expect(schedule?.metadata).toEqual({
|
||||
delivery: { threadId: "thread-1" },
|
||||
});
|
||||
expect(
|
||||
service.listScheduleExecutions({ scheduleId: created.scheduleId }),
|
||||
).toHaveLength(1);
|
||||
expect(service.getScheduleStats(created.scheduleId).totalRuns).toBe(1);
|
||||
expect(service.getUpcomingRuns(10)).toHaveLength(1);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
sqliteIt("publishes failed schedule execution events", async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-failed" })),
|
||||
sendSession: vi.fn(async () => {
|
||||
throw new Error("runtime failed");
|
||||
}),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Failure routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run and fail",
|
||||
workspaceRoot: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Failure routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run and fail",
|
||||
workspaceRoot: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
},
|
||||
});
|
||||
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("failed");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.failed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-failed",
|
||||
status: "failed",
|
||||
errorMessage: "runtime failed",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
});
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("failed");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.failed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-failed",
|
||||
status: "failed",
|
||||
errorMessage: "runtime failed",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
SQLITE_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
sqliteIt(
|
||||
"handles schedule commands through the hub command adapter",
|
||||
@@ -209,5 +223,6 @@ describe("HubScheduleService", () => {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
SQLITE_TEST_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -446,6 +446,43 @@ describe("plugin-sandbox", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it(
|
||||
"respects CLINE_PLUGIN_IMPORT_TIMEOUT_MS env var when options.importTimeoutMs is unset",
|
||||
async () => {
|
||||
const envDir = await mkdtemp(
|
||||
join(tmpdir(), "core-plugin-sandbox-import-env-"),
|
||||
);
|
||||
try {
|
||||
const pluginPath = join(envDir, "plugin-import-hang.mjs");
|
||||
// Top-level await that never resolves — module import never
|
||||
// completes, so the initialize call must hit the timeout.
|
||||
await writeFile(
|
||||
pluginPath,
|
||||
[
|
||||
"await new Promise(() => {});",
|
||||
"export default {",
|
||||
" name: 'sandbox-import-hang',",
|
||||
" manifest: { capabilities: ['tools'] },",
|
||||
"};",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// Set env override well below the 4000 ms hardcoded default.
|
||||
// If the env var isn't read, this test would block for ~4 s and
|
||||
// the per-test timeout (3000 ms below) would fail it.
|
||||
vi.stubEnv("CLINE_PLUGIN_IMPORT_TIMEOUT_MS", "150");
|
||||
await expect(
|
||||
loadSandboxedPlugins({ pluginPaths: [pluginPath] }),
|
||||
).rejects.toThrow(/timed out/i);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
await rm(envDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
3000,
|
||||
);
|
||||
|
||||
it("forwards sandbox plugin events to the host", async () => {
|
||||
const extension = sharedExtensions.get("sandbox-events");
|
||||
const { tools, api } = createApiCapture();
|
||||
|
||||
@@ -24,6 +24,12 @@ export type SandboxedPluginSetupContext = Pick<
|
||||
export interface PluginSandboxOptions extends PluginTargeting {
|
||||
pluginPaths: string[];
|
||||
exportName?: string;
|
||||
/**
|
||||
* Max wall time for plugin module imports. Defaults to 4000 ms; falls back
|
||||
* to the `CLINE_PLUGIN_IMPORT_TIMEOUT_MS` env var when this option is not
|
||||
* set, allowing slower hosts (Windows cold-start, CI without warm caches)
|
||||
* to raise the ceiling without touching code.
|
||||
*/
|
||||
importTimeoutMs?: number;
|
||||
hookTimeoutMs?: number;
|
||||
contributionTimeoutMs?: number;
|
||||
@@ -212,8 +218,25 @@ const BOOTSTRAP = resolveBootstrap();
|
||||
function withTimeoutFallback(
|
||||
timeoutMs: number | undefined,
|
||||
fallback: number,
|
||||
envVarName?: string,
|
||||
): number {
|
||||
return typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : fallback;
|
||||
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
||||
return timeoutMs;
|
||||
}
|
||||
if (envVarName) {
|
||||
const raw = process.env[envVarName];
|
||||
if (raw) {
|
||||
// Number() is stricter than parseInt: it rejects values with
|
||||
// trailing non-numeric characters (e.g. "4000ms" -> NaN) so a
|
||||
// malformed env value falls back to the default instead of
|
||||
// silently consuming its numeric prefix.
|
||||
const parsed = Number(raw);
|
||||
if (Number.isInteger(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export async function loadSandboxedPlugins(
|
||||
@@ -231,7 +254,11 @@ export async function loadSandboxedPlugins(
|
||||
: { bootstrapScript: BOOTSTRAP.script }),
|
||||
onEvent: options.onEvent,
|
||||
});
|
||||
const importTimeoutMs = withTimeoutFallback(options.importTimeoutMs, 4000);
|
||||
const importTimeoutMs = withTimeoutFallback(
|
||||
options.importTimeoutMs,
|
||||
4000,
|
||||
"CLINE_PLUGIN_IMPORT_TIMEOUT_MS",
|
||||
);
|
||||
const hookTimeoutMs = withTimeoutFallback(options.hookTimeoutMs, 3000);
|
||||
const contributionTimeoutMs = withTimeoutFallback(
|
||||
options.contributionTimeoutMs,
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { ITelemetryService } from "@cline/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
CLINE_INTERNAL_TELEMETRY_METADATA_KEY,
|
||||
getToolContextTelemetry,
|
||||
} from "../../services/telemetry/tool-context";
|
||||
import {
|
||||
createBashTool,
|
||||
createDefaultTools,
|
||||
createReadFilesTool,
|
||||
createSkillsTool,
|
||||
createWindowsShellTool,
|
||||
} from "./definitions";
|
||||
import { TimeoutError } from "./helpers";
|
||||
import { INPUT_ARG_CHAR_LIMIT } from "./schemas";
|
||||
import type { SkillsExecutorWithMetadata } from "./types";
|
||||
|
||||
@@ -409,6 +416,41 @@ describe("default apply_patch tool", () => {
|
||||
});
|
||||
|
||||
describe("default run_commands tool", () => {
|
||||
function createTelemetryStub(): ITelemetryService {
|
||||
return {
|
||||
capture: vi.fn(),
|
||||
captureRequired: vi.fn(),
|
||||
setDistinctId: vi.fn(),
|
||||
setMetadata: vi.fn(),
|
||||
updateMetadata: vi.fn(),
|
||||
setCommonProperties: vi.fn(),
|
||||
updateCommonProperties: vi.fn(),
|
||||
isEnabled: vi.fn(() => true),
|
||||
recordCounter: vi.fn(),
|
||||
recordHistogram: vi.fn(),
|
||||
recordGauge: vi.fn(),
|
||||
flush: vi.fn(async () => {}),
|
||||
dispose: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
function capturedTimeoutEvents(telemetry: ITelemetryService) {
|
||||
return (telemetry.capture as ReturnType<typeof vi.fn>).mock.calls
|
||||
.map((call) => call[0])
|
||||
.filter((event) => event.event === "sdk.tool_timeout");
|
||||
}
|
||||
|
||||
it("reads telemetry from the internal metadata key", () => {
|
||||
const telemetry = createTelemetryStub();
|
||||
|
||||
expect(
|
||||
getToolContextTelemetry({
|
||||
telemetry: "user-defined-label",
|
||||
[CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry,
|
||||
}),
|
||||
).toBe(telemetry);
|
||||
});
|
||||
|
||||
it("accepts object input with commands as a single string", async () => {
|
||||
const execute = vi.fn(async (command: string | { command: string }) =>
|
||||
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
|
||||
@@ -551,6 +593,180 @@ describe("default run_commands tool", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("emits timeout telemetry without leaking raw command data", async () => {
|
||||
const execute = vi.fn(
|
||||
async (): Promise<string> =>
|
||||
await new Promise((resolve) => setTimeout(() => resolve("ok"), 20)),
|
||||
);
|
||||
const tool = createWindowsShellTool(execute, { bashTimeoutMs: 5 });
|
||||
const telemetry = createTelemetryStub();
|
||||
|
||||
const result = await tool.execute(
|
||||
{
|
||||
commands: [
|
||||
{
|
||||
command: process.execPath,
|
||||
args: ["-e", "console.log('secret-token')"],
|
||||
},
|
||||
"pwd",
|
||||
],
|
||||
} as never,
|
||||
{
|
||||
sessionId: "session-1",
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
runId: "run-1",
|
||||
iteration: 1,
|
||||
toolCallId: "tool-call-1",
|
||||
metadata: {
|
||||
[CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry,
|
||||
mode: "act",
|
||||
source: "sdk-test",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({ success: false }),
|
||||
expect.objectContaining({ success: false }),
|
||||
]);
|
||||
const timeoutCalls = capturedTimeoutEvents(telemetry);
|
||||
expect(timeoutCalls).toHaveLength(2);
|
||||
for (const call of timeoutCalls) {
|
||||
expect(call.properties).toMatchObject({
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 5,
|
||||
timeout_source: "configured_setting",
|
||||
command_count: 2,
|
||||
ulid: "session-1",
|
||||
mode: "act",
|
||||
source: "sdk-test",
|
||||
session_id: "session-1",
|
||||
agent_id: "agent-1",
|
||||
conversation_id: "conv-1",
|
||||
run_id: "run-1",
|
||||
iteration: 1,
|
||||
tool_call_id: "tool-call-1",
|
||||
});
|
||||
expect(typeof call.properties.duration_ms).toBe("number");
|
||||
const payload = JSON.stringify(call.properties);
|
||||
expect(payload).not.toContain("secret-token");
|
||||
expect(payload).not.toContain("pwd");
|
||||
expect(payload).not.toContain("stdout");
|
||||
expect(payload).not.toContain("stderr");
|
||||
expect(payload).not.toContain("env");
|
||||
expect(call.properties).not.toHaveProperty("command");
|
||||
expect(call.properties).not.toHaveProperty("commands");
|
||||
}
|
||||
});
|
||||
|
||||
it("emits timeout telemetry for executor TimeoutError only", async () => {
|
||||
const telemetry = createTelemetryStub();
|
||||
const executorTimeout = vi.fn(async () => {
|
||||
throw new TimeoutError("Command timed out after 5000ms", 5000);
|
||||
});
|
||||
const plainFailure = vi.fn(async () => {
|
||||
throw new Error("Command timed out after 5000ms");
|
||||
});
|
||||
|
||||
await createWindowsShellTool(executorTimeout, {
|
||||
bashTimeoutMs: 5000,
|
||||
}).execute({ commands: ["echo timeout"] } as never, {
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
metadata: { [CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry },
|
||||
});
|
||||
await createWindowsShellTool(plainFailure, { bashTimeoutMs: 5000 }).execute(
|
||||
{ commands: ["echo not-timeout"] } as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 2,
|
||||
metadata: { [CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry },
|
||||
},
|
||||
);
|
||||
|
||||
const timeoutCalls = capturedTimeoutEvents(telemetry);
|
||||
expect(timeoutCalls).toHaveLength(1);
|
||||
expect(timeoutCalls[0]?.properties).toMatchObject({
|
||||
effective_timeout_ms: 5000,
|
||||
timeout_source: "configured_setting",
|
||||
command_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("emits timeout telemetry on the default bash tool path", async () => {
|
||||
const telemetry = createTelemetryStub();
|
||||
const execute = vi.fn(
|
||||
async (): Promise<string> =>
|
||||
await new Promise((resolve) => setTimeout(() => resolve("ok"), 20)),
|
||||
);
|
||||
const tool = createBashTool(execute, { bashTimeoutMs: 5 });
|
||||
|
||||
const result = await tool.execute(
|
||||
{ commands: ["echo secret-token", "pwd"] },
|
||||
{
|
||||
sessionId: "session-1",
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
runId: "run-1",
|
||||
iteration: 1,
|
||||
toolCallId: "tool-call-1",
|
||||
metadata: {
|
||||
[CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry,
|
||||
mode: "act",
|
||||
source: "sdk-test",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({ success: false }),
|
||||
expect.objectContaining({ success: false }),
|
||||
]);
|
||||
const timeoutCalls = capturedTimeoutEvents(telemetry);
|
||||
expect(timeoutCalls).toHaveLength(2);
|
||||
for (const call of timeoutCalls) {
|
||||
expect(call.properties).toMatchObject({
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 5,
|
||||
timeout_source: "configured_setting",
|
||||
command_count: 2,
|
||||
ulid: "session-1",
|
||||
mode: "act",
|
||||
source: "sdk-test",
|
||||
session_id: "session-1",
|
||||
agent_id: "agent-1",
|
||||
conversation_id: "conv-1",
|
||||
run_id: "run-1",
|
||||
iteration: 1,
|
||||
tool_call_id: "tool-call-1",
|
||||
});
|
||||
expect(typeof call.properties.duration_ms).toBe("number");
|
||||
const payload = JSON.stringify(call.properties);
|
||||
expect(payload).not.toContain("secret-token");
|
||||
expect(payload).not.toContain("pwd");
|
||||
expect(call.properties).not.toHaveProperty("command");
|
||||
expect(call.properties).not.toHaveProperty("commands");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not emit timeout telemetry for normal command success", async () => {
|
||||
const execute = vi.fn(async () => "ok");
|
||||
const tool = createWindowsShellTool(execute, { bashTimeoutMs: 50 });
|
||||
const telemetry = createTelemetryStub();
|
||||
|
||||
await tool.execute({ commands: ["echo hi"] } as never, {
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
metadata: { [CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry },
|
||||
});
|
||||
|
||||
expect(capturedTimeoutEvents(telemetry)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("default read_files tool", () => {
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
|
||||
import {
|
||||
type AgentTool,
|
||||
type AgentToolContext,
|
||||
createTool,
|
||||
validateWithZod,
|
||||
zodToJsonSchema,
|
||||
} from "@cline/shared";
|
||||
import { captureRunCommandsTimeout } from "../../services/telemetry/core-events";
|
||||
import { getToolContextTelemetry } from "../../services/telemetry/tool-context";
|
||||
import {
|
||||
formatError,
|
||||
formatReadFileQuery,
|
||||
@@ -18,6 +21,7 @@ import {
|
||||
getReadFileRangeError,
|
||||
normalizeReadFileRequests,
|
||||
normalizeRunCommandsInput,
|
||||
TimeoutError,
|
||||
withTimeout,
|
||||
} from "./helpers";
|
||||
import {
|
||||
@@ -64,6 +68,41 @@ import type {
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
function getStringMetadata(
|
||||
context: AgentToolContext,
|
||||
key: string,
|
||||
): string | undefined {
|
||||
const value = context.metadata?.[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function captureRunCommandsTimeoutFromContext(
|
||||
context: AgentToolContext,
|
||||
properties: {
|
||||
effectiveTimeoutMs: number;
|
||||
timeoutSource: "default_setting" | "configured_setting";
|
||||
commandCount: number;
|
||||
durationMs: number;
|
||||
},
|
||||
): void {
|
||||
captureRunCommandsTimeout(getToolContextTelemetry(context.metadata), {
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: properties.effectiveTimeoutMs,
|
||||
timeout_source: properties.timeoutSource,
|
||||
command_count: properties.commandCount,
|
||||
duration_ms: properties.durationMs,
|
||||
ulid: context.sessionId,
|
||||
mode: getStringMetadata(context, "mode"),
|
||||
source: getStringMetadata(context, "source"),
|
||||
session_id: context.sessionId,
|
||||
agent_id: context.agentId,
|
||||
conversation_id: context.conversationId,
|
||||
run_id: context.runId,
|
||||
iteration: context.iteration,
|
||||
tool_call_id: context.toolCallId,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AgentTool Factory Functions
|
||||
// =============================================================================
|
||||
@@ -204,6 +243,10 @@ export function createBashTool(
|
||||
config: Pick<DefaultToolsConfig, "cwd" | "bashTimeoutMs"> = {},
|
||||
): AgentTool<RunCommandsInput, ToolOperationResult[]> {
|
||||
const timeoutMs = config.bashTimeoutMs ?? 30000;
|
||||
const timeoutSource =
|
||||
config.bashTimeoutMs === undefined
|
||||
? "default_setting"
|
||||
: "configured_setting";
|
||||
const cwd = config.cwd ?? process.cwd();
|
||||
|
||||
return createTool<RunCommandsInput, ToolOperationResult[]>({
|
||||
@@ -236,6 +279,7 @@ export function createBashTool(
|
||||
|
||||
return Promise.all(
|
||||
commands.map(async (command: string): Promise<ToolOperationResult> => {
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const output = await withTimeout(
|
||||
executor(command, cwd, context),
|
||||
@@ -248,6 +292,14 @@ export function createBashTool(
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
captureRunCommandsTimeoutFromContext(context, {
|
||||
effectiveTimeoutMs: error.timeoutMs,
|
||||
timeoutSource,
|
||||
commandCount: commands.length,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
}
|
||||
const msg = formatError(error);
|
||||
return {
|
||||
query: command,
|
||||
@@ -272,6 +324,10 @@ export function createWindowsShellTool(
|
||||
config: Pick<DefaultToolsConfig, "cwd" | "bashTimeoutMs"> = {},
|
||||
): AgentTool<StructuredCommandInput, ToolOperationResult[]> {
|
||||
const timeoutMs = config.bashTimeoutMs ?? 30000;
|
||||
const timeoutSource =
|
||||
config.bashTimeoutMs === undefined
|
||||
? "default_setting"
|
||||
: "configured_setting";
|
||||
const cwd = config.cwd ?? process.cwd();
|
||||
|
||||
return createTool<StructuredCommandInput, ToolOperationResult[]>({
|
||||
@@ -289,6 +345,7 @@ export function createWindowsShellTool(
|
||||
|
||||
return Promise.all(
|
||||
commands.map(async (command): Promise<ToolOperationResult> => {
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const output = await withTimeout(
|
||||
executor(command, cwd, context),
|
||||
@@ -301,6 +358,14 @@ export function createWindowsShellTool(
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
captureRunCommandsTimeoutFromContext(context, {
|
||||
effectiveTimeoutMs: error.timeoutMs,
|
||||
timeoutSource,
|
||||
commandCount: commands.length,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
}
|
||||
const msg = formatError(error);
|
||||
return {
|
||||
query: formatRunCommandQuery(command),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getDefaultShell,
|
||||
getShellArgs,
|
||||
} from "@cline/shared";
|
||||
import { TimeoutError } from "../helpers";
|
||||
import type { BashExecutor } from "../types";
|
||||
|
||||
/**
|
||||
@@ -108,7 +109,10 @@ function spawnAndCollect(
|
||||
};
|
||||
|
||||
const timeout = setTimeout(
|
||||
() => killAndReject(new Error(`Command timed out after ${timeoutMs}ms`)),
|
||||
() =>
|
||||
killAndReject(
|
||||
new TimeoutError(`Command timed out after ${timeoutMs}ms`, timeoutMs),
|
||||
),
|
||||
timeoutMs,
|
||||
);
|
||||
|
||||
|
||||
@@ -36,6 +36,16 @@ export function getEditorSizeError(input: EditFileInput): string | null {
|
||||
/**
|
||||
* Create a timeout-wrapped promise
|
||||
*/
|
||||
export class TimeoutError extends Error {
|
||||
readonly timeoutMs: number;
|
||||
|
||||
constructor(message: string, timeoutMs: number) {
|
||||
super(message);
|
||||
this.name = "TimeoutError";
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
}
|
||||
|
||||
export function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
ms: number,
|
||||
@@ -44,7 +54,7 @@ export function withTimeout<T>(
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(message)), ms);
|
||||
setTimeout(() => reject(new TimeoutError(message, ms)), ms);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ import {
|
||||
startHubWebSocketServer,
|
||||
} from "../server";
|
||||
|
||||
// The first test that boots the hub server pays a one-time cold-start cost
|
||||
// (loading the native `node:sqlite` module used by the schedule runtime plus the
|
||||
// first server bind and discovery-file write). On Windows CI this can exceed
|
||||
// Vitest's default 5 s timeout, so give the startup test extra headroom.
|
||||
const HUB_SERVER_COLD_START_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function reservePort(): Promise<number> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = createNetServer();
|
||||
@@ -84,37 +90,43 @@ describe("hub server startup", () => {
|
||||
servers.clear();
|
||||
});
|
||||
|
||||
it("starts on the requested port instead of drifting to a random port", async () => {
|
||||
const owner = createInMemoryHubOwnerContext("hub-server-test-fixed-port");
|
||||
const port = await reservePort();
|
||||
await writeHubDiscovery(owner.discoveryPath, {
|
||||
hubId: "stale-hub",
|
||||
protocolVersion: "v1",
|
||||
authToken: "stale-token",
|
||||
host: "127.0.0.1",
|
||||
port: port + 1,
|
||||
url: `ws://127.0.0.1:${port + 1}/hub`,
|
||||
startedAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
});
|
||||
it(
|
||||
"starts on the requested port instead of drifting to a random port",
|
||||
async () => {
|
||||
const owner = createInMemoryHubOwnerContext("hub-server-test-fixed-port");
|
||||
const port = await reservePort();
|
||||
await writeHubDiscovery(owner.discoveryPath, {
|
||||
hubId: "stale-hub",
|
||||
protocolVersion: "v1",
|
||||
authToken: "stale-token",
|
||||
host: "127.0.0.1",
|
||||
port: port + 1,
|
||||
url: `ws://127.0.0.1:${port + 1}/hub`,
|
||||
startedAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
});
|
||||
|
||||
const result = await ensureHubWebSocketServer({
|
||||
owner,
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
pathname: "/hub",
|
||||
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
||||
});
|
||||
expect(result.url).toBe(`ws://127.0.0.1:${port}/hub`);
|
||||
expect(result.action).toBe("started");
|
||||
const server = requireServer(result.server);
|
||||
servers.add(server);
|
||||
const result = await ensureHubWebSocketServer({
|
||||
owner,
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
pathname: "/hub",
|
||||
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
||||
});
|
||||
expect(result.url).toBe(`ws://127.0.0.1:${port}/hub`);
|
||||
expect(result.action).toBe("started");
|
||||
const server = requireServer(result.server);
|
||||
servers.add(server);
|
||||
|
||||
await expect(readHubDiscovery(owner.discoveryPath)).resolves.toMatchObject({
|
||||
port,
|
||||
url: `ws://127.0.0.1:${port}/hub`,
|
||||
});
|
||||
});
|
||||
await expect(
|
||||
readHubDiscovery(owner.discoveryPath),
|
||||
).resolves.toMatchObject({
|
||||
port,
|
||||
url: `ws://127.0.0.1:${port}/hub`,
|
||||
});
|
||||
},
|
||||
HUB_SERVER_COLD_START_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it("fails when the requested port is already occupied", async () => {
|
||||
const owner = createInMemoryHubOwnerContext("hub-server-test-port-busy");
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
AgentToolContext,
|
||||
} from "@cline/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CLINE_INTERNAL_TELEMETRY_METADATA_KEY } from "../../services/telemetry/tool-context";
|
||||
import {
|
||||
SessionRuntime,
|
||||
type SessionRuntimeOrchestratorDeps,
|
||||
@@ -659,10 +660,15 @@ describe("SessionRuntime message preparation", () => {
|
||||
it("derives tool image support metadata from resolved provider model catalog", async () => {
|
||||
const { deps, configs } = withCapturingFakeRuntime();
|
||||
const execute = vi.fn(async () => "ok");
|
||||
const telemetry = {
|
||||
capture: vi.fn(),
|
||||
captureRequired: vi.fn(),
|
||||
} as unknown as AgentConfig["telemetry"];
|
||||
const session = new SessionRuntime(
|
||||
makeAgentConfig({
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
telemetry,
|
||||
tools: [
|
||||
{
|
||||
name: "read_file",
|
||||
@@ -712,8 +718,12 @@ it("derives tool image support metadata from resolved provider model catalog", a
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
expect(execute.mock.calls[0]).toEqual([expect.anything(), toolContext]);
|
||||
expect(runtimeConfig.toolContextMetadata).toEqual(
|
||||
expect.objectContaining({ modelSupportsImages: true }),
|
||||
expect.objectContaining({
|
||||
modelSupportsImages: true,
|
||||
[CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry,
|
||||
}),
|
||||
);
|
||||
expect(runtimeConfig.toolContextMetadata?.telemetry).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("SessionRuntime.run", () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
createAgentModelFromConfig,
|
||||
resolveKnownModelsFromConfig,
|
||||
} from "../../services/llms/handler-factory";
|
||||
import { CLINE_INTERNAL_TELEMETRY_METADATA_KEY } from "../../services/telemetry/tool-context";
|
||||
import { MessageBuilder } from "../../session/services/message-builder";
|
||||
import { ConversationStore } from "../../session/stores/conversation-store";
|
||||
import {
|
||||
@@ -753,6 +754,7 @@ export class SessionRuntime {
|
||||
modelSupportsImages:
|
||||
modelInfo?.capabilities?.includes("images") ?? true,
|
||||
...this.config.toolContextMetadata,
|
||||
[CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: this.telemetry,
|
||||
},
|
||||
hooks: this.createRuntimeHooks(),
|
||||
prepareTurn: this.createRuntimePrepareTurn(modelInfo, tools),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
captureCompactionSkipped,
|
||||
captureExtensionActivated,
|
||||
captureProviderConfigured,
|
||||
captureRunCommandsTimeout,
|
||||
captureTelemetryOptOut,
|
||||
captureWorkspaceInitError,
|
||||
captureWorkspaceInitialized,
|
||||
@@ -347,6 +348,99 @@ describe("captureCompactionSkipped", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("captureRunCommandsTimeout", () => {
|
||||
test("emits sdk.tool_timeout with sanitized timeout metadata", () => {
|
||||
const stub = createTelemetryStub();
|
||||
captureRunCommandsTimeout(stub.telemetry, {
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 1500,
|
||||
timeout_source: "default_setting",
|
||||
command_count: 2,
|
||||
duration_ms: 1502,
|
||||
ulid: "session-1",
|
||||
mode: "act",
|
||||
source: "sdk-test",
|
||||
session_id: "session-1",
|
||||
agent_id: "agent-1",
|
||||
conversation_id: "conv-1",
|
||||
run_id: "run-1",
|
||||
iteration: 3,
|
||||
tool_call_id: "tool-call-1",
|
||||
});
|
||||
|
||||
expect(stub.capture).toHaveBeenCalledTimes(1);
|
||||
expect(stub.captureRequired).not.toHaveBeenCalled();
|
||||
const { event, properties } = captureCallAt(stub, 0);
|
||||
expect(event).toBe(CORE_TELEMETRY_EVENTS.SDK.TOOL_TIMEOUT);
|
||||
expect(properties).toEqual({
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 1500,
|
||||
timeout_source: "default_setting",
|
||||
command_count: 2,
|
||||
duration_ms: 1502,
|
||||
ulid: "session-1",
|
||||
mode: "act",
|
||||
source: "sdk-test",
|
||||
session_id: "session-1",
|
||||
agent_id: "agent-1",
|
||||
conversation_id: "conv-1",
|
||||
run_id: "run-1",
|
||||
iteration: 3,
|
||||
tool_call_id: "tool-call-1",
|
||||
});
|
||||
expect(properties).not.toHaveProperty("command");
|
||||
expect(properties).not.toHaveProperty("commands");
|
||||
expect(properties).not.toHaveProperty("stdout");
|
||||
expect(properties).not.toHaveProperty("stderr");
|
||||
expect(properties).not.toHaveProperty("env");
|
||||
expect(properties).not.toHaveProperty("workspace_path");
|
||||
});
|
||||
|
||||
test("omits undefined optional properties", () => {
|
||||
const stub = createTelemetryStub();
|
||||
captureRunCommandsTimeout(stub.telemetry, {
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 1500,
|
||||
timeout_source: "default_setting",
|
||||
command_count: 1,
|
||||
duration_ms: 1502,
|
||||
mode: undefined,
|
||||
source: undefined,
|
||||
});
|
||||
|
||||
const { properties } = captureCallAt(stub, 0);
|
||||
expect(properties).toEqual({
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 1500,
|
||||
timeout_source: "default_setting",
|
||||
command_count: 1,
|
||||
duration_ms: 1502,
|
||||
});
|
||||
});
|
||||
|
||||
test("allows configured timeout source", () => {
|
||||
const stub = createTelemetryStub();
|
||||
captureRunCommandsTimeout(stub.telemetry, {
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 5000,
|
||||
timeout_source: "configured_setting",
|
||||
command_count: 1,
|
||||
duration_ms: 5001,
|
||||
ulid: "session-1",
|
||||
});
|
||||
|
||||
const { properties } = captureCallAt(stub, 0);
|
||||
expect(properties).toEqual({
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 5000,
|
||||
timeout_source: "configured_setting",
|
||||
command_count: 1,
|
||||
duration_ms: 5001,
|
||||
ulid: "session-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Telemetry-policy regression coverage.
|
||||
*
|
||||
@@ -493,6 +587,22 @@ describe("telemetry policy: helpers respect telemetry opt-out", () => {
|
||||
expect(emitRequired).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("captureRunCommandsTimeout never invokes captureRequired", () => {
|
||||
const { adapter, emitRequired } = createDisabledAdapter();
|
||||
const service = new TelemetryService({
|
||||
distinctId: "test-distinct-id",
|
||||
adapters: [adapter],
|
||||
});
|
||||
captureRunCommandsTimeout(service, {
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 1500,
|
||||
timeout_source: "default_setting",
|
||||
command_count: 2,
|
||||
duration_ms: 1502,
|
||||
});
|
||||
expect(emitRequired).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("a correctly-policed adapter drops these events when disabled", () => {
|
||||
// This test layers on top of the previous four to assert the *full*
|
||||
// end-to-end policy: when the adapter is disabled, a real adapter
|
||||
@@ -564,6 +674,13 @@ describe("telemetry policy: helpers respect telemetry opt-out", () => {
|
||||
thresholdRatio: 0.9,
|
||||
durationMs: 17,
|
||||
});
|
||||
captureRunCommandsTimeout(service, {
|
||||
tool_name: "run_commands",
|
||||
effective_timeout_ms: 1500,
|
||||
timeout_source: "default_setting",
|
||||
command_count: 2,
|
||||
duration_ms: 1502,
|
||||
});
|
||||
expect(observed).toEqual([]);
|
||||
expect(dropped).toEqual([
|
||||
"user.extension_activated",
|
||||
@@ -573,6 +690,7 @@ describe("telemetry policy: helpers respect telemetry opt-out", () => {
|
||||
"user.provider_configured",
|
||||
"task.compaction_executed",
|
||||
"task.compaction_skipped",
|
||||
"sdk.tool_timeout",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,9 +72,27 @@ export const CORE_TELEMETRY_EVENTS = {
|
||||
},
|
||||
SDK: {
|
||||
ERROR: SDK_ERROR_TELEMETRY_EVENT,
|
||||
TOOL_TIMEOUT: "sdk.tool_timeout",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface RunCommandsTimeoutTelemetryProperties {
|
||||
tool_name: "run_commands";
|
||||
effective_timeout_ms: number;
|
||||
timeout_source: "default_setting" | "configured_setting";
|
||||
command_count: number;
|
||||
duration_ms: number;
|
||||
ulid?: string;
|
||||
mode?: string;
|
||||
source?: string;
|
||||
session_id?: string;
|
||||
agent_id?: string;
|
||||
conversation_id?: string;
|
||||
run_id?: string;
|
||||
iteration?: number;
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceInitializedProperties {
|
||||
root_count: number;
|
||||
vcs_types: ReadonlyArray<string>;
|
||||
@@ -421,6 +439,27 @@ export function captureProviderApiError(
|
||||
});
|
||||
}
|
||||
|
||||
export function captureRunCommandsTimeout(
|
||||
telemetry: ITelemetryService | undefined,
|
||||
properties: RunCommandsTimeoutTelemetryProperties,
|
||||
): void {
|
||||
emit(
|
||||
telemetry,
|
||||
CORE_TELEMETRY_EVENTS.SDK.TOOL_TIMEOUT,
|
||||
stripUndefinedProperties(properties),
|
||||
);
|
||||
}
|
||||
|
||||
function stripUndefinedProperties(properties: object): TelemetryProperties {
|
||||
const result: TelemetryProperties = {};
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (value !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function captureMentionUsed(
|
||||
telemetry: ITelemetryService | undefined,
|
||||
mentionType:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ITelemetryService } from "@cline/shared";
|
||||
|
||||
export const CLINE_INTERNAL_TELEMETRY_METADATA_KEY =
|
||||
"__clineInternalTelemetry";
|
||||
|
||||
export function getToolContextTelemetry(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
): ITelemetryService | undefined {
|
||||
const telemetry = metadata?.[CLINE_INTERNAL_TELEMETRY_METADATA_KEY];
|
||||
return telemetry &&
|
||||
typeof telemetry === "object" &&
|
||||
"capture" in telemetry &&
|
||||
typeof telemetry.capture === "function"
|
||||
? (telemetry as ITelemetryService)
|
||||
: undefined;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type RootPackageJson = {
|
||||
|
||||
const TARGETS = ["dist", "node_modules"] as const;
|
||||
|
||||
const root = path.join(import.meta.dir, "..");
|
||||
const root = path.join(import.meta.dir, "..", "..");
|
||||
|
||||
async function readWorkspaceGlobs(): Promise<string[]> {
|
||||
const raw = await readFile(path.join(root, "package.json"), "utf8");
|
||||
|
||||
+11
-6
@@ -76,9 +76,10 @@ if (explicitVersion && !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(explicitVersion)) {
|
||||
|
||||
const SDK_PUBLISH_ORDER = ["shared", "llms", "agents", "core", "sdk"] as const;
|
||||
const MAIN_BRANCH = "main";
|
||||
const root = join(import.meta.dir, "..");
|
||||
const packagesDir = join(root, "packages");
|
||||
const cliDir = join(root, "apps/cli");
|
||||
const root = join(import.meta.dir, "..", "..");
|
||||
const sdkRoot = join(import.meta.dir, "..");
|
||||
const packagesDir = join(sdkRoot, "packages");
|
||||
const cliDir = join(sdkRoot, "apps/cli");
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -135,7 +136,11 @@ async function stageSdkReadmeForPublish(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await copyFile(join(root, "README.md"), destination, constants.COPYFILE_EXCL);
|
||||
await copyFile(
|
||||
join(sdkRoot, "README.md"),
|
||||
destination,
|
||||
constants.COPYFILE_EXCL,
|
||||
);
|
||||
return destination;
|
||||
}
|
||||
|
||||
@@ -407,11 +412,11 @@ async function releaseSDK(version: string): Promise<number> {
|
||||
// Step 2: Update versions
|
||||
// version.ts handles: version bump -> lockfile regeneration -> generate:models -> format -> build
|
||||
header("Step 2/5: Updating package versions and lockfile");
|
||||
await run(["bun", "scripts/version.ts", version]);
|
||||
await run(["bun", "scripts/version.ts", version], { cwd: sdkRoot });
|
||||
|
||||
// Step 3: Verify publishability
|
||||
header("Step 3/5: Verifying packed tarballs");
|
||||
await run(["bun", "scripts/check-publish.ts"]);
|
||||
await run(["bun", "scripts/check-publish.ts"], { cwd: sdkRoot });
|
||||
|
||||
// Step 4: Publish in dependency order
|
||||
header("Step 4/5: Publishing packages");
|
||||
|
||||
@@ -25,8 +25,9 @@ function incrementPatchVersion(input: string): string {
|
||||
return `${major}.${minor}.${Number(patch) + 1}`;
|
||||
}
|
||||
|
||||
const root = join(import.meta.dir, "..");
|
||||
const packagesDir = join(root, "packages");
|
||||
const root = join(import.meta.dir, "..", "..");
|
||||
const sdkRoot = join(import.meta.dir, "..");
|
||||
const packagesDir = join(sdkRoot, "packages");
|
||||
const dirs = await readdir(packagesDir, { withFileTypes: true });
|
||||
const workspaces = dirs.filter((d) => d.isDirectory()).map((d) => d.name);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"./packages/agents/src/*",
|
||||
"./packages/agents/src/*/index.ts"
|
||||
],
|
||||
"@cline/cline-hub": ["./apps/cline-hub/src/server.ts"],
|
||||
"@cline/core": ["./packages/core/src/index.ts"],
|
||||
"@cline/core/hub": ["./packages/core/src/hub/index.ts"],
|
||||
"@cline/core/hub/daemon-entry": [
|
||||
|
||||
Reference in New Issue
Block a user